EJB 异常处理的最佳做法(2)
实体 EJB 组件中的应用程序异常
清单 2 显示了实体 EJB 的一个 ejbCreate() 方法。这个方法的调用者传入一个 OrderItemValue 并请求创建一个 OrderItem 实体。因为 OrderItemValue 没有名称,所以抛出了 CreateException。
清单 2. 实体 EJB 组件中的样本 ejbCreate() 方法
public Integer ejbCreate(OrderItemValue value) throws CreateException {
if (value.getItemName() == null) {
throw new CreateException("Cannot create Order without a name");
}
..
..
return null;
}
清单 2 显示了 CreateException 的一个很典型的用法。类似地,如果方法的输入参数的值不正确,则查找程序方法将抛出 FinderException。
然而,如果您在使用容器管理的持久性(CMP),则开发者无法控制查找程序方法,从而 FinderException 永远不会被 CMP 实现抛出。尽管如此,在 Home 接口的查找程序方法的 throws 子句中声明 FinderException 还是要更好一些。RemoveException 是另一个应用程序异常,它在实体被删除时被抛出。
从实体 EJB 组件抛出的应用程序异常基本上限定为这三种类型(CreateException、FinderException 和 RemoveException)及它们的子类。多数应用程序异常都来源于会话 EJB 组件,因为那里是作出智能决策的地方。实体 EJB 组件一般是哑类,它们的唯一职责就是创建和取回数据。
会话 EJB 组件中的应用程序异常
清单 3 显示了来自会话 EJB 组件的一个方法。这个方法的调用者设法订购 n 件某特定类型的某商品。SessionEJB() 方法计算出仓库中的数量不够,于是抛出 NotEnoughStockException。NotEnoughStockException 适用于特定于业务的场合;当抛出了这个异常时,调用者会得到采用另一个备用途径的建议,让他订购更少数量的商品。
清单 3. 会话 EJB 组件中的样本容器回调方法
public ItemValueObject[] placeOrder(int n, ItemType itemType) throws
NotEnoughStockException {
//Check Inventory.
Collection orders = ItemHome.findByItemType(itemType);
if (orders.size() < n) {
throw NotEnoughStockException("Insufficient stock for " + itemType);
}
}
处理系统异常
系统异常处理是比应用程序异常处理更为复杂的论题。由于会话 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 技术的模式、最佳做法、设计策略以及经验证的解决方案。
教程“Java design patterns 101”(developerWorks,2002 年 1 月)是对设计模式的介绍。看看为什么模式对面向对象的设计和开发是有用而且重要的,以及模式是如何归档、分类以及编目的。该教程包含有一些重要模式及实现的示例。
请查看 developerWorks 教程主页,那里有来自 developerWorks Java 技术专区
的免费教程的完整清单。
您可以在 developerWorks Java 技术专区找到数以百计关于 Java 编程的方方面面的文章。
作者简介
Srikanth Shenoy 专门从事大型 J2EE 和 EAI 项目的体系结构、设计、开发和部署工作。他在 Java 平台一出现时就迷上了它,从此便全心投入。Srikanth 已经帮他的制造业、物流业和金融业客户实现了 Java 平台“一次编写,随处运行”的梦想。您可以通过 srikanth@srikanth.org 与他联系。
(全文完)
|