[ 永远的UNIX::UNIX技术资料的宝库 ]

首页 > 编程技术 > Java > 正文
 

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

作者:IBM DW 来源:IBM DW中国 (2007-02-01 13:06:55)

  处理系统异常

  系统异常处理是比应用程序异常处理更为复杂的论题。由于会话 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 技术的模式、最佳做法、设计策略以及经验证的解决方案。


(http://www.fanqiang.com)



 
 相关文章

 

★  感谢所有的作者为我们学习技术知识提供了一条捷径  ★
www.fanqiang.com