异常的通知:面向方面的模型

http://www.javaeedev.com/blog/article.jspx?articleId=ff80808115cbcd9c0115f88f7af70d5c

摘要

  有效的异常处理策略是一大架构关注点,它超越了独立应用程序组件的边界。有效的Java异常(Dev2Dev中文版,2007年2月)概述了错误-意外事件(Fault-Contingency)异常模型,消除了在Java应用程序中使用已检查还是未检查异常的迷惑。使用传统Java技术实现这种模型要求所有组件都遵循一组规则和行为。这也就暗中表明原本无关的组件间的耦合需要为意料之外的失误和故障留出空间。在错误-意外事件异常模型中应用面向方面的技术需要首先处理这一关注点,并允许其他组件专心于其主要工作。

  本文解释了为什么基于错误-意外事件异常模型的异常处理方面是对传统实现的重大改进。还提供了使用AspectJ(Java的一种面向方面的扩展)创建的异常处理方面的完整示例,来展示这种概念。文中提供的代码可在BEA WebLogic 9.2 和 Tomcat 5.0 应用服务器上运行。

AOP 与架构

  作为一名应用程序架构师,您的主要职责是制订决策来监管组件之间的关系。架构决策会影响到组件的设计方法、组件用来协作的模式以及所遵循的惯例。如果决策合理、沟通充分,并得到了项目团队的遵从,那么就会得到一个易于理解、维护和扩展的软件系统。每个人都希望得到这样的结果,但实现起来却极具挑战。架构是跨组件的,它要求组件执行某些操作,或者避免特定的行为,以使一切在一个总体愿景下协调工作。

  开发团队是由人构成的,而人都是不完美的。即便最出色的开发团队也会在维护架构愿景的纯净方面遇到麻烦。团队可以利用两种传统的对策来避免架构违规。第一种对策是规定对设计和编码进行定期审查。第二种是构建框架。审查的目标是在问题刚刚出现时发现问题。而框架提供了一种可重用的基础架构,其约束的目标是从一开始就预防问题,避免问题出现。

  面向方面的设计是应对架构关注点的第三种选择。它不是将架构行为分散到所有无关的组件,而是将行为封装在一个方面中,并在特定执行点应用。面向方面编程(AOP)方面的工作自20世纪90年代就已启动,但可以公正地说,AOP的广泛采用依然有待时日。或许造成这种情况的原因之一就是缺乏令人鼓舞的典范,表明这种技术能够带来怎样的收益。一个引人注目的AOP实例应具有如下特点:

  • 有价值――解决公认的问题
  • 没有AOP就难以解决
  • 使用AOP可轻松解决

  跟踪方法执行的常用示例是展示方面功能的好办法,但并不那么令人鼓舞――或许应该说,鼓舞的程度对于大多数人来说还不够,不足以投入人力物力去学习这种技术,不足以成为在下一个项目中使用AOP的理由。其实存在更好的例子,但您需要详查所有记录了方法的例子,来找到所需的那些。

  在许多软件项目中,Java应用程序中的异常处理就是一个公认的关注点。管理不善的异常规程将导致难以理解和维护的易出错代码。一致的异常处理方法对于多数应用程序来说都是一项重大收益。即便在团队采用了异常的架构模型时,确保每个模型都符合模型也需要不懈的努力和深入的洞察力。看上去异常处理模型似乎是探索AOP的不错方法。这是否能成为一个鼓舞人心的例子,一切由您决定。

错误-意外事件异常模型

  异常处理方面从您希望应用到整个应用程序中的一个模型或者一组行为开始。错误-意外事件异常模型提供了一种切实可行的方式来考虑执行软件时所遇到异常。该模型将不规则的输出描述为意外事件(Contingency)或错误(Fault)。意外事件是一种可选输出,能够使用组件目标用途的词汇表加以描述。方法或构造方法的调用方具有处理其意外事件输出的战略。另一方面,错误是无法在语义契约方面进行描述的一种故障,仅可就实现细节进行描述。举例来说,考虑一个Account Service,它带有一个getAccount()方法,在为此方法提供一个Account ID后,它将返回Account对象。很容易就能设想出可能出现的意外事件,例如“No such account”或“Invalid Account ID”,是就方法的目标用途表述的。要预计可能出现的错误,您首先需要了解getAccount() 方法是怎样实现的。它是否因未连接到数据库而接收到了一个SQLException?或许存在超时,正在等待一个宕机维护的Web服务。或许一个丢失的文件(本应存在)导致了FileNotFoundException。此处的要点在于getAccount() 的调用方不应对实现有任何了解,也不应被迫为预计到的任何错误捕捉已检查异常。

  错误-意外事件异常模型的一个简单Java实现具有三个基本概念:意外事件异常、错误异常、错误屏障。方法和构造方法使用意外事件异常来通知作为其契约一部分的可选输出。意外事件异常是已检查的异常,因此编译器将帮助确保调用方考虑到所有约定的输出。错误异常用于通知特定于实现的故障。错误异常是未检查的异常,运行中的代码通常会避免捕捉这些异常,将这一责任留给作为错误屏障的类处理。错误屏障的主要责任就是在获得错误异常时为正在处理的活动提供一个出色的出口。这种出色的出口通常包含对正在处理的故障的表示,例如在用户界面(如果有用户界面)上显示一条道歉信息,或者通过其他方式向“外部世界”指出故障。

  传统的实现使用RuntimeException的一个子类(比如FaultExceptio)来表示错误异常。作为一个未检查的异常,FaultException可在未在方法和构造方法的签名中被显式捕获或声明的情况下抛出。因此,这种异常完全可能在未被错误屏障捕获或处理时处于未发现的状态。意外事件异常基于Exception的一个子类(比如ContingencyException),该子类使Java编译器可以检查此类异常。由于ContingencyException是语义契约的完整组成部分,因此可以借助编译器来保证调用方具备处理此类异常的战略。

  模型中的组件需要遵循一组使一切正常工作的规范。首先,组件不能抛出FaultException 或 ContingencyException子类以外的异常。其次,组件必须避免捕获FaultException,将这一责任留给错误屏障。组件负责处理它们所调用的外部方法抛出的异常,并在必要时将其转换为FaultException或ContingencyException。任何未捕获的RuntimeException都被视为错误,需要错误屏障的关注。这些规则非常简单,有效地消除了应用程序代码中混乱、令人迷惑的异常序列。通过清晰地将错误划分出来,由错误屏障负责处理,使用低级代码处理错误条件的诱惑得到了极大的消减。错误不再干预应由意外事件异常处理的情况,意外事件异常的目的显然是在组件间传输有意义的信息。

传统实现的不足之处

  错误-意外事件异常模型的传统实现是对临时异常处理的极大改进,但离理想还相去甚远。所有的组件都必须遵循规范,即便这些组件之间再无其他关联。确保它们确实遵循了规范的惟一方法就是审查代码。可能有一个组件无意中捕获了错误异常,使之无法传递给错误屏障。如果发生这种情况,您可以顺利从那个出色的出口退出并离开,但没有任何办法去诊断所发生的错误。

  传统实现给错误屏障设定了两方面的责任。其固有的责任就是完美地终止处理序列。由于其位置靠近调用堆栈的顶端,因此错误屏障了解周围环境,了解哪些内容能够构成恰当的输出响应。另外一种责任是记录与错误相关的分析信息,以使人们了解发生了什么。它具有这种责任的惟一原因就是没有其他合适的位置来完成这个任务。如果系统需要多个错误屏障(有些系统确实需要),那么每个错误屏障都必须包含类似的逻辑,来捕获可用信息。

  修正一个问题的能力取决于可用信息的质量。实际上,传统实现能够提供的信息仅限于RuntimeException能够提供的那些:堆栈跟踪和错误消息。每一名Java程序员都会乐于在没有任何与实际发生情况有关的线索的前提下,启动一次堆栈跟踪。堆栈跟踪将显示发生了什么、在哪里发生,但不会显示为什么发生这样的情况。理想情况下,您希望了解哪些方法被调用,以及它们是怎样被调用的――传入各方法且导致错误的参数类型和值。将代码分散到每一个方法之中并在输入时记录其参数这种方法令人不满、不切实际、易于出错,如果未实际出现任何错误,那么所做的一切都是白费功夫。

方面、切入点和通知

  方面编程正是为解决此类问题而出现的。在我们的例子中,应用程序内的所有组件都必须关注错误和意外事件的规则。如果一个类中出现失误,会波及众多不相关的类,导致较大的异常模型出现故障。同样,我们可以使用错误屏障来完成记录分析信息的任务,尽管其自身的角色只是了解如何为外部世界生成一般响应并执行清除操作。

  AOP的理念是将所需行为封装在一个实体中:方面。一个方面包含在应用程序中某些定义好的点上运行的逻辑。所运行的这种逻辑就称为通知。应用通知的点称为连接点。可通过定义切入点来指定一组应用通知的连接点。切入点基本上就是一个表达式,过滤应用程序中所有潜在连接点,并根据标准(如接入点的类型和各种类型的模式匹配)来选择部分连接点。如果恰当地制作了方面,它会执行一些操作,若不使用方面,这些操作将分散在应用程序之中。将一切都集中到一处之后,应用程序中的其他组件即可集中关注其主要任务。最终得到更出色的组件内聚,这是人人都希望得到的结果。

  示例应用程序包含我们的异常处理方面,它是使用Java语言的一个超集AspectJ构建的。这种语言支持对于任何应用程序中的异常处理都非常重要的连接点多样性。在Java应用程序的执行过程中,可以通过多种方式生成并捕获异常。方法执行只是其中的一种方式。异常处理方面需要考虑构造方法的执行、对象初始化和类初始化,这些都可能导致异常。此外还要考虑显式抛出和捕获异常的位置。AspectJ的切入点语言支持实现理想模型所需的一切。

ExceptionHandling方面

  ExceptionHandling方面在设计时就考虑到了最大化灵活性,因此它只有两个编译时依赖项。它们是表示错误和意外事件的两个Java类:

  • FaultException:表示错误条件的RuntimeException类的一个子类。
  • ContingencyException:表示意外事件输出的Exception类的一个子类。子类表示具体的意外事件条件。
  • 方面假设(并强制)应用程序的其他部分按照模型规则使用这些类。AspectJ系统的绝妙特性之一就是能够“为编译器编程”,从而实施超越标准Java语言规则的策略。在我们的例子中,我们希望鼓励开发人员以错误和意外事件为依据进行思考,并清楚地加以区分。由于我们的架构提供了一个ContingencyException基类,我们希望确保开发人员仅使用该类的子类来表示意外事件条件。通过这种做法,就能够避免开发人员尝试声明一个抛出(比如说)SQLException的方法,此方法应将任何意料之外的JDBC问题都视为错误。
  • ExceptionHandling方面使用切入点来检测声明了已检查异常而非基于ContingencyException的异常的方法和构造方法。违规将作为编译错误标出,这种方式能确保相关人员注意到问题。
package exception;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.util.Stack;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.CodeSignature;
public abstract aspect ExceptionHandling {
         ...
    pointcut methodViolation():
        execution(* *(..) throws (Exception+
            && !ContingencyException+
            && !RuntimeException+));
    declare error:
        methodViolation():
            "Method throws a checked exception that is not 
                                      a ContingencyException";
    pointcut constructorViolation():
        execution(*.new(..) throws (Exception+
            && !ContingencyException+
            && !RuntimeException+));
    declare error:
        constructorViolation():
            "Constructor throws a checked exception that is not
                                         a ContingencyException";
         ...
}

  清单 1. 编译时异常策略实施

  在下面的例子中,Transaction类的commit()方法声明其抛出SQLException,而在这种环境中SQLException应视为错误,因而它违背了异常策略。编译器以ExceptionHandling方面中的声明为依据,将这一违规作为编译错误标出。rollback()方法将意料之外的SQLException视为错误处理,符合此模型,因而不会出现任何标记。为本文开发的示例是使用安装了AspectJ Development Tools (AJDT) 1.4.1插件的Eclipse 3.2.1开发的。

  Exception Policy Violation Flagged as Compile Error

  图 1.异常策略违规将生成一个错误标记

异常通知连接点

  ExceptionHandling方面使用exceptionAdvicePoints()切入点来为任何能够抛出异常的执行序列应用通知。这个方面多次使用此切入点,在可能抛出异常时注入处理。切入点包含如下连接点:

  • 所有方法执行
  • 所有构造方法执行
  • 所有对象初始化
  • 所有对象预初始化
  • 所有类初始化

  由于ExceptionHandling方面具有自己的方法、自己的构造方法,也要经历类和对象的初始化过程,上述连接点中有一些就处于这个方面之中。通常这不是什么好事,会在方面尝试发布自己的通知时引起递归循环。为避免这样的可能性,exceptionAdvicePoints()切入点明确地在上述连接点中排除了部分连接点:

  • 方面自身的词法作用域(lexical scope)内的任何连接点
  • 其子方面的词法作用域内的任何连接点
  • 通知执行控制流内的任何连接点
public abstract aspect ExceptionHandling {
         ...
    pointcut exceptionAdvicePoints():
           (execution (* *.*(..))
         || execution (*.new(..))
         || initialization(*.new(..))
         || preinitialization(*.new(..))
         || staticinitialization(*))
         && !within(ExceptionHandling+)
         && !cflow(adviceexecution());
         ...
}

  清单 2. 异常通知点

  现在,ExceptionHandling方面能够为自身以外的所有应用程序组件应用与异常相关的通知。不会尝试为那些可能是作为执行其自身通知的结果运行的方法应用通知。

运行时异常转换

  错误-意外事件模型的惯例表明,未被捕获的Throwable应视为错误条件。在传统实现中,完全不能保证此类异常在较低级别被捕获并转换,因此错误屏障必须随时准备捕捉Throwable,而不仅仅是FaultException。在AOP实现中,我们可以更好地完成这个任务。可以确保从任一组件抛出且未被捕获的Throwable都会自动转换成FaultException。这使错误屏障的实现更为简单,也确保了ExceptionHandling方法内的任何其他通知都会将未被捕获的Throwable视为错误处理。

  如果一个执行序列抛出了任何类型的Throwable,“抛出后”通知将运行。如果异常是FaultException或ContingencyException,通知不会采取任何措施。否则,通知会将违规的异常替换为FaultException的一个新实例,将未被捕获的异常作为诱因。请注意,方面的编译时异常策略实施简化了通知必须进行的检查。

public abstract aspect ExceptionHandling {
  ...
  after() throwing(Throwable throwable):exceptionAdvicePoints(){
    if (!(throwable instanceof FaultException || 
          throwable instanceof ContingencyException)) {
        throw new FaultException("Unhandled exception: ",
            throwable);
    }
  }
  ...
}

  清单 3.运行时异常转换

到达错误屏障

  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时,这是否足够令人鼓舞?只有您能决定。

 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值