111111
精灵王
精灵王
  • 注册日期2010-12-08
  • 发帖数640
  • QQ
  • 火币1103枚
  • 粉丝120
  • 关注75
  • 社区明星
阅读:7280回复:0

JAVA高级编程:EJB异常处理的最佳做法(2)

楼主#
更多 发布于:2010-12-15 12:43
  处理系统异常
  系统异常处理是比应用程式异常处理更为复杂的论题。由于会话 EJB 组件和实体 EJB 组件处理系统异常的方式相似,所以,对于本部分的所有示例,我们都将着重于实体 EJB 组件,不过请记住,其中的大部分示例也适用于处理会话 EJB 组件。
  当引用其他 EJB 远程接口时,实体 EJB 组件会碰到 RemoteException,而查找其他 EJB 组件时,则会碰到 NamingException,如果使用 bean 管理的持久性(BMP),则会碰到 SQLException。和这些类似的受查系统异常应该被捕捉并作为 EJBException 或他的一个子类抛出。原始的异常应被包装起来。清单 4 显示了一种处理系统异常的办法,这种办法和处理系统异常的 EJB 容器的行为一致。通过包装原始的异常并在实体 EJB 组件中将他重新抛出,你就确保了能够在想记录他的时候访问该异常。
  清单 4. 处理系统异常的一种常见方式
  try {
  OrderHome orderHome = EJBHomeFactory.getInstance().getOrderHome();
  Order order = orderHome.findByPrimaryKey(Integer id);
  } catch (NamingException ne) {
  throw new EJBException(ne);
  } catch (SQLException se) {
  throw new EJBException(se);
  } catch (RemoteException re) {
  throw new EJBException(re);
  }
  避免重复记录
  通常,异常记录发生在会话 EJB 组件中。但如果直接从 EJB 层外部访问实体 EJB 组件,又会怎么样呢?要是这样,你就不得不在实体 EJB 组件中记录异常并抛出他。这里的问题是,调用者没办法知道异常是否已被记录,因而非常可能再次记录他,从而导致重复记录。更重要的是,调用者没办法访问初始记录时所生成的唯一的标识。所有没有交叉引用机制的记录都是毫无用处的。
  请考虑这种最糟糕的情形:单机 java 应用程式访问了实体 EJB 组件中的一个方法 foo()。在一个名为 bar() 的会话 EJB 方法中也访问了同一个方法。一个 web 层客户机调用会话 EJB 组件的方法 bar() 并也记录了该异常。如果当从 Web 层调用会话 EJB 方法 bar() 时在实体 EJB 方法 foo() 中发生了一个异常,则该异常将被记录到三个地方:先是在实体 EJB 组件,然后是在会话 EJB 组件,最后是在 Web 层。而且,没有一个堆栈跟踪能被交叉引用!
  幸运的是,解决这些问题用常规办法就能非常容易地做到。你所需要的只是一种机制,使调用者能够:
  访问唯一的标识
  查明异常是否已被记录了。
  你能派生 EJBException 的子类来存储这样的信息。清单 5 显示了 LoggableEJBException 子类:
  清单 5. LoggableEJBException ? EJBException 的一个子类
  public class LoggableEJBException extends EJBException {
  protected boolean isLogged;
  protected String uniqueID;
  public LoggableEJBException(Exception exc) {
  super(exc);
  isLogged = false;
  uniqueID = ExceptionIDGenerator.getExceptionID();
  }
  ..
  ..
  }
  类 LoggableEJBException 有一个指示符标志(isLogged),用于检查异常是否已被记录了。每当捕捉一个 LoggableEJBException 时,看一下该异常是否已被记录了(isLogged == false)。如果 isLogged 为 false,则记录该异常并把标志设置为 true。
  ExceptionIDGenerator 类用当前时间和机器的主机名为异常生成唯一的标识。如果你喜欢,也能用有想象力的算法来生成这个唯一的标识。如果你在实体 EJB 组件中记录了异常,则这个异常将不会在别的地方被记录。如果你没有记录就在实体 EJB 组件中抛出了 LoggableEJBException,则这个异常将被记录到会话 EJB 组件中,但不记录到 Web 层中。
  清单 6 显示了使用这一技术重写后的清单 4。你还能继承 LoggableException 以适合于你的需要(通过给异常指定错误代码等)。
  清单 6. 使用 LoggableEJBException 的异常处理
  try {
  OrderHome orderHome = EJBHomeFactory.getInstance().getOrderHome();
  Order order = orderHome.findByPrimaryKey(Integer id);
  } catch (NamingException ne) {
  throw new LoggableEJBException(ne);
  } catch (SQLException se) {
  throw new LoggableEJBException(se);
  } catch (RemoteException re) {
  Throwable t = re.detail;
  if (t != null ;; t instanceof Exception) {
  throw new LoggableEJBException((Exception) re.detail);
  } else {
  throw new LoggableEJBException(re);
  }
  }
  记录 RemoteException
  从清单 6 中,你能看到 naming 和 SQL 异常在被抛出前被包装到了 LoggableEJBException 中。但 RemoteException 是以一种稍有不同 ? 而且要稍微花点气力 ? 的方式处理的。 会话 EJB 组件中的系统异常。
  如果你决定记录会话 EJB 异常,请使用清单 7 所示的记录代码;否则,请抛出异常,如清单 6 所示。你应该注意到,会话 EJB 组件处理异常可有一种和实体 EJB 组件不同的方式:因为大多数 EJB 系统都只能从 Web 层访问,而且会话 EJB 能作为 EJB 层的虚包,所以,把会话 EJB 异常的记录推迟到 Web 层实际上是有可能做到的。
  他之所以不同,是因为在 RemoteException 中,实际的异常将被存储到一个称为 detail(他是 Throwable 类型的)的公共属性中。在大多数情况下,这个公共属性保存有一个异常。如果你调用 RemoteException 的 printStackTrace,则除打印 detail 的堆栈跟踪之外,他还会打印异常本身的堆栈跟踪。你不必像这样的 RemoteException 的堆栈跟踪。
  为了把你的应用程式代码从错综复杂的代码(例如 RemoteException 的代码)中分离出来,这些行被重新构造成一个称为 ExceptionLogUtil 的类。有了这个类,你所要做的只是每当需要创建 LoggableEJBException 时调用 ExceptionLogUtil.createLoggableEJBException(e)。请注意,在清单 6 中,实体 EJB 组件并没有记录异常;不过,即便你决定在实体 EJB 组件中记录异常,这个解决方案仍然行得通。清单 7 显示了实体 EJB 组件中的异常记录:
  清单 7. 实体 EJB 组件中的异常记录
  try {
  OrderHome orderHome = EJBHomeFactory.getInstance().getOrderHome();
  Order order = orderHome.findByPrimaryKey(Integer id);
  } catch (RemoteException re) {
  LoggableEJBException le =
  ExceptionLogUtil.createLoggableEJBException(re);
  String traceStr = StackTraceUtil.getStackTrace(le);
  Category.getInstance(getClass().getName()).error(le.getUniqueID() +
  ":" + traceStr);
  le.setLogged(true);
  throw le;
  }
  你在清单 7 中看到的是个非常简单明了的异常记录机制。一旦捕捉受查系统异常就创建一个新的 LoggableEJBException。接着,使用类 StackTraceUtil 获取 LoggableEJBException 的堆栈跟踪,把他作为一个字符串。然后,使用 Log4J category 把该字符串作为一个错误加以记录。
  StackTraceUtil 类的工作原理
  在清单 7 中,你看到了一个新的称为 StackTraceUtil 的类。因为 Log4J 只能记录 String 消息,所以这个类负责解决把堆栈跟踪转换成 String 的问题。清单 8 说明了 StackTraceUtil 类的工作原理:
  清单 8. StackTraceUtil 类
  public class StackTraceUtil {
  public static String getStackTrace(Exception e)
  {
  StringWriter sw = new StringWriter();
  PrintWriter pw = new PrintWriter(sw);
  return sw.toString();
  }
  ..
  ..
  }
  java.lang.Throwable 中缺省的 printStackTrace() 方法把出错消息记录到 System.err。 Throwable 更有一个重载的 printStackTrace() 方法,他把出错消息记录到 PrintWriter 或 PrintStream。上面的 StackTraceUtil 中的方法把 StringWriter 包装到 PrintWriter 中。当 PrintWriter 包含有堆栈跟踪时,他只是调用 StringWriter 的 toString(),以获取该堆栈跟踪的 String 表示。
  Web 层的 EJB 异常处理
  在 Web 层设计中,把异常记录机制放到客户机端往往更容易也更高效。要能做到这一点,Web 层就必须是 EJB 层的唯一客户机。此外,Web 层必须建立在以下模式或框架之一的基础上:
  模式:业务委派(Business Delegate)、FrontController 或拦截过滤器(intercepting Filter)
  框架:Struts 或所有包含层次结构的类似于 MVC 框架的框架
  为什么异常记录应该在客户机端上发生呢?嗯,首先,控制尚未传到应用程式服务器之外。所谓的客户机层在 J2EE 应用程式服务器本身上运行,他由 JSP 页、servlet 或他们的助手类组成。其次,在设计良好的 Web 层中的类有一个层次结构(例如:在业务委派(Business Delegate)类、拦截过滤器(Intercepting Filter)类、http 请求处理程式(http request handler)类和 JSP 基类(JSP base class)中,或在 Struts Action 类中),或 FrontController servlet 形式的单点调用。这些层次结构的基类或 Controller 类中的中心点可能包含有异常记录代码。对于基于会话 EJB 记录的情况,EJB 组件中的每一个方法都必须具有记录代码。随着业务逻辑的增加,会话 EJB 方法的数量也会增加,记录代码的数量也会增加。Web 层系统将需要更少的记录代码。如果你的 Web 层和 EJB 层在同一地方并且不必支持所有其他类型的客户机,那么你应该考虑这一备用方案。不管怎样,记录机制不会改动;你能使用和前面的部分所描述的相同技术。
  真实世界的复杂性
  到目前为止,你已看到了简单情形的会话和实体 EJB 组件的异常处理技术。然而,应用程式异常的某些组合可能会更令人费解,并且有多种解释。清单 9 显示了一个示例。OrderEJB 的 ejbCreate() 方法试图获取 CustomerEJB 的一个远程引用,这会导致 FinderException。OrderEJB 和 CustomerEJB 都是实体 EJB 组件。你应该怎么解释 ejbCreate() 中的这个 FinderException 呢?是把他当作应用程式异常对待呢(因为 EJB 规范把他定义为标准应用程式异常),还是当作系统异常对待?
  清单 9. ejbCreate() 方法中的 FinderException
  public Object ejbCreate(OrderValue val) throws CreateException {
  try {
  if (value.getItemName() == null) {
  throw new CreateException("Cannot create Order without a name");
  }
  String custId = val.getCustomerId();
  Customer cust = customerHome.fingByPrimaryKey(custId);
  this.customer = cust;
  } catch (FinderException ne) {
  // How do you handle this Exception ?
  } catch (RemoteException re) {
  // This is clearly a System Exception
  throw ExceptionLogUtil.createLoggableEJBException(re);
  }
  return null;
  }
  虽然没有什么东西阻止你把 FinderException 当应用程式异常对待,但把他当系统异常对待会更好。原因是:EJB 客户机倾向于把 EJB 组件当黑箱对待。如果 createOrder() 方法的调用者获得了一个 FinderException,这对调用者并没有所有意义。 OrderEJB 正试图设置客户远程引用这件事对调用者来说是透明的。从客户机的角度看,失败仅仅意味着该订单无法创建。
  这类情形的另一个示例是,会话 EJB 组件试图创建另一个会话 EJB,因而导致了一个 CreateException。一种类似的情形是,实体 EJB 方法试图创建一个会话 EJB 组件,因而导致了一个 CreateException。这两个异常都应该当作系统异常对待。
  另一个可能碰到的挑战是会话 EJB 组件在他的某个容器回调方法中获得了一个 FinderException。你必须逐例处理这类情况。你可能要决定是把 FinderException 当应用程式异常还是系统异常对待。请考虑清单 1 的情况,其中调用者调用了会话 EJB 组件的 deleteOldOrder 方法。如果我们不是捕捉 FinderException,而是将他抛出,会怎么样呢?在这一特定情况中,把 FinderException 当系统异常对待似乎是符合逻辑的。这里的理由是,会话 EJB 组件倾向于在他们的方法中做许多工作,因为他们处理工作流情形,并且他们对调用者而言是黑箱。
  另一方面,请考虑会话 EJB 正在处理下订单的情形。要下一个订单,用户必须有一个简档 ? 但这个特定用户却还没有。业务逻辑可能希望会话 EJB 显式地通知用户她的简档丢失了。丢失的简档非常可能表现为会话 EJB 组件中的 javax.ejb.ObjectNotFoundException(FinderException 的一个子类)。在这种情况下,最佳的办法是在会话 EJB 组件中捕捉 ObjectNotFoundException 并抛出一个应用程式异常,让用户知道她的简档丢失了。
  即使是有了非常好的异常处理策略,另一个问题还是经常会在测试中出现,而且在产品中也更加重要。编译器和运行时优化会改动一个类的整体结构,这会限制你使用堆栈跟踪实用程式来跟踪异常的能力。这就是你需要代码重构的帮助的地方。你应该把大的方法调用分割为更小的、更易于管理的块。而且,只要有可能,异常类型需要多少就划分为多少;每次你捕捉一个异常,都应该捕捉已规定好类型的异常,而不是捕捉所有类型的异常。
  结束语
  我们已在本文讨论了非常多东西,你可能想知道我们已讨论的主要设计是否都物有所值。我的经验是,即便是在中小型项目中,在研发周期中,你的付出就已能看到回报,更不用说测试和产品周期了。此外,在宕机对业务具有毁灭性影响的生产系统中,良好的异常处理体系结构的重要性再怎么强调也不过分。
  我希望本文所展示的最佳做法对你有益。要深入理解这里提供的某些信息,请参看参考资料部分中的清单。
  参考资料
  你能阅读 Sun Microsystems 的 EJB 规范了解关于 EJB 体系结构的更多信息。
  Apache 的 Jakarta 项目有几个珍品。Log4J 框架即是其中之一。
  Struts 框架是 Jakarta 项目的另一个珍品。Struts 建立在 MVC 体系结构的基础上,提供了一个完全的分离,他把系统的表示层从系统的业务逻辑层中分离出来。
  要周详了解 Struts,请阅读 Malcom Davis 所写的讲述这个主题的非常受欢迎的文章“Struts, an open- source MVC implementation”(developerWorks,2001 年 2 月)。请注意:有一篇由 Wellie Chao 撰写的最新文章定于 2002 年夏季发表。
  你能通过阅读相关的 J2SE 文件了解关于新的 Java Logging API(java.util.logging)的更多信息。
  刚接触 J2EE?来自“WebSphere 研发者园地”的这篇文章告诉你怎么用 WebSphere Studio Application Developer 研发和测试 J2EE 应用程式(2001 年 10 月)。
  如果你想更多了解关于测试基于 EJB 的系统的知识,请从最近的 developerWorks 文章“Test flexibly with AspectJ and mock objects”(2002 年 5 月)开始。
  如果你不满足于单元测试,还想了解企业级系统测试的知识,请看看 IBM Performance Management, Testing, and Scalability Services 企业级测试库提供了什么。
  Sun 的 J2EE 模式 Web 站点着重于使用 J2EE 技术的模式、最佳做法、设计策略及经验证的解决方案。


喜欢0 评分0
游客

返回顶部