许多人认为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文档中的示例片断:
<servlet
<servlet-nameErrorHandlerServlet</servlet-name
<servlet-classdk.rhos.fw.rampart.util.errorhandling.ErrorHandlerServlet</servlet-class</servlet<servlet-mapping <servlet-nameErrorHandlerServlet</servlet-name
<url-pattern/errorhandler</url-pattern</servlet-mapping<error-page
<exception-typejava.lang.Throwable</exception-type
<location/errorhandler</location</error-page
这些标记指示所有未处理的异常会转到/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.