浅谈J2EE架构下异常管理和错误跟踪
(在J2EE环境下尝试开发异常处理框架)
概述
回顾自己几年来所做过的J2EE项目。你可曾遇到过错误没有记录或记录不止一次的情形吗?你是否曾花费数不尽的时间来追踪BUG,而真正的原因是某些人在某个地方取消了异常处理?你的用户是否能够看到堆栈跟踪信息?如果你有过这样的经历,机会来了,你可能需要一种管理异常的通用策略,以及一些补救代码。
本文提供了在J2EE环境下开发一套策略连同错误处理框架的基础,欢迎大家一起参详。
前言
如果将Java领域中长久以来关于异常处理的辩论比作一场宗教战争,一点也不为过:一方面是,异常检测的拥护者主张调用者始终都应该处理他们调用的代码中所发生的错误情况。另一方面是,不检测异常的追随者指出异常检测会使代码变得混乱,并且经常不能够立刻被客户端处理,为什么还要强制进行呢?
作为一个初级程序员,我们首先是异常检测的簇拥者,但过了几年,在书写了很多,很多try-catch块后,我们会逐渐地将我们的“信仰”转变为后者。为什么会这样?我们形成了一套简单的处理错误情况的规则:
u 如果处理异常是有意义的,那么就做吧。
u 如果不处理异常,那么抛出它。
u 如果你不抛出它,那把它封装在一个不检测地基类异常中,然后抛出它。
但是对于那些上面没有提及的异常怎么处理呢?对于那些异常,我们建立最后的防线以确保错误信息能够被记录并合理地提示给用户。
本文向大家展示了另外一个异常处理框架,它源自“为J2EE创建应用级别的用户会话”一文所提及的灵巧的应用级用户会话(详细信息请参考该文)。J2EE应用使用该框架会:
u 对用户总是产生有意义的错误信息。
u 对没有处理的错误记录日志一次,且仅记录一次。
u 同一请求ID的相关异常记录在日志中,以便于更精确的跟踪调试信息。
u 对于所有层次提供一个完善的、易于扩展的,却又简单的策略机制。
为了更好的完成这个框架,我们有效地利用了AOP,设计模式,并且用XDoclet来产生代码。
为什么我们需要通用错误处理机制?
在项目初始阶段,决定架构是很有意义的:软件的各单元如何有效地互相作用?会话状态信息存放在何处?采用何种通讯协议?等等。然而时常会出现这样的情况,错误处理策略却未被包括在内。因而,一些随机的策略被实现了,随之而来的,每个开发人员都可以任意地决定错误信息如何定义,分类,模块化,以及处理。作为一名工程师,你当然最希望能够重新认识这种策略所能带来的结果:
u 冗余的日志:每一个try-catch块包含一条日志记录,多余地日志项目会“玷污”源程式。
u 多余地实现:同样类型的错误有不同的展示形式,使最终的处理变得复杂。
u 破坏封装:异常定义作为组件方法的一部分,打破了接口与实现之间的清晰界限。
u 混淆的异常定义:方法仅声明抛出java.lang.Exception异常。这样的话,客户端将不能确认异常相关地最细节的线索。
对于没有定义策略的通常解释就是“Java已经提供了错误处理”。这是事实,但Java也提供了一系列的方法来定义、传递、响应错误信息的方法。决定在实际的项目中如何组织这些方法是编程人员的责任。一些决定是必须要做的,包括以下:
u 应不应该检查:新的异常是否应该检查?
u 错误信息的消费者:当一没有处理的错误存在时谁需要知道,谁来负责记录日志并对操作人员进行提示?
u 基本的异常继承机制:什么样的异常信息应该被传送,什么样的异常语义可以通过继承反射机制来处理?
u 异常信息传递:定义的未经处理的异常或是传递到其它异常类中的异常,如何在分布式环境中传递?
u 解释:如何将未经处理的异常信息变为人们易读的,甚至是多语言的信息?
将规则封装入框架,但很仓促!
建造一个通用的异常处理策略,我们提供的“处方”包括以下几方面因素:
u 使用未经检测的异常:使用检测的异常,客户端不得不接受他们很少能够处理的错误。对于未经检测的异常则留给客户做出选择。当使用第三方的类库时,你不能控制异常是否被检测。在这种情况下,你需要封装未经检测的异常来捕获检测的异常。使用未经检测最大的权衡就是你再也不能强制客户端去处理它们了。然而,当被定义为接口的一部分时,他成为契约的关键部分,并继而成为Javadoc文档的一部分。
u 封装错误处理并在顶层加入一个错误处理器:有了安全的网络环境,就可以只关注于业务逻辑相关的异常信息。由处理器在指定层次按照标准的步骤(记录日志、系统提示、信息转换,等等)处理剩余的异常就可以完成一个优美的“本垒打”。
u 使用简易接口来组织异常继承机制:即使发现了新的异常也不要自动创建新的异常类。如果您简单地应付其它类型的变异异常,如果客户端能够明确地捕获它,问问自己,那不是很好。要记得异常是具有属性的,至少在某种程度上能够模拟不同状态的对象。
u 给终端用户传递有用的信息:未经处理的异常包含了不可预知的事件和错误。提示用户并将细节记录下来提供给技术人员。
尽管项目进程中需求、限制、异常继承、通知机制等可能会不同,但有些元素却是持久的。为什么我们不实现一个通用的策略框架,达到一劳永逸的效果呢?这个框架遵循简单易用的原则,与开发人员有着很好的交互接口,并且易于安装(使用jar格式)又有很好的文档(使用javadoc格式),不是很好吗?
但是,你不能要求开发小组延期错误处理直到策略和框架完全准备好。错误处理必须被确定,当第一个源文件被创建的同时。一个好的开始来自于定义一个基础的异常继承机制。
基本的例外层次
我们的实用任务是定义能够横跨项目的通用异常继承机制。基类是我们自己的未经检测的异常类UnrecoverableException,这样命名是由于历史的原因,可能会引入些许歧义。你也许可以考虑为你自己的类继承起一个更好的名字。
当你想要摆脱被检测的异常时,可以想象的一种状况是,客户端总能够处理这类异常。WrappedException提供了一般的简单传送机制:封装并抛出。WrappedException保留了异常起因作为内部引用,当原始异常类仍可用时能够运行的很好。当这不是实际情况时,使用SerializableException,它类似于WrappedException,除了假设客户端没有类库时仍能被使用外。
尽管我们更喜欢和推荐使用未经检测的异常,你可能仍保留使用被检测异常的选择。InstrumentedException接口作为一个仿效属性模式实现的接口,应用于被检测的和未经检测的异常。
下面的类图显示了我们基本的异常继承机制。
到目前为止,我们已经有了策略和一套待抛出的异常。现在是建立一个安全网络环境的时候了。
最后的防线
“为J2EE创建应用级别的用户会话”一文中为我们展示了一个“堡垒”,企业信息系统由一个多层的架构组成,业务逻辑层由无状态消息Bean驱动,客户层即可以是Web应用,也可以是独立的应用程式。在该框架中异常可以在任何一层被抛出,也可以被处理或直到调用的结尾才被处理。J2SE和J2EE服务器都能保证自己免受那些“迷离”错误以及RuntimeExceptions的干扰,通过将堆栈中的异常输出到Systom.out控制台中,记录日志,或者是执行一些其它的缺省操作。无论如何,如果用户获得了任何类型的输出,通常情况下,它被证明是完全没有意义的,更糟的情况是,错误很有可能会破坏程式本身的稳定。我们必须设置自己的堡垒以提供更健全的异常处理机制作为最后的一道防线。
异常可能存在于服务器端的EJB层和Web应用层,或者是在单独的应用程式中。一种情况是,虚拟机中产生的异常按照自己的方式传递到Web应用层。这就是我们安装顶层异常处理器的地方。
另外一种情况是,异常最终到达EJB容器的边缘,通过RMI连接到客户层。特别要注意的是,不要将那些服务器端专有的异常信息传送到客户端。例如,来自于框架的对象关系映射之类的。EJB异常处理器通过SerializableException类来完成处理责任。在客户层面,顶层的Swing异常处理器可以捕获任意一个游离的错误并采取相应的处理。
异常处理框架
“堡垒”框架中的异常处理器类实现了ExceptionHandler接口。该接口仅有一个带有两个参数的方法:当前的线程Thread和需要处理的异常Throwable。为了方便起见,框架提供了一个缺省的实现ExceptionHandlerBase,用户只要继承该类,提供抽象方法的实现就可以了。
下面的类图显示异常处理器的继承关系。
一些支持使用未经检测异常的人都认为Sun应该在所有的基于J2EE框架的EJB容器中加入“钩子”。这样就会允许定制错误处理、安全等方面的信息,而不必依靠信任度不高的供应商提供的框架。遗憾的是,Sun在EJB规范中并没有提供这样的机制。所以我们只有选用AOP框架来完成这方面的任务:
public class EJBExceptionHandler implements AroundAdvice {
private ExceptionHandler handler;
public EJBExceptionHandler() {
handler = ConfigHelper.getEJBExceptionHandler();
}
public Object invoke(JoinPoint joinPoint) throws Throwable {
Log log = LogFactory.getLog(joinPoint.getEnclosingStaticJoinPoint()
.getClass().getName());
log.debug("EJB Exception Handler bean context aspect!!");
try {
return joinPoint.proceed();
} catch (RuntimeException e) {
handler.handle(Thread.currentThread(), e);
} catch (Error e) {
handler.handle(Thread.currentThread(), e);
}
return null;
}
}
现行的处理器是通过ConfigHelper类来完成配制和维护工作的。如果业务逻辑Bean运行期间抛出运行时异常或错误时,处理器将会被要求来处理异常。
类DefaultEJBExceptionHandler将来自于Sun核心包以外的异常堆栈信息序列化入SerializableException,一方面客户端不存在的类的异常堆栈信息能够被传递出去,另一方面原始的异常信息会在传递的过程中丢失。
EJB容器能够如实地将产生的运行时异常(RuntimeException)和错误捕获,并将它们封装入java.rmi.RemoteException,如果客户端是远程的,则将它们封装入javax.ejb.EJBException。为了能够准确把握起因和跟踪堆栈信息,框架在客户端业务代理(BusinessDelegate)内部剥离了传递过来的无用的异常信息,然后重新抛出最原始的错误信息。
“堡垒”中的BusinessDelegate类将“不可知的”EJB接口暴露给客户端,同时将EJB本地和远程接口封装在内。BusinessDelegate类从EJB实现类中产生不同的XDoclet,按照下面的UML结构图:
类BusinessDelegate暴露出EJB实现类的所有业务方法,并将它们委派给相应的LocalProxy类和RemoteProxy类。通过这样两个代理类来处理EJB相关的异常,因此屏蔽了调用BusinessDelegate类的实现细节。下面所示的代码显示了LocalProxy类的一些方法:
public java.lang.String someOtherMethod() {
try {
return serviceInterface.someOtherMethod();
} catch (EJBException e) {
BusinessDelegateUtil.throwActualException(e);
}
return null; // Statement is never reached
}
变量serviceInterface存放EJB本地接口的引用。任何一个由EJB容器抛出的,表明不可期的错误信息的EJBException异常都由BusinessDelegateUtil类捕获和处理,如下面所示:
public static void throwActualException(EJBException e) {
doThrowActualException(e);
}
private static void doThrowActualException(Throwable actual) {
boolean done = false;
while(!done) {
if(actual instanceof RemoteException) {
actual = ((RemoteException)actual).detail;
} else if (actual instanceof EJBException) {
actual = ((EJBException)actual).getCausedByException();
} else {
done = true;
}
}
if(actual instanceof RuntimeException) {
throw (RuntimeException)actual;
} else if (actual instanceof Error) {
throw (Error)actual;
}
}
实际的异常信息被重新提取并抛出到顶级客户端异常处理器。当异常到传递到处理器时,堆栈信息将是服务器端所抛出的实际的错误信息。没有多余的客户端的附加信息被添加到里面。
Swing异常处理器
JVM为每一个控制线程提供缺省的顶级异常处理器。当运行的时候,该处理器将错误信息和异常信息堆砌到System.err,并杀掉该线程!对于用户而言,上面的那些信息是无用的,从调试的角度来讲,也带来了很大的麻烦。我们需要这样一种机制,当我们为了以后的调试而保留堆栈信息和唯一的请求ID时,允许我们给用户做出提示。在“为J2EE创建应用级别的用户会话”一文中讲述了在各层次的应用中怎样使请求ID变得有价值。
对于J2SE1.4来说,在线程实例中,未被捕获的异常,会引起线程组自身的uncaughtException()方法被执行。在应用程式中控制异常处理的简单方法就是扩展类ThreadGroup,覆盖uncaughtException()方法,并确保所有的线程实例都来自于定制的ThreadGroup类。
我们的框架基于J2SE1.3,采用了继承ThreadGroup的方法:
private static class SwingThreadGroup extends ThreadGroup {
private ExceptionHandler handler;
public SwingThreadGroup(ExceptionHandler handler) {
super("Swing ThreadGroup");
this.handler = handler;
}
public void uncaughtException(Thread t, Throwable e) {
handler.handle(t, e);
}
}
上面代码片断中所示的SwingThreadGroup类,覆盖了uncaughtException()方法,传递当前的线程实例并抛出Throwable到异常处理器中。
在我们提出控制客户端的所有的游离异常之前,施点小小的魔法是必要的。为了计划能够实施,所有的线程必须要实例化自我们定制的SwingThreadGroup。这是通过产生一个新的主线程实例并传递SwingThreadGroup实例来实现的。所有的线程实例来自于这个主线程实例,并自动加入SwingThreadGroup实例,因此,当未经检测的异常被抛出时使用我们新的异常处理器来处理。
框架在工具类SwingExceptionHandlerController中实现了该逻辑。应用提供了SwingMain接口的实现并传递异常控制器到其中。控制器必须被启动,旧的主线程才能够加入新的主线程并等待中断信号。下面的代码显示了示例应用如何完成既定的任务。方法createAndShowGUI()构成了应用的实体,完成初始化Swing组件和传送控制信息到用户端的任务。
public DemoApp() {
SwingExceptionHandlerController.setHandler(new DefaultSwingExceptionHandler());
SwingExceptionHandlerController.setMain(new SwingMain() {
public Component getParentComponent() {
return frame;
}
public void run() {
createAndShowGUI();
}
});
SwingExceptionHandlerController.start();
SwingExceptionHandlerController.join();
}
现在是时候在Swing层建立最后的防线了,但我们仍需要提供有意义的信息给用户。示例应用提供了最基本的实现,仅仅简单地通过对话框显示国际化的信息和唯一的请求ID作为有效的帮助。同时,异常信息通过唯一的请求ID被log4j记录到日志中。
更完善的错误处理可能会发送电子邮件,SNMP信息,或者提供请求ID相关的技术支持等等。最重要的一点是客户端和服务器端的日志都能够被过滤通过唯一的请求ID,对每一个请求ID提供最精确的错误信息。
图表6显示了对于请求ID lcffeb4:feb53del38:-7ffb Swing客户端和J2EE服务器日志提供的精确的跟踪记录,显示了异常是在何处被抛出的。注意堆栈跟踪包含的仅仅是服务器端异常的抛出信息。
向单独的J2SE应用中加入异常处理与基于Web应用是不同的。
WAR异常处理器
在J2EE架构的众多组件当中,Web应用组件是很幸运的,可以有他们自己的方式来设置异常处理器。通过配置描述文件web.xml,异常以及HTTP错误能够被映射到由Servlet或JSP页面做成的错误页面中。参考下面示例的web.xml文件片断:
ErrorHandlerServlet
dk.rhos.fw.rampart.util.errorhandling.ErrorHandlerServlet
ErrorHandlerServlet
/errorhandler
java.lang.Throwable
/errorhandler
这些标签直接将所有未捕获的异常定位到错误处理器,在这里被定位到ErrorHandlerServlet类。后者是一个目的明确的servlet,它的唯一角色就是Web组件与异常处理框架间的桥梁。当一个来自于Web应用的未捕获异常到达servlet容器后,一套包含了异常信息的参数将被设置到HttpServletRequest实例中,然后传递给ErrorHandlerServlet类的service方法。下面的代码示例了service()方法:
...
private static final String CONST_EXCEPTION =
"javax.servlet.error.exception";
...
protected void service( HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse)
throws ServletException, IOException
{
Throwable exception =
(Throwable)httpServletRequest.getAttribute(CONST_EXCEPTION);
ExceptionHandler handler = ConfigHelper.getWARExceptionHandler();
handler.handle(Thread.currentThread(), exception);
String responsePage = (String)ConfigHelper.getRequestContextFactory().
getRequestContext().
getAttribute(ExceptionConstants.CONST_RESPONSEPAGE);
if(responsePage == null) {
responsePage = "/error.jsp";
}
httpServletResponse.setStatus(HttpServletResponse.SC_OK);
RequestDispatcher dispatcher =
httpServletRequest.getRequestDispatcher(responsePage);
try {
dispatcher.include(httpServletRequest, httpServletResponse);
} catch (Exception e) {
log.error("Failed to dispatch error to responsePage " + responsePage, e);
}
}
在service()方法中,第一,来自于HttpServletRequest实例的实际的异常是通过关键字javax.servlet.error.exception重新获得的。第二,异常处理器实例是重新获得的。在这个层面上,处理器被调用,HttpServletRequest实例被定位到关键字rampart.servlet.exception.responsepage所指定的页面去。
类DefaultWARExceptionHandler查找异常信息中的国际化信息,并重定位输出到JSP页面error.jsp。然后该页面自由的显示信息给用户,包括当前的请求ID作为支持参考。更完善的机制能够很容易地通过扩展或替换处理器来实现。
封装
通常情况下,异常处理并不是足够的,因而调试和错误码跟踪变得复杂起来。因此,在系统开发开始时确定适当的策略和框架是至关重要的。虽然在这方面做事后补就是可行的,不过时间的花费是很惊人的。
本文仅是异常策略定义的一个起点,仅是向您介绍了一个简单,易于扩展的未捕获异常的继承处理机制。通过一个示例的J2EE应用演示了你应该如何建立顶层的异常处理器来提供最后的防线。
快点下载源代码,去尝试一下吧,用心去体会,就会有收获。
参考资料及源代码
-
-
-