Exception Advice: An Aspect-Oriented Model

by Barry Ruzek
07/04/2007

Abstract

An effective exception handling strategy is an architectural concern that transcends the boundaries of individual application components. Effective Java Exceptions outlines the Fault-Contingency exception model, which eliminates much of the confusion over using checked and unchecked exceptions in Java applications. Implementing the model using traditional Java techniques requires all components to follow a set of rules and behaviors. This implicit coupling between otherwise unrelated components leaves room for accidental lapses and failures. Applying aspect-oriented techniques to the Fault-Contingency exception model focuses the handling of this concern in one place, allowing other components to concentrate on their primary jobs.

This article shows how an exception-handling aspect based on the Fault-Contingency exception model is an improvement over the traditional implementation. It offers a complete example of an exception-handling aspect created with AspectJ, an aspect-oriented extension to Java, to illustrate the concepts. The code supplied herein runs on both BEA WebLogic 9.2 and Tomcat 5.0 application servers.

AOP and Architecture

As an application architect, you are responsible for making the decisions that govern how components relate to each other. Architectural decisions influence how components are designed, the patterns they use to collaborate, and the conventions they follow. If the decisions are sound, duly communicated, and followed by the project team, the result is a software system that is easy to understand, maintain, and extend. Everyone likes that, but it can be a challenge to achieve it. Architecture spans components, requiring them to perform certain actions or avoid particular behaviors so that everything harmonizes with an overall encompassing vision.

Development teams are composed of human beings, and humans are not perfect. Even the best development teams have some trouble maintaining the purity of their architectural vision. There are two traditional countermeasures teams use to avoid architectural transgressions. The first is to set up regular reviews of design and code. The second is to build frameworks. Reviews are intended to catch problems as they emerge. Frameworks provide a reusable infrastructure whose constraints are intended to prevent problems from emerging in the first place.

Aspect-oriented design is a third alternative for addressing architectural concerns. Instead of scattering architectural behavior throughout unrelated components, the behavior is encapsulated in an aspect and applied at specific execution points. Although work on aspect-oriented programming (AOP) began in the 1990s, it would be fair to say that widespread adoption is still some distance away. One reason may be a shortage of inspiring examples of how the technology can be of benefit. A compelling AOP example would have these traits:

  • Valuable - Solving a well-recognized problem
  • Hard to accomplish without AOP
  • Easy to accomplish with AOP

The commonly used example of tracing method executions is a good way to illustrate what an aspect can do, but is not very inspiring—certainly not inspiring enough for most to invest in learning the technology or to make a case for using AOP in their next project. There are better examples out there, but you need to sift through all of the method-logging examples to find them.

Exception handling in Java applications is a well-recognized concern in many software projects. Poorly managed exception discipline leads to fragile code that's difficult to understand and maintain. A consistent exception handling approach is a real benefit to most applications. Even when a team adopts an architectural model for exceptions, ensuring that every component adheres to the model requires effort and oversight. It seems like an exception handling model is a good candidate for an AOP exploration. Only you can judge whether or not it makes an inspiring example.

The Fault-Contingency Exception Model

An exception handling aspect starts with a model, or set of behaviors, that you want to apply across your application. The Fault-Contingency exception model provides a practical way of thinking about the exceptional conditions encountered by executing software. The model characterizes an anomalous outcome as either a Contingency or a Fault. A Contingency is an alternate outcome that can be described using the vocabulary of a component's intended purpose. Callers of a method or constructor have a strategy for handling its Contingency outcomes. A Fault, on the other hand, is a failure that cannot be described in terms of a semantic contract but only in terms of implementation details. For example, consider an Account Service with a getAccount() method that returns an Account object when supplied with an Account ID. You could easily imagine possible contingencies such as "No such account," or "Invalid Account ID," expressed in terms of the method's intended purpose. To imagine the possible faults, you would first have to know how the getAccount() method is implemented. Did it receive an SQLException because it could not connect to a database? Maybe there was a timeout waiting for a Web service that was down for maintenance. Or a missing file (that really ought to be there) caused a FileNotFoundException. The point here is that the caller of getAccount() should not know anything about the implementation and should not be forced to catch checked exceptions for any of its projected faults.

A simple Java implementation of the Fault-Contingency exception model has three basic concepts: Contingency Exception, Fault Exception, and Fault Barrier. Methods and constructors use Contingency Exceptions to signal alternate outcomes that are part of their contracts. Contingency Exceptions are checked exceptions, so the compiler helps ensure that callers consider all contracted outcomes. Fault Exceptions are used to signal implementation-specific failures. Fault Exceptions are unchecked exceptions, and working code generally avoids catching them, leaving that responsibility to the class acting as Fault Barrier. The Fault Barrier's main responsibility is to craft a graceful exit to the processing activity when a Fault Exception reaches it. The graceful exit usually includes an indication of processing failure such as an apologetic display on the user interface (if there is one), or some other gesture indicating failure to the world "outside."

A traditional implementation uses a subclass of RuntimeException (say, FaultException) to represent Fault Exceptions. Being an unchecked exception, FaultException can be thrown without being explicitly caught or declared in method and constructor signatures. Therefore, it can remain under the radar until it is caught and handled by the Fault Barrier. Contingency Exceptions are based on a subclass of Exception (say, ContingencyException) that makes them subject to checking by the Java compiler. Since a ContingencyException is an integral part of a semantic contract, it makes sense to enlist the compiler’s help to ensure that a caller has a strategy for handling it.

Components that participate in the model need to follow a set of conventions that make everything work. First, components must not throw exceptions other than FaultException or ContingencyException subclasses. Second, components must avoid catching FaultException, leaving that responsibility to the Fault Barrier. Components are responsible for handling exceptions thrown from external methods they invoke, performing translations to FaultException or ContingencyException, if needed. Any uncaught RuntimeException is considered to be a fault, and the Fault Barrier needs to be aware of this. These rules are simple and they go a long way toward eliminating messy, confusing exception sequences in application code. By cleanly separating faults and making them the responsibility of the Fault Barrier, the temptation for low-level code to handle fault conditions is greatly diminished. That leaves the field clear for Contingency Exceptions, which are expressly intended to convey meaningful information between components.

Where the Traditional Implementation Falls Short

The traditional implementation of the Fault-Contingency exception model is a nice improvement over ad hoc exception handling but is still some distance away from ideal. All components must adhere to the conventions, even if they have no other relationship to each other. The only way to ensure that they do is to review the code. A component may inadvertently catch Fault Exceptions, preventing them from reaching the Fault Barrier. If that happens, you can kiss your graceful exit goodbye and be left without a way to diagnose the fault.

The traditional implementation places two obligations on the Fault Barrier. Its natural responsibility is to gracefully terminate the processing sequence. By virtue of its position near the top of the call stack, the Fault Barrier knows about the surrounding context and what constitutes an appropriate outward response. Its other obligation is to record analytic information associated with the fault so that people can figure out what happened. The only reason it has this job is that there is no other good place to do it. If a system requires more than one Fault Barrier (some do), then each must contain similar logic to capture the available information.

The ability to fix a problem depends on the quality of the information available. Practically speaking, the information that traditional implementation can supply is limited to what a RuntimeException can support: stack traces and error messages. Every Java programmer has experienced the joy of staring at a stack trace without a clue as to what really happened. Stack traces show what happened and where it happened but not why it happened. Ideally, you want to know which methods were called and how they were called—the types and values of arguments passed to each method leading up to the fault. Scattering code across every method to record its arguments upon entry is unpleasant, impractical, error prone, and a waste of effort unless a fault actually occurs.

Aspects, Pointcuts, and Advice

Aspect programming was invented to address problems like this. In our case, all the components in the application have to be concerned about the rules for faults and contingencies. If there is a lapse in a single class, its effect can ripple across any number of unrelated classes, causing the larger exception model to fail. Also, we have the Fault Barrier doing the job of recording analytic information when its natural role is simply to know how to generate a generic response to the external world and perform cleanup operations.

The AOP idea is to encapsulate the required behaviors in a single entity: an aspect. An aspect contains logic that runs at certain well-defined points across the application. The logic that runs is called advice. The points at which advice is applied are called join points. You specify the sets of join points advice applies to by defining pointcuts. A pointcut is basically an expression that filters through all of the potential join points in your application, choosing some based on criteria such as the kind of join point, and various types of pattern matching. If you craft the aspect properly, it performs actions that would otherwise be scattered around the application. With everything in one place, the components in the rest of the application are free to concentrate on their primary jobs. The result is greater component cohesion, and that is a good thing.

The sample application containing our exception-handling aspect is built using AspectJ, a superset of the Java language. The language supports the diversity of join points that are significant to exception handling across any application. Exceptions can be generated and caught in lots of ways during the execution of a Java application. Method executions are just one of them. An exception handling aspect also needs to worry about constructor execution, object initialization, and class initialization, which can all result in exceptions. It also needs to worry about places where exceptions are explicitly thrown and caught. AspectJ's pointcut language supports everything needed to implement the model we have in mind.

The ExceptionHandling Aspect

The ExceptionHandling aspect is designed to be as flexible as possible so it has only two compile-time dependencies. They are the two Java classes that represent faults and contingencies:

  • FaultException - A subclass of RuntimeException that represents fault conditions.
  • ContingencyException - A subclass of Exception that represents contingency outcomes. Subclasses represent specific contingency conditions.

The aspect assumes (and enforces) that the rest of the application uses these classes according to model rules. A nice feature of the AspectJ system is the ability to "program the compiler" to enforce a policy that transcends standard Java language rules. In our case, we want to encourage developers to think in terms of faults and contingencies and clearly distinguish between them. Since our architecture provides a ContingencyException base class, we want to ensure that developers use its subclasses exclusively to represent contingency conditions. By doing so, we will prevent the temptation to declare that a method throws SQLException (for example), when the method should be treating any unexpected JDBC problem as a fault.

The ExceptionHandling aspect uses pointcuts to detect methods and constructors that declare checked exceptions other than those based on ContingencyException. Violations are flagged as compile errors, a sure way to draw attention to the problem.


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";



        ...

}

Listing 1. Compile-time exception policy enforcement

In the example below, the commit() method of the Transaction class violates the exception policy by declaring that it throws SQLException, which should be considered to be a fault in its context. The compiler flags the violation as a compile error, based on the declaration in the ExceptionHandling aspect. The rollback() method conforms to the model by treating an unexpected SQLException as a fault, so no flag appears. The examples developed for this article were developed using Eclipse 3.2.1 with the AspectJ Development Tools (AJDT) 1.4.1 plug-in installed.

Exception Policy Violation Flagged as Compile Error

Figure 1. Exception policy violation generates an error flag

Exception Advice Join Points

The ExceptionHandling aspect uses the exceptionAdvicePoints() pointcut to apply advice to any execution sequence capable of throwing an exception. The aspect uses this pointcut several times to inject processing when exceptions might be thrown. The pointcut includes these join points:

  • All Method executions
  • All Constructor executions
  • All Object initializations
  • All Object preinitializations
  • All Class initializations

Since the ExceptionHandling aspect has its own methods, has its own constructor, and undergoes class and object initialization, some of the join points selected above fall within the aspect itself. That's generally a bad thing, causing recursions when the aspect tries to advise its own advice. To avoid this possibility, the exceptionAdvicePoints() pointcut specifically excludes these join points from those selected above:

  • Any join point within the aspect's own lexical scope
  • Any join point within the lexical scope of its subaspects
  • Any join point in the control flow of advice execution

public abstract aspect ExceptionHandling {

        ...



    pointcut exceptionAdvicePoints():

           (execution (* *.*(..))

         || execution (*.new(..))

         || initialization(*.new(..))

         || preinitialization(*.new(..))

         || staticinitialization(*))

         && !within(ExceptionHandling+)

         && !cflow(adviceexecution());



        ...

}

Listing 2. Exception advice points

Now, the ExceptionHandling aspect is able to apply exception-related advice to all application components outside of itself. It won't try to apply its advice to methods that may run as a result of executing its own advice.

Runtime Exception Translation

ThrowableThrowableFaultExceptionThrowableFaultExceptionThrowableExceptionHandling

The "after throwing" advice runs if an execution sequence throws any sort of Throwable. If the exception is a FaultException or ContingencyException, the advice takes no action. Otherwise, it replaces the offending exception with a new instance of FaultException, citing the uncaught exception as the cause. Note that the aspect's compile-time exception policy enforcement simplifies the check that the advice has to make.


public abstract aspect ExceptionHandling {

  ...



  after() throwing(Throwable throwable):exceptionAdvicePoints(){

    if (!(throwable instanceof FaultException || 

          throwable instanceof ContingencyException)) {

        throw new FaultException("Unhandled exception: ",

            throwable);

    }

  }



  ...

}

Listing 3. Runtime exception translation