主题
企业架构
异常通知:面向方面的模型
页面: 1, 2
ExceptionHandling 方面确保FaultException总是会到达错误屏障,无论中间参与的代码行为如何。这保证了错误屏障总是以一种有序的方式终止处理序列。它使中间参与的代码免于担忧意外捕获FaultException。此通知使这些异常更加棘手,错误屏障以外的任何处理程序都无法捕获它们。allHandlers()切入点应用到应用程序中的所有异常处理程序,并使包含处理程序和所处理异常的类对before() 通知逻辑可用。通知会在异常处理程序内代码执行前执行。除非异常是一个FaultException,否则通知不会采取任何措施。对于FaultException,通知会检查处理程序是否位于指定错误屏障的类中。如果是,则允许该处理程序捕获FaultException。如果不是,则再次抛出FaultException,忽略将捕获它的处理程序。最终,FaultException将到达指定错误屏障类中的一个处理程序处。
public abstract aspect ExceptionHandling {
...
pointcut allHandlers(Object handlerType, Throwable throwable):
handler(Throwable+) && args(throwable) &&
this(handlerType);
before(Object handler, Throwable throwable):
allHandlers(handler, throwable) {
if (throwable instanceof FaultException) {
if (!(isFaultBarrier(handler))) {
FaultException fault = (FaultException) throwable;
throw (fault);
}
}
}
abstract boolean isFaultBarrier(Object exceptionHandler);
...
}
清单 4. 仅错误屏障捕获错误方面要如何知道一个处理程序是否位于指定错误屏障内呢?一种方法就是将指定错误屏障的类名称硬编码到ExceptionHandling方面当中。但那会将方面与特定应用程序绑定在一起。为了使ExceptionHandling 方面尽可能地灵活,它声明了一个抽象方法,回答错误屏障的问题。isFaultBarrier()的实现是在了解应用程序细节且能判断一个处理程序对象是否为错误屏障的子方面中提供的。这也就是说,ExceptionHandling必须声明为抽象方面。它必须被具体子方面扩展,之后其通知才能被激活。子方面仅需要应用isFaultBarrier()的一个实现,加上另外一个方法,下面将讨论这个方法。
上面介绍的ExceptionHandling方面确保了应用程序抛出且未被捕获的所有异常都会作为FaultException到达错误屏障。这适用于来自Java库方法的意外异常、应用程序代码的bug导致的意外异常,以及在错误条件未被发现时显式抛出的FaultExceptions。错误屏障仅需捕获FaultException,而非传统实现需要捕获的Throwable。可以通过任何看似自然的方式构造应用程序代码,无需考虑对应用程序的错误处理能力造成的影响。
这是面向方面方法的一大优势。而ExceptionHandling方面实际证明了在错误发生时它能够提供的诊断信息的质量极具价值。一个方面能够在应用程序运行时观测整个应用程序。ExceptionHandling 方面利用这种能力跟踪传递给应用程序中各方法和构造方法的参数。出现错误时,该方面为所记录的标准异常和堆栈跟踪信息附加一个特殊的应用程序跟踪(Application Trace)部分。应用程序跟踪中的每个条目都描述了处理的类型;类、方法或构造方法的名称;用于调用它的参数名称、类型和值。结果如下所示:
FATAL : exception.ServiceExceptionHandling - Application Fault Detected exception.FaultException: Unexpected failure on catalog query: com.ibm.db2.jcc.b.SQLException: The string constant beginning with "'" does not have an ending string delimiter. at domain.CatalogDAO.performQuery(CatalogDAO.java:86) at domain.CatalogDAO.getCatalogEntries(CatalogDAO.java:57) at domain.CatalogService.getCatalogEntry(CatalogService.java:16) at domain.CartItem.<init>(CartItem.java:22) at domain.ShoppingCart.addToCart(ShoppingCart.java:28) at action.SelectItemAction.performAction(SelectItemAction.java:44) at action.BaseAction.execute(BaseAction.java:57) at org.apache.struts.action.RequestProcessor.processActionPerform(RequestProcessor.java:484) at org.apache.struts.action.RequestProcessor.process(RequestProcessor.java:274) at org.apache.struts.action.ActionServlet.process(ActionServlet.java:1482) at org.apache.struts.action.ActionServlet.doPost(ActionServlet.java:525) at javax.servlet.http.HttpServlet.service(HttpServlet.java:763) at javax.servlet.http.HttpServlet.service(HttpServlet.java:856) at weblogic.servlet.internal.StubSecurityHelper$ServletServiceAction.run(StubSecurityHelper.java:225) at weblogic.servlet.internal.StubSecurityHelper.invokeServlet(StubSecurityHelper.java:127) at weblogic.servlet.internal.ServletStubImpl.execute(ServletStubImpl.java:283) at weblogic.servlet.internal.ServletStubImpl.execute(ServletStubImpl.java:175) at weblogic.servlet.internal.WebAppServletContext$ServletInvocationAction.run(WebAppServletContext.java:3214) at weblogic.security.acl.internal.AuthenticatedSubject.doAs(AuthenticatedSubject.java:321) at weblogic.security.service.SecurityManager.runAs(SecurityManager.java:121) at weblogic.servlet.internal.WebAppServletContext.securedExecute(WebAppServletContext.java:1983) at weblogic.servlet.internal.WebAppServletContext.execute(WebAppServletContext.java:1890) at weblogic.servlet.internal.ServletRequestImpl.run(ServletRequestImpl.java:1344) at weblogic.work.ExecuteThread.execute(ExecuteThread.java:209) at weblogic.work.ExecuteThread.run(ExecuteThread.java:181) Application Trace: method-execution: domain.CatalogDAO.performQuery(query:java.lang.String=SELECT * FROM ADMINISTRATOR.CATALOG WHERE CATALOGID = 'BAD'INPUT', transaction:integration.Transaction=jdbc:db2://localhost:50000/DOCMGMT) method-execution: domain.CatalogDAO.getCatalogEntries(catalogID:java.lang.String=BAD'INPUT, transaction:integration.Transaction=jdbc:db2://localhost:50000/DOCMGMT) method-execution: domain.CatalogService.getCatalogEntry(catalogID:java.lang.String=BAD'INPUT) constructor-execution: domain.CartItem.<init>(catalogID:java.lang.String=BAD'INPUT, quantity:int=1) initialization: domain.CartItem.<init>(catalogID:java.lang.String=BAD'INPUT, quantity:int=1) method-execution: domain.ShoppingCart.addToCart(catalogID:java.lang.String=BAD'INPUT, quantity:int=1) method-execution: action.SelectItemAction.performAction(cart:domain.ShoppingCart=Cart0024988, action:org.apache.struts.action.ActionMapping=ActionConfig[path=/SelectItem,name=SelectItemForm,scope=session,type=action.SelectItemAction, form:org.apache.struts.action.ActionForm=CatalogID-BAD'INPUT Quantity-1, request:javax.servlet.http.HttpServletRequest=Http Request: /ShoppingServices/SelectItem.do, response:javax.servlet.http.HttpServletResponse=weblogic.servlet.internal.ServletResponseImpl@22a6c2) method-execution: action.BaseAction.execute(action:org.apache.struts.action.ActionMapping=ActionConfig[path=/SelectItem,name=SelectItemForm,scope=session,type=action.SelectItemAction, form:org.apache.struts.action.ActionForm=CatalogID-BAD'INPUT Quantity-1, request:javax.servlet.http.HttpServletRequest=Http Request: /ShoppingServices/SelectItem.do, response:javax.servlet.http.HttpServletResponse=weblogic.servlet.internal.ServletResponseImpl@22a6c2)清单 5. 应用程序跟踪的结果
应用程序跟踪仅包含受ExceptionHandling方面影响的那些方法:作为应用程序特定部分的方法。请注意,应用程序跟踪中的条目大致对应于堆栈跟踪顶端的项目。(堆栈跟踪的下端涵盖作为WebLogic Server实现的具体部分的类。)这里给出的示例来自一个允许用户应用形参(BAD'INPUT)的Struts应用程序,该参数包含单个引号字符,从而在SQL预计中导致语法错误。在诊断记录中显示参数值有助于确定错误在何处发生。这是ExceptionHandling方面中与错误记录相关的一段出色代码。首先,观察一下方面是如何控制错误记录方式的。
public abstract aspect ExceptionHandling {
...
private boolean FaultException.logged = false;
private boolean FaultException.isLogged() {
return this.logged;
}
private void FaultException.setLogged() {
this.logged = true;
}
after() throwing(FaultException fault): exceptionAdvicePoints(){
if (!fault.isLogged()) {
logFault(fault);
fault.setLogged();
}
}
...
}
清单 6. 错误记录通知 只要应用程序的任何一点抛出FaultException,抛出后通知就会运行。它的任务是调用方面的logFault()方法,此方法完成实际记录工作。单独一个错误在调用堆栈的所有方法上传播时可能会多次触发通知,因此通知需要找到一种方法,来了解记录在何时完成。为此使用了另外一种AOP技术:成员引入(member introduction)。方面将一个布尔标记引入FaultException 类型,附带一些用于访问的方法。这个标记和这些方法被合理地标为私有,仅在ExceptionHandling方面内可见。总体影响是:诊断记录在错误出现时立即发生,而且不再重复。
错误可能在任何时候出现。要为可能出现的错误做好准备,ExceptionHandling 方面需要在应用程序运行的时候跟踪其活动。这样,如果出现错误,它就可以随时记录导致错误的调用序列及其参数值。为此,该方面维护了一个JoinPoint对象引用的每线程堆栈。执行应用程序时,方面的跟踪堆栈随调用堆栈一起伸缩。AspectJ运行时利用语言构造thisJoinPoint使JoinPoint对通知逻辑可用。JoinPoint 对象包含通知逻辑的动态上下文信息,使逻辑能够了解关于触发通知的环境的细节。
public abstract aspect ExceptionHandling {
...
private static ThreadLocal<Stack<JoinPoint>> traceStack =
new ThreadLocal<Stack<JoinPoint>>() {
protected Stack<JoinPoint> initialValue() {
return new Stack<JoinPoint>();
}
};
private static void pushJoinPoint(JoinPoint joinPoint) {
traceStack.get().push(joinPoint);
}
private static JoinPoint popJoinPoint() {
Stack<JoinPoint> stack = traceStack.get();
if (stack.empty()) {
return null;
} else {
JoinPoint joinPoint = stack.pop();
return joinPoint;
}
}
private static JoinPoint[] getJoinPointTrace() {
Stack<JoinPoint> stack = traceStack.get();
return stack.toArray(new JoinPoint[stack.size()]);
}
...
}
清单 7. ThreadLocal 调用跟踪方法有了这些方法之后,跟踪应用程序调用的通知就非常简单了。exceptionAdvicePoints()切入点(可能因异常突然终止的任何执行序列)标识的连接点被推入堆栈。在序列开始之前,JoinPoint对象被推入线程的跟踪堆栈。序列完成后,其JoinPoint对象从堆栈弹出。跟踪堆栈中的JoinPoint 对象永远不会被解除引用,除非出现错误。
public abstract aspect ExceptionHandling {
...
before(): exceptionAdvicePoints(){
pushJoinPoint(thisJoinPoint);
}
after(): exceptionAdvicePoints(){
popJoinPoint();
}
...
}
清单 8. 调用跟踪通知 发生错误时,将运行清单6中的通知,同时调用下面的方法来呈现诊断。来自堆栈跟踪的信息包含在FaultException 中,这些信息来自方面自身的每线程连接点堆栈。formatJoinPoint()方法从各JoinPointobject对象中提取我们需要的信息:限定的方法或构造方法名称、其形参的名称和类型、作为自变量传递给那些参数的值。
public abstract aspect ExceptionHandling {
...
private void logFault(FaultException fault) {
ByteArrayOutputStream traceInfo =
new ByteArrayOutputStream();
PrintStream traceStream = new PrintStream(traceInfo);
fault.printStackTrace(traceStream);
StringBuffer applicationTrace = new StringBuffer();
JoinPoint[] joinPoints = getJoinPointTrace();
for (int i = joinPoints.length - 1; i >= 0; i--) {
applicationTrace.append("\n\t"
+ formatJoinPoint(joinPoints[i]));
}
recordFaultDiagnostics("Application Fault Detected"
+ "\n" + traceInfo.toString()
+ "\nApplication Trace:"
+ applicationTrace.toString());
}
abstract void recordFaultDiagnostics(String diagnostics);
private String formatJoinPoint(JoinPoint joinPoint) {
CodeSignature signature = (CodeSignature)
joinPoint.getSignature();
String[] names = signature.getParameterNames();
Class[] types = signature.getParameterTypes();
Object[] args = joinPoint.getArgs();
StringBuffer argumentList = new StringBuffer();
for (int i = 0; i < args.length; i++) {
if (argumentList.length() != 0) {
argumentList.append(", ");
}
argumentList.append(names[i]);
argumentList.append(":");
argumentList.append(types[i].getName());
argumentList.append("=");
argumentList.append(args[i]);
}
StringBuffer format = new StringBuffer();
format.append(joinPoint.getKind());
format.append(": ");
format.append(signature.getDeclaringTypeName());
format.append(".");
format.append(signature.getName());
format.append("(");
format.append(argumentList);
format.append(")");
return format.toString();
}
}
清单 9.错误记录方法ExceptionHandling 方面定义了抽象方法recordFaultDiagnostics(),允许应用程序指定希望如何记录方面所产生的诊断信息。应用程序在具体子方面内提供该方法的一个实现。这种安排使记录细节脱离基本方面,从而保证了方面的最大灵活性。
一个方面观测应用程序其他部分的能力使之能够在错误发生时提供全面的诊断。它能够在不了解其他应用程序组件或不与其协作的前提下完成这一任务。将实现诊断记录关注点的代码聚集在一处是面向方面方法的一大优势。
错误-意外事件异常模型对于许多Java应用程序都很有帮助。使用AOP技术实现该模型的关注点具有一些令人着迷的优势。在编译时检测模型偏差的能力只是其中之一。将与错误处理相关的逻辑隔离起来是另外一种优势。以错误和意外事件为依据进行思考能够消除应用程序中众多令人迷惑的代码。而以方面为已经进行思考则会使应用程序的代码更简单,减少代码中充满无心之错的机会。那么,在考虑采用AOP时,这是否足够令人鼓舞?只有您能决定。
Barry Ruzek 被Open Group组织授予Master Certified IT Architect的称号。他有30多年开发操作系统和企业应用程序的经验。