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

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

楼主#
更多 发布于:2010-12-15 12:43
 随着 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.out 和 System.err 映射到 ’ ’,在 UNIX 上,能映射到 dev/null。此外,如果你把 J2EE 应用程式服务器作为 NT 服务运行,甚至不会有控制台。即使你把控制台日志重定向到一个输出文件,当产品 J2EE 应用程式服务器重新启动时,这个文件非常可能也将被重写。
  异常处理的原则
  以下是一些普遍接受的异常处理原则:
  ☆ 如果无法处理某个异常,那就不要捕捉他。
  ☆ 如果捕捉了一个异常,请不要胡乱处理他。
  ☆ 尽量在靠近异常被抛出的地方捕捉异常。
  ☆ 在捕捉异常的地方将他记录到日志中,除非你打算将他重新抛出。
  ☆ 按照你的异常处理必须多精细来构造你的方法。
  ☆ 需要用几种类型的异常就用几种,尤其是对于应用程式异常。
  第 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 的子类抛出。例如, NullPointerException 或 ArrayOutOfBoundsException 将因代码中的错误而被抛出。另一种类型的系统异常在系统碰到设置不当的资源(例如,拼写错误的 JNDI 查找(JNDI lookup))时发生。在这种情况下,系统就将抛出一个受查异常。捕捉这些受查系统异常并将他们作为非受查异常(unchecked exception)抛出颇有意义。最重要的规则是,如果你对某个异常无能为力,那么他就是个系统异常并且应当作为非受查异常抛出。
  注:受查异常是个作为 java.lang.Exception 的子类的 Java 类。通过从 java.lang.Exception 派生子类,就强制你在编译时捕捉这个异常。相反地,非受查异常则是个作为 java.lang.RuntimeException 的子类的 Java 类。从 java.lang.RuntimeException 派生子类确保了编译器不会强制你捕捉这个异常。
  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 组件抛出的应用程式异常基本上限定为这三种类型(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);
  }

喜欢0 评分0
游客

返回顶部