编程语言

img yukikaze

EJB 异常处理的最佳做法

发表于2004/10/15 15:14:00  1418人阅读

Srikanth Shenoysrikanth@srikanth.org
J2EE 顾问
2002 年 5 月

随着 J2EE 成为企业开发平台之选,越来越多基于 J2EE 的应用程序将投入生产。J2EE 平台的重要组件之一是 Enterprise JavaBean(EJB)API。J2EE 和 EJB 技术一起提供了许多优点,但随之而来的还有一些新的挑战。特别是企业系统,其中的任何问题都必须快速得到解决。在本文中,企业 Java 编程老手 Srikanth Shenoy 展现了他在 EJB 异常处理方面的最佳做法,这些做法可以更快解决问题。

在 hello-world 情形中,异常处理非常简单。每当碰到某个方法的异常时,就捕获该异常并打印堆栈跟踪或者声明这个方法抛出异常。不幸的是,这种办法不足以处理现实中出现的各种类型的异常。在生产系统中,当有异常抛出时,很可能是最终用户无法处理他或她的请求。当发生这样的异常时,最终用户通常希望能这样:

  • 有一条清楚的消息表明已经发生了一个错误

  • 有一个唯一的错误号,他可以据此访问可方便获得的客户支持系统

  • 问题快速得到解决,并且可以确信他的请求已经得到处理,或者将在设定的时间段内得到处理

理想情况下,企业级系统将不仅为客户提供这些基本的服务,还将准备好一些必要的后端机制。举例来说,客户服务小组应该收到即时的错误通知,以便在客户打电话求助之前服务代表就能意识到问题。此外,服务代表应该能够交叉引用用户的唯一错误号和产品日志,从而快速识别问题 — 最好是能把问题定位到确切的行号或确切的方法。为了给最终用户和支持小组提供他们需要的工具和服务,在构建一个系统时,您就必须对系统被部署后可能出问题的所有地方心中有数。

在本文中,我们将谈谈基于 EJB 的系统中的异常处理。我们将从回顾异常处理的基础知识开始,包括日志实用程序的使用,然后,很快就转入对 EJB 技术如何定义和管理不同类型的异常进行更详细的讨论。此后,我们将通过一些代码示例来研究一些常见的异常处理解决方案的优缺点,我还将展示我自己在充分利用 EJB 异常处理方面的最佳做法。

请注意,本文假设您熟悉 J2EE 和 EJB 技术。您应理解实体 bean 和会话 bean 的差异。如果您对 bean 管理的持久性(bean-managed persistence(BMP))和容器管理的持久性(container-managed persistence(CMP))在实体 bean 上下文中是什么意思稍有了解,也是有帮助的。请参阅参考资料部分了解关于 J2EE 和 EJB 技术的更多信息。

异常处理基础知识
解决系统错误的第一步是建立一个与生产系统具有相同构造的测试系统,然后跟踪导致抛出异常的所有代码,以及代码中的所有不同分支。在分布式应用程序中,很可能是调试器不工作了,所以,您可能将用 System.out.println() 方法跟踪异常。System.out.println 尽管很方便,但开销巨大。在磁盘 I/O 期间,System.out.println 对 I/O 处理进行同步,这极大降低了吞吐量。在缺省情况下,堆栈跟踪被记录到控制台。但是,在生产系统中,浏览控制台以查看异常跟踪是行不通的。而且,不能保证堆栈跟踪会显示在生产系统中,因为,在 NT 上,系统管理员可以把 System.outSystem.err 映射到 ' ',在 UNIX 上,可以映射到 dev/null。此外,如果您把 J2EE 应用程序服务器作为 NT 服务运行,甚至不会有控制台。即使您把控制台日志重定向到一个输出文件,当产品 J2EE 应用程序服务器重新启动时,这个文件很可能也将被重写。

异常处理的原则
以下是一些普遍接受的异常处理原则:
  1. 如果无法处理某个异常,那就不要捕获它。
  2. 如果捕获了一个异常,请不要胡乱处理它。
  3. 尽量在靠近异常被抛出的地方捕获异常。
  4. 在捕获异常的地方将它记录到日志中,除非您打算将它重新抛出。
  5. 按照您的异常处理必须多精细来构造您的方法。
  6. 需要用几种类型的异常就用几种,尤其是对于应用程序异常。

第 1 点显然与第 3 点相抵触。实际的解决方案是以下两者的折衷:您在距异常被抛出多近的地方将它捕获;在完全丢失原始异常的意图或内容之前,您可以让异常落在多远的地方。

:尽管这些原则的应用遍及所有 EJB 异常处理机制,但它们并不是特别针对 EJB 异常处理的。

由于以上这些原因,把代码组装成产品并同时包含 System.out.println 并不是一种选择。在测试期间使用 System.out.println,然后在形成产品之前除去 System.out.println 也不是上策,因为这样做意味着您的产品代码与测试代码运行得不尽相同。您需要的是一种声明控制日志机制,以使您的测试代码和产品代码相同,并且当记录日志以声明方式关闭时,给产品带来的性能开销最小。

这里的解决方案显然是使用一个日志实用程序。采用恰当的编码约定,日志实用程序将负责精确地记录下任何类型的消息,不论是系统错误还是一些警告。所以,我们将在进一步讲述之前谈谈日志实用程序。

日志领域:鸟瞰
每个大型应用程序在开发、测试及产品周期中都使用日志实用程序。在今天的日志领域中,有几个角逐者,其中有两个广为人知。一个是 Log4J,它是来自 Apache 的 Jakarta 的一个开放源代码的项目。另一个是 J2SE 1.4 捆绑提供的,它是最近刚加入到这个行列的。我们将使用 Log4J 说明本文所讨论的最佳做法;但是,这些最佳做法并不特别依赖于 Log4J。

Log4J 有三个主要组件:layout、appender 和 category。Layou 代表消息被记录到日志中的格式。appender 是消息将被记录到的物理位置的别名。而 category 则是有名称的实体:您可以把它当作是日志的句柄。layout 和 appender 在 XML 配置文件中声明。每个 category 带有它自己的 layout 和 appender 定义。当您获取了一个 category 并把消息记录到它那里时,消息在与该 category 相关联的各个 appender 处结束,并且所有这些消息都将以 XML 配置文件中指定的 layout 格式表示。

Log4J 给消息指定四种优先级:它们是 ERROR、WARN、INFO 和 DEBUG。为便于本文的讨论,所有异常都以具有 ERROR 优先级记录。当记录本文中的一个异常时,我们将能够找到获取 category(使用 Category.getInstance(String name) 方法)的代码,然后调用方法 category.error()(它与具有 ERROR 优先级的消息相对应)。

尽管日志实用程序能帮助我们把消息记录到适当的持久位置,但它们并不能根除问题。它们不能从产品日志中精确找出某个客户的问题报告;这一便利技术留给您把它构建到您正在开发的系统中。

要了解关于 Log4J 日志实用程序或 J2SE 所带的日志实用程序的更多信息,请参阅参考资料部分。

异常的类别
异常的分类有不同方式。这里,我们将讨论从 EJB 的角度如何对异常进行分类。EJB 规范将异常大致分成三类:

  • JVM 异常:这种类型的异常由 JVM 抛出。OutOfMemoryError 就是 JVM 异常的一个常见示例。对 JVM 异常您无能为力。它们表明一种致命的情况。唯一得体的退出办法是停止应用程序服务器(可能要增加硬件资源),然后重新启动系统。

  • 应用程序异常:应用程序异常是一种定制异常,由应用程序或第三方的库抛出。这些本质上是受查异常(checked exception);它们预示了业务逻辑中的某个条件尚未满足。在这样的情况下,EJB 方法的调用者可以得体地处理这种局面并采用另一条备用途径。

  • 系统异常:在大多数情况下,系统异常由 JVM 作为 RuntimeException 的子类抛出。例如,NullPointerExceptionArrayOutOfBoundsException 将因代码中的错误而被抛出。另一种类型的系统异常在系统碰到配置不当的资源(例如,拼写错误的 JNDI 查找(JNDI lookup))时发生。在这种情况下,系统就将抛出一个受查异常。捕获这些受查系统异常并将它们作为非受查异常(unchecked exception)抛出颇有意义。最重要的规则是,如果您对某个异常无能为力,那么它就是一个系统异常并且应当作为非受查异常抛出。

受查异常是一个作为 java.lang.Exception 的子类的 Java 类。通过从 java.lang.Exception 派生子类,就强制您在编译时捕获这个异常。相反地,非受查异常则是一个作为 java.lang.RuntimeException 的子类的 Java 类。从 java.lang.RuntimeException 派生子类确保了编译器不会强制您捕获这个异常。

EJB 容器怎样处理异常
EJB 容器拦截 EJB 组件上的每一个方法调用。结果,方法调用中发生的每一个异常也被 EJB 容器拦截到。EJB 规范只处理两种类型的异常:应用程序异常和系统异常。

EJB 规范把应用程序异常定义为在远程接口中的方法说明上声明的任何异常(而不是 RemoteException)。应用程序异常是业务工作流中的一种特殊情形。当这种类型的异常被抛出时,客户机会得到一个恢复选项,这个选项通常是要求以一种不同的方式处理请求。不过,这并不意味着任何在远程接口方法的 throws 子句中声明的非受查异常都会被当作应用程序异常对待。EJB 规范明确指出,应用程序异常不应继承 RuntimeException 或它的子类。

当发生应用程序异常时,除非被显式要求(通过调用关联的 EJBContext 对象的 setRollbackOnly() 方法)回滚事务,否则 EJB 容器就不会这样做。事实上,应用程序异常被保证以它原本的状态传送给客户机:EJB 容器绝不会以任何方式包装或修改异常。

系统异常被定义为受查异常或非受查异常,EJB 方法不能从这种异常恢复。当 EJB 容器拦截到非受查异常时,它会回滚事务并执行任何必要的清理工作。接着,它把该非受查异常包装到 RemoteException 中,然后抛给客户机。这样,EJB 容器就把所有非受查异常作为 RemoteException(或者作为其子类,例如 TransactionRolledbackException)提供给客户机。

对于受查异常的情况,容器并不会自动执行上面所描述的内务处理。要使用 EJB 容器的内部内务处理,您将必须把受查异常作为非受查异常抛出。每当发生受查系统异常(如 NamingException)时,您都应该通过包装原始的异常抛出 javax.ejb.EJBException 或其子类。因为 EJBException 本身是非受查异常,所以不需要在方法的 throws 子句中声明它。EJB 容器捕获 EJBException 或其子类,把它包装到 RemoteException 中,然后把 RemoteException 抛给客户机。

虽然系统异常由应用程序服务器记录(这是 EJB 规范规定的),但记录格式将因应用程序服务器的不同而异。为了访问所需的统计信息,企业常常需要对所生成的日志运行 shell/Perl 脚本。为了确保记录格式的统一,在您的代码中记录异常会更好些。

:EJB 1.0 规范要求把受查系统异常作为 RemoteException 抛出。从 EJB 1.1 规范起规定 EJB 实现类绝不应抛出 RemoteException

常见的异常处理策略
如果没有异常处理策略,项目小组的不同开发者很可能会编写以不同方式处理异常的代码。由于同一个异常在系统的不同地方可能以不同的方式被描述和处理,所以,这至少会使产品支持小组感到迷惑。缺乏策略还会导致在整个系统的多个地方都有记录。日志应该集中起来或者分成几个可管理的单元。理想的情况是,应在尽可能少的地方记录异常日志,同时不损失内容。在这一部分及其后的几个部分,我将展示可以在整个企业系统中以统一的方式实现的编码策略。您可以从参考资料部分下载本文开发的实用程序类。

清单 1 显示了来自会话 EJB 组件的一个方法。这个方法删除某个客户在特定日期前所下的全部订单。首先,它获取 OrderEJB 的 Home 接口。接着,它取回某个特定客户的所有订单。当它碰到在某个特定日期之前所下的订单时,就删除所订购的商品,然后删除订单本身。请注意,抛出了三个异常,显示了三种常见的异常处理做法。(为简单起见,假设编译器优化未被使用。)

清单 1. 三种常见的异常处理做法

100  try {
101    OrderHome homeObj = EJBHomeFactory.getInstance().getOrderHome();
102    Collection orderCollection = homeObj.findByCustomerId(id);
103    iterator orderItter = orderCollection.iterator();
104    while (orderIter.hasNext()) {
105      Order orderRemote = (OrderRemote) orderIter.getNext();
106      OrderValue orderVal = orderRemote.getValue();
107      if (orderVal.getDate() < "mm/dd/yyyy") {
108        OrderItemHome itemHome = 
              EJBHomeFactory.getInstance().getItemHome();
109        Collection itemCol = itemHome.findByOrderId(orderId)
110        Iterator itemIter = itemCol.iterator();
111        while (itemIter.hasNext()) {
112          OrderItem item = (OrderItem) itemIter.getNext();
113          item.remove();
114        }
115        orderRemote.remove();
116      }
117    }
118  } catch (NamingException ne) {
119    throw new EJBException("Naming Exception occurred");
120  } catch (FinderException fe) {
121    fe.printStackTrace();
122    throw new EJBException("Finder Exception occurred");
123  } catch (RemoteException re) {
124    re.printStackTrace();
125    //Some code to log the message
126    throw new EJBException(re);
127  }

现在,让我们用上面所示的代码来研究一下所展示的三种异常处理做法的缺点。

抛出/重抛出带有出错消息的异常
NamingException 可能发生在行 101 或行 108。当发生 NamingException 时,这个方法的调用者就得到 RemoteException 并向后跟踪该异常到行 119。调用者并不能告知 NamingException 实际是发生在行 101 还是行 108。由于异常内容要直到被记录了才能得到保护,所以,这个问题的根源很难查出。在这种情形下,我们就说异常的内容被“吞掉”了。正如这个示例所示,抛出或重抛出一个带有消息的异常并不是一种好的异常处理解决办法。

记录到控制台并抛出一个异常
FinderException 可能发生在行 102 或 109。不过,由于异常被记录到控制台,所以仅当控制台可用时调用者才能向后跟踪到行 102 或 109。这显然不可行,所以异常只能被向后跟踪到行 122。这里的推理同上。

包装原始的异常以保护其内容
RemoteException 可能发生在行 102、106、109、113 或 115。它在行 123 的 catch 块被捕获。接着,这个异常被包装到 EJBException 中,所以,不论调用者在哪里记录它,它都能保持完整。这种办法比前面两种办法更好,同时演示了没有日志策略的情况。如果 deleteOldOrders() 方法的调用者记录该异常,那么将导致重复记录。而且,尽管有了日志记录,但当客户报告某个问题时,产品日志或控制台并不能被交叉引用。

EJB 异常处理探试法
EJB 组件应抛出哪些异常?您应将它们记录到系统中的什么地方?这两个问题盘根错结、相互联系,应该一起解决。解决办法取决于以下因素:

  • 您的 EJB 系统设计:在良好的 EJB 设计中,客户机绝不调用实体 EJB 组件上的方法。多数实体 EJB 方法调用发生在会话 EJB 组件中。如果您的设计遵循这些准则,则您应该用会话 EJB 组件来记录异常。如果客户机直接调用了实体 EJB 方法,则您还应该把消息记录到实体 EJB 组件中。然而,存在一个难题:相同的实体 EJB 方法可能也会被会话 EJB 组件调用。在这种情形下,如何避免重复记录呢?类似地,当一个会话 EJB 组件调用其它实体 EJB 方法时,您如何避免重复记录呢?很快我们就将探讨一种处理这两种情况的通用解决方案。(请注意,EJB 1.1 并未从体系结构上阻止客户机调用实体 EJB 组件上的方法。在 EJB 2.0 中,您可以通过为实体 EJB 组件定义本地接口规定这种限制。)

  • 计划的代码重用范围:这里的问题是您是打算把日志代码添加到多个地方,还是打算重新设计、重新构造代码来减少日志代码。

  • 您要为之服务的客户机的类型:考虑您是将为 J2EE Web 层、单机 Java 应用程序、PDA 还是将为其它客户机服务是很重要的。Web 层设计有各种形状和大小。如果您在使用命令(Command)模式,在这个模式中,Web 层通过每次传入一个不同的命令调用 EJB 层中的相同方法,那么,把异常记录到命令在其中执行的 EJB 组件中是很有用的。在多数其它的 Web 层设计中,把异常记录到 Web 层本身要更容易,也更好,因为您需要把异常日志代码添加到更少的地方。如果您的 Web 层和 EJB 层在同一地方并且不需要支持任何其它类型的客户机,那么就应该考虑后一种选择。

  • 您将处理的异常的类型(应用程序或系统):处理应用程序异常与处理系统异常有很大不同。系统异常的发生不受 EJB 开发者意图的控制。因为系统异常的含义不清楚,所以内容应指明异常的上下文。您已经看到了,通过对原始异常进行包装使这个问题得到了最好的处理。另一方面,应用程序异常是由 EJB 开发者显式抛出的,通常包装有一条消息。因为应用程序异常的含义清楚,所以没有理由要保护它的上下文。这种类型的异常不必记录到 EJB 层或客户机层;它应该以一种有意义的方式提供给最终用户,带上指向所提供的解决方案的另一条备用途径。系统异常消息没必要对最终用户很有意义。

处理应用程序异常
在这一部分及其后的几个部分中,我们将更仔细地研究用 EJB 异常处理应用程序异常和系统异常,以及 Web 层设计。作为这个讨论的一部分,我们将探讨处理从会话和实体 EJB 组件抛出的异常的不同方式。

实体 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 组件抛出的应用程序异常基本上限定为这三种类型(CreateExceptionFinderExceptionRemoveException)及它们的子类。多数应用程序异常都来源于会话 EJB 组件,因为那里是作出智能决策的地方。实体 EJB 组件一般是哑类,它们的唯一职责就是创建和取回数据。

会话 EJB 组件中的应用程序异常
清单 3 显示了来自会话 EJB 组件的一个方法。这个方法的调用者设法订购 n 件某特定类型的某商品。SessionEJB() 方法计算出仓库中的数量不够,于是抛出 NotEnoughStockExceptionNotEnoughStockException 适用于特定于业务的场合;当抛出了这个异常时,调用者会得到采用另一个备用途径的建议,让他订购更少数量的商品。

清单 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 类型的)的公共属性中。在大多数情况下,这个公共属性保存有一个异常。如果您调用 RemoteExceptionprintStackTrace,则除打印 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.errThrowable 还有一个重载的 printStackTrace() 方法,它把出错消息记录到 PrintWriterPrintStream。上面的 StackTraceUtil 中的方法把 StringWriter 包装到 PrintWriter 中。当 PrintWriter 包含有堆栈跟踪时,它只是调用 StringWritertoString(),以获取该堆栈跟踪的 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 显示了一个示例。OrderEJBejbCreate() 方法试图获取 CustomerEJB 的一个远程引用,这会导致 FinderExceptionOrderEJBCustomerEJB 都是实体 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.ObjectNotFoundExceptionFinderException 的一个子类)。在这种情况下,最好的办法是在会话 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 的照片Srikanth Shenoy 专门从事大型 J2EE 和 EAI 项目的体系结构、设计、开发和部署工作。他在 Java 平台一出现时就迷上了它,从此便全心投入。Srikanth 已经帮他的制造业、物流业和金融业客户实现了 Java 平台“一次编写,随处运行”的梦想。您可以通过
srikanth@srikanth.org 与他联系。
阅读全文
0 0

相关文章推荐

img
取 消
img