站内搜索: 请输入搜索关键词

当前页面: 开发资料首页J2EE 专题J2EE中的异常管理及错误跟踪

J2EE中的异常管理及错误跟踪

摘要: 回顾一下你上一个J2EE工程,是否遇到过类似错误没有记入日志或者被多次记录的情况?是否只是因为在某处代码吃掉了异常导致你花费无数次时间来跟踪一个bug?是否你的用户直接看到了堆栈的跟踪信息?如果这样的话,你可能需要一种通用的异常管理的策略和一些补充的代码。这篇文章为你提供了在J2EE项目中通过使用错误处理框架使用一些策略的基础
J2EE中的异常管理及错误跟踪

-为J2EE定制一个用来处理错误的异常处理框架

作者:Kåre Kjelstrøm/Jens Schjærff Byager

翻译:xMatrix


版权声明:可以任意转载,转载时请务必以超链接形式标明文章原始出处和作者信息及本声明
原文地址:
http://www.javaworld.com/javaworld/jw-07-2005/jw-0711-exception.html
中文地址:
http://www.matrix.org.cn/resource/article/43/43744_J2EE_Exception.html
关键词: J2EE Exception


摘要
回顾一下你上一个J2EE工程,是否遇到过类似错误没有记入日志或者被多次记录的情况?是否只是因为在某处代码吃掉了异常导致你花费无数次时间来跟踪一个bug?是否你的用户直接看到了堆栈的跟踪信息?如果这样的话,你可能需要一种通用的异常管理的策略和一些补充的代码。这篇文章为你提供了在J2EE项目中通过使用错误处理框架使用一些策略的基础。(3100个英文单词,2005年7月11日)


Java中关于异常处理的争论可以被认为是一种信仰上的争执:一方面,强制异常(checked exceptions)的支持者认为调用者应该处理他们调用代码出现的异常;另一方面,非强制(unchecked exceptions)异常的追随者认为强制异常混乱了代码,而且通常客户端不能立即处理,那为什么还要检查他呢。

作为初级工程师,我们首先信奉的是强制异常,但几年后,在使用N久的try/catch/finally后,我们开始转向非强制异常了。因为我们开始相信一些处理错误状况的基本规则:
如果需要处理异常,那么就处理
如果处理不了,就抛出
如果抛不了,就用非强制的基类异常包装后再抛出

但这些异常被抛到最顶层时会怎么样呢?对这种情况,我们有一个底线确保错误信息被记录并且用户得到正确的提示。

本文提供了另外一种框架来处理异常,它扩展了“Create an Application-Wide User Session for J2EE”(JavaWorld, 2005年3月)所提出的企业应用session工具。使用此框架的J2EE应用将:

总是向用户提供有意义的错误信息
记下未处理的错误环境,并且只记录一次
在日志文件中用唯一的请求ID号对异常进行编号,以便进行高精度的调试
在各层中设置一个强壮的、可扩展的,而又简单的策略来处理异常

为了搭建框架,我们运用了面向状态编程(AOP,aspect-oriented programming)、设计模式和使用XDoclet进行代码生成。

你可以在资源中找到所有代码及一个使用框架的J2EE应用。这些源程序组成了一个名为Rampart的完整框架,当初是为丹麦哥本哈根基于J2EE的电子保健系统应用(EHR, electronic healthcare records)而开发的。

为什么我们需要通用的错误处理方法

在项目的开始,我们会做一些关键性的系统架构决定,如:系统中的元素如何交互?会话状态保存在哪儿?哪种通信协议会被使用等等。但这里并没有包含错误处理。因而每个开发人员都可以任意决定如何定义、分类、建模和处理错误。作为一个开发人员,你可以想象在这种方式下的结果:
1. 臃肿的日志:每个try/catch都包含log语句,这导致被污染的代码生成臃肿和多余的日志入口。
2. 多余的实现:同一类型的错误有不同的表示,这导致处理的复杂化。
3. 破碎的封装:来自其他组件的异常被定义为方法标识的一部分,这导致接口和实现的分离被打破了。
4. 不明确的异常定义:方法签名通常采用抛出java.lang.Exception,这导致客户端不能明确得到方法错误的语义。

通常没有定义异常处理策略的借口是:java已经提供了异常处理。这是事实,java也提供一贯的定义、通信、传播及响应异常的工具。但开发人员需要决定如何在实际的项目中使用这些服务。几个方面是必须要考虑的,如:
1. 检查或不检查异常:是否应该检查或不检查新异常类?
2. 异常的使用者:究竟是谁需要知道什么时候会发生未处理的异常及由谁来负责记录及通知操作人员?
3. 基础的异常层次:异常需要包含什么信息及异常层次需要反映什么语义?
4. 传递;是否未处理的异常会被定义或传递给别的异常类,及他们如何在分布式环境中传递?
5. 解释:未处理的异常如何被解释为可阅读的,甚至支持多语言的信息?

在框架中封装规则,要快!

我们给出的通用异常处理策略是基于如下的因素:
使用非强制的异常:使用强制异常,调用者要被迫处理他们几乎不能处理的错误。非强制的异常则给调用者一个选择。在使用第三方类库时,你不能控制异常是强制或非强制的。这种情况下,你需要用非强制异常来包含强制异常。在使用非强制异常时,最大的让步是你不能再强制调用者来处理异常了。然而作为接口定义的一部分,异常仍是约定的关键部分并且继续成为Javadoc文档的一部分。

封装异常处理并在每一层的顶层提供处理器:你可以专注于只处理业务逻辑相关的异常。处理器可以为特定层剩余的异常执行标准操作:记录日志、系统管理提示及转换等等。

通过“简单生活”方式来建模异常类层次:不要在发现新的错误类型时就创建新的异常类。首先问一下是否可以作为其他类型的变体来对待或者调用者确实需要捕获。记住异常至少在某方面是可以用他的属性来为不同的状况建立变化模型的对象。较少的异常类在开始时是足够的,但也仅在这种情况下可能需要用特定属性来处理。

提供有意义的信息给使用者:未处理的异常代表不可预知的事件和问题。告诉用户并且保存细节给技术支持人员。

虽然在不同的项目中需求、限制、异常层次及通知机制会有所不同,但许多元素还是一致的。因此为什么不完全地通过框架实现通用的策略呢?依据简单使用原则的框架是强制使用策略的最好方法。通过jar文件与javadoc之类的可执行工件与开发人员对话比白纸和幻灯片更容易表示架构准则。

然而,你不能要求开发团队直到异常处理策略及附加的框架支持准备完毕后才开始错误处理。错误处理必须在第一个源文件创建时确定。一个好的启动方法是定义基础的异常层次。


基础异常层次

我们首要的任务是定义一个可以跨项目的通用异常层次。这里的非强制异常基类是UnrecoverableException,由于历史原因,这个名字可能会有些误导。你可以在自己的层次中使用更好的名字

当你不想使用强制异常时WrappedException可以提供一种简单通用的传送机制:包裹原来的异常并重新抛出。WrappedException保存原始异常作为内部引用,这使得当类需要原始异常时也可以可以正常工作。当这不重要时,你可以使用SerializableException,他类似于WrappedException,此外还可以在客户端没有对类库作任何假设的情况下使用。

虽然我们偏好和推荐非强制异常,但你可以保留强制异常作为可选项。InstrumentedException是一个支持强制非强制异常的接口,他遵循一定属性实现模式。他允许异常处理者一致地检查来源页不需要考虑是来自强制或非强制的异常。

下面的类图显示了我们基础的异常层次。


这时候我们已经拥有了一个策略及相应的一组可以被抛出的异常。现在是时候建立安全网了。


防守的底线

“创建应用范围的用户会话”这篇文章描述了Rampart,一个使用了由企业信息系统层,基于无状态会话bean的业务层及基于网页和标准J2SE客户端的客户层的分层架构。异常可以从任意层次抛出,可以在线处理或者延迟到调用链的最终端。J2SE和J2EE应用服务器都可以通过捕获未处理的Errors和RuntimeExceptions来抵御侵入性的行为,通过输出栈信息、记入日志或者执行其他默认的操作。在任何情况下,用户都不应该看到输出信息,通常是没有意义的甚至影响程序稳定性的错误。因此我们必须构建自己的壁垒来提供更好的异常处理机制来维持这一防守的底线
看一下图2:


异常可能发生在EJB层的服务端和网页层,甚至独立的客户端。在第一种情况下,异常停留在同一VM中,也可能被传送到网页层。这儿就是我们要安装的顶层异常处理器的地方。

在后一种情况下,异常发生在EJB容器的边缘并且通过RMI连接传递到客户端。必须注意不要传送任何属于服务端类的异常(如来自对象关系映射框架这类的)到客户端。而由EJB异常处理器通过使用SerializableException作为中介来处理这个问题。在客户端,顶层的Swing异常处理器捕获其他未处理的错误并采取相应措施。


异常处理框架

在Rampart框架中异常处理器是一个实现了ExceptionHandler接口的类。这个接口仅有一个包含两个参数(待处理的Throwable和当前的Thread)的方法。方便起见,框架提供了包含基本的实现类ExceptionHandlerBase,他辨别Throwable并将其代理给RuntimeException, Error, Throwable和Rampart框架的Unrecoverable的特定的抽象方法来处理。子类提供这些方法的实现并区别处理。

下面的类图显示了异常处理器的层次和三个缺省的异常处理器。


许多人认为SUN应该在每应用的基础上给J2EE框架内置插入所有容器的钩子。这样就允许自定义错误处理方案、安全及更多可安装的功能,而不需要依赖特定厂商的方案和框架。不幸地是,SUN并没有在EJB规范中提供这样的机制。既然如此,我们只有拿出AOP这个强有力的工具来增加异常处理。我们选择的AspectWerkz框架,可以如下使用方面:

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类来配置和获取的。如果RuntimeException 或者Error在业务逻辑处理过程被抛出时,处理器就会被请求处理了。
DefaultEJBExceptionHandler序列化任何并非来自SUN核心包异常的堆栈信息到专门的SerializableException中,这从好的方面来看,可以将在远程客户端不存在的类的异常的堆栈以任意方式传播,另一方面,这会丢失原始的异常。

如果客户端是远程的,EJB容器忠实地捕获RuntimeException或Error并将他包在java.rmi.RemoteException中,否则使用javax.ejb.EJBException。为了在最低程度保持来源的精确性及堆栈信息,框架在BusinessDelegates剥离传送异常并重新抛出原始异常。
Rampart框架的BusinessDelegate类提供一个EJB无关的接口给客户端,而在内部包含本地或远程的EJB接口。BusinessDelegate类从EJB实现类中用XDoclet生成的,他遵循图4中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本地接口。任何被容器抛出的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;
}
}


actual异常被摘出并重新被抛出给顶层的客户端异常处理器。当异常到达处理器时,堆栈信息会是来自服务端且包含实际错误的原始异常。没有多余的客户端信息被附加。


Swing异常处理器

JVM为每一个控制线程提供了缺省的顶层异常处理器。在异常发生时,处理器输出Error或RuntimeException的堆栈信息到System.err并且结束线程。这种处理行为与用户的要求相差很远而且从调试的观点来看也不是很优雅。我们需要一种机制在保存堆栈信息和为以后调试准备的唯一请求ID的同时允许通知用户。“创建基于J2EE的应用范围用户会话”描述了如何在所有层都可以形成这样的请求ID。

J2SE1.4以前的版本,在Thread实例中未捕获的异常将导致其所在的ThreadGroup的uncaughtException()方法被执行。在应用中控制异常处理的简单方法是继承ThreadGroup类,重写uncaughtException()方法,并且确认所有Thread在自定义的ThreadGroup类的实例中启动。
J2SE5提供了一种更方便的机制允许在Thread类的实例中安装UncaughtExceptionHandler实现。处理器在未捕获的异常到达Thread实例的运行方法中通过回调机制起作用。我们的框架基于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()方法并传递Thread实例及抛出Throwable给配置的异常处理器。

在我们在客户端层控制所有未处理的异常之前还需要做些技巧性的工作。为了使用这个方案有效,所有线程必须与我们的SwingThreadGroup实例关联。这可以通过生成一个主Thread实例并且通过Runnable实现传递SwingThreadGroup实例,这样就可以执行整个程序了。所有来自这个新的主Thread实例的Thread实例自动加入SwingThreadGroup实例,因此当非强制异常被抛出时会触发新的异常处理器。


如图5中框架在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。一个更复杂的错误处理器可以发送email、SNMP信息或者包含请求ID的技术支持。关键的是客户端及服务端日志可以用请求ID来过滤从而使基于每个请求的定位更精确。



图6显示合并的Swing客户端及J2EE服务端日志为请求ID为1cffeb4:feb53del38:-7ff6提供精确的定位。注意堆栈信息仅包含来自服务端的信息,而异常就是来自那儿。

虽然为独立的J2SE应用增加异常处理的底层框架是基础的,但当我们移植到基于网页客户端时还是需要作些改变。


WAR异常处理器

网页应用在J2EE开发中是比较幸运的,拥有自己安装异常处理的能力。通过web.xml配置描述文档,异常和HTTP错误可以在servlets或JSP中映射到错误页面。看一下下面的来自web.xml文档中的示例片断:


ErrorHandlerServlet
dk.rhos.fw.rampart.util.errorhandling.ErrorHandlerServlet



ErrorHandlerServlet
/errorhandler



java.lang.Throwable
/errorhandler


这些标记指示所有未处理的异常会转到/errorhandler这个URL去,在这里就是映射到ErrorHandlerServlet类。这是一个专门的servlet用来作为网页组件与异常处理框架之间的桥梁。当来自网页应用的未处理异常到达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类查找异常信息所对应的国际化信息并重定向输出到/error.jsp。然后这个页面显示信息给用户,包括当前的请求ID。更复杂的机制可以通过简单地扩展或代替这个处理器来实现。

小结
通常异常处理并没有被迅速地处理掉,因而复杂的调试和错误信息在许多时候破坏了整个用户感受。因此在系统开发启动之前准备好异常处理的策略和框架是重要的。在开发完成后再进行修补也是可行的,但代价要昂贵的多。

这篇文章给了你定义异常策略的起点,并且介绍给你一个简单但可扩展的非强制异常层次。我们已经通览了一个示例J2EE架构的业务和客户端层,并且告诉你如何安装顶层的异常处理器来提供一个防卫的底线。框架代码也告诉你一种通过附加给异常和日志入口的唯一请求ID的方式来确定基于每用户请求的确切错误。

你可以下载这个框架,尝试一下,并根据你的需求来更改并在你控制下获取异常。

关于作者
Jens Byager是斯堪的纳维亚北欧地区最大的IT服务公司TietoEnator保健部门的首席顾问。他也有在银行、电信、后勤方面作为软件开发人员及架构师的背景。他拥有Niels Brock Copenhagen商业大学计算机科学的博士士学位和丹麦Aalborg大学计算机科学硕士学位。

Kåre Kjelstrøm是一家丹麦顾问公司Silverbullet的创始人,主要帮助设计和构建企业级应用。他曾经在Trifork从事有关EAS J2EE应用服务器开发和作为J2EE顾问为当地的商业和保健业工作。这些年,他居住在硅谷,从事电子商务过程自动化、Web services管理及J2EE应用服务器相关工作。他拥有丹麦Aarhus大学计算机科学学士和硕士学位。他的Weblog是Drops of a Vapor Trail。




资源
·下载本文示例代码:
http://www.javaworld.com/javaworld/jw-07-2005/exception/jw-0711-exception.zip
核心J2EE模式:业务代理(SUN开发网络):
http://java.sun.com/blueprints/corej2eepatterns/Patterns/BusinessDelegate.html
· XDoclet:
http://xdoclet.sourceforge.net/xdoclet/index.html
·AspectWerkz:
http://aspectwerkz.codehaus.org/
·Maven:
http://maven.apache.org/
·Servlet规范:
http://java.sun.com/products/servlet/download.html
·Rampart框架:
http://213.237.179.87/projects/rampart/index.html
·"创建J2EE应用范围用户会话" Kåre Kjelstrøm (JavaWorld, March 2005):
http://www.javaworld.com/javaworld/jw-03-2005/jw-0314-usersession.html
·更多在JavaWorld中的异常处理文章:
o"Patch an Exception-Handling Framework," Niranjan R. Kamath (March 2005)为异常处理框架打补丁:
http://www.javaworld.com/javaworld/jw-03-2005/jw-0321-exception.html
o在“异常设计”(JavaWorld, July 1998)中,Bill Venners解释了如何使用强制和非强制异常来支持契约设计:
http://www.javaworld.com/javaworld/jw-07-1998/jw-07-techniques.html
o“异常实践”,Brian Goetz
§第一部分:在程序中有效使用异常(August 2001):
http://www.javaworld.com/javaworld/jw-08-2001/jw-0803-exceptions.html?
§第二部分:用异常链来保存调试信息(September 2001):
http://www.javaworld.com/javaworld/jw-09-2001/jw-0914-exceptions.html?
§第三部分:: 用信息目录简化本地化(December 2001):
http://www.javaworld.com/javaworld/jw-12-2001/jw-1221-exceptions.html?
o“异常:不要为丢弃而处理异常”Tony Sintes (February 2002):
http://www.javaworld.com/javaworld/javaqa/2002-02/01-qa-0208-exceptional.html
o“java提示124:不要在捕获异常时把网洒得太大” Dave Schweisguth (February 2003):
http://www.javaworld.com/javaworld/javatips/jw-javatip134.html?
o通用异常的危险:“小心通用异常的危险” Paul Philion (October 2003): http://www.javaworld.com/javaworld/jw-10-2003/jw-1003-generics.html?
·在JavaWorld的j2EE部分有更多有关J2EE的文章
http://www.javaworld.com/channel_content/jw-j2ee-index.shtml?
·在JavaWorld的EJB部分有更多有关J2EE的文章
http://www.javaworld.com/channel_content/jw-ejbs-index.shtml?
·在JavaWorld的Testing部分有更多参考
http://www.javaworld.com/channel_content/jw-testing-index.shtml

↑返回目录
前一篇: 探索Laszlo的类、属性及事件
后一篇: 使用Eclipse RCP的IBM Workplace Managed Client