Exception management and error tracking in J2EE

Develop an exception framework for handling errors in the J2EE world

The perpetual debate on exception handling in Java can at best be described as a religious war: On one side, you have the proponents of checked exceptions arguing that callers should always deal with error situations arising in code they call. On the other side stand the followers of unchecked exceptions pointing out that checked exceptions clutter code and often can't be handled in immediate clients anyway, so why force it?

As junior engineers, we first sided with the proselytes of checked exceptions, but over the years, and after many, many catch blocks later, we have gradually converted to the order of the unchecked. Why? We have come to believe in a simple set of rules for dealing with error situations:

  1. If it makes sense to handle the exception, do so
  2. If you can't handle it, throw it
  3. If you can't throw it, wrap it in an unchecked base exception and then throw it

But what about those exceptions that bubble all the way to the top, you ask? For those, we install a last line of defense to ensure error messages are always logged and the user is properly notified.

This article presents yet another framework for exception handling, which extends the enterprise-wide application session facility presented in "Create an Application-Wide User Session for J2EE" (JavaWorld, March 2005). J2EE applications that use this framework will:

  1. Always present meaningful error messages to users
  2. Log unhandled error situations once and only once
  3. Correlate exceptions with unique request IDs in log files for high-precision debugging
  4. Have a powerful, extensible, yet simple strategy for exception handling at all tiers

To forge the framework, we wield aspect-oriented programming (AOP), design patterns, and code generation using XDoclet.

You will find all the code in Resources along with a sample J2EE application that uses it. The source constitutes a complete framework named Rampart, which was developed for Copenhagen County, Denmark in the context of J2EE-based electronic healthcare records (EHR) applications.

Why do we need common error handling?

During a project's initial state, significant architecture decisions are made: How will software elements interact? Where will session state reside? What communication protocols will be used? And so on. More often than not, however, these decisions do not include error handling. Thus, some variant on the happy-go-lucky strategy is implemented, and, subsequently, every developer arbitrarily decides how errors are declared, categorized, modeled, and handled. As an engineer, you most likely recognize the results of this "strategy":

  1. Swollen logs: Every catch block contains a log statement, leading to bloated and redundant log entries caused by polluted source code.
  2. Redundant implementations: The same type of error has different representations, which complicates how it is handled.
  3. Broken encapsulation: Exceptions from other components are declared as part of the method signature, breaking the clear division between interface and implementation.
  4. Noncommittal exception declaration: The method signature is generalized to throw java.lang.Exception. This way, clients are ensured not to have the least clue about the method's error semantics.

A common excuse for not defining a strategy is that "Java already provides error handling." This is true, but the language also offers facilities for consistently declaring, signaling, propagating, and responding to errors. The language user is responsible for deciding how these services should be used in an actual project. Several decisions must be made, including:

  1. To check or not to check: Should new exception classes be checked or unchecked?
  2. Error consumers: Who needs to know when an unhandled error occurs, and who is responsible for logging and notifying operations staff?
  3. Basic exception hierarchy: What kind of information should an exception carry and what semantics does the exception hierarchy reflect?
  4. Propagation: Are nonhandled exceptions declared or transformed into other exception classes, and how are they propagated in a distributed environment?
  5. Interpretation: How are unhandled exceptions turned into human readable, even multilingual messages?

Encapsulate rules in a framework, but hurry!

Our recipe for a common error-handling strategy builds on the following shortlist of ingredients:

  1. Use unchecked exceptions: By using checked exceptions, clients are forced to take a position on errors they can rarely handle. Unchecked exceptions leave the client with a choice. When using third-party libraries, you don't control whether exceptions are modeled as checked or unchecked ones. In this case, you need unchecked wrapper exceptions to carry the checked ones. The biggest tradeoff in using only unchecked exceptions is that you can't force clients to handle them anymore. Yet, when declared as part of the interface, they remain a crucial element of the contract and continue to be part of Javadoc documentation.
  2. Encapsulate error handling and install a handler on top of each tier: By having a safety net, you can focus on handling only exceptions relevant to business logic. The handler performs the safe touchdown for the remaining exceptions at the specific tier executing standardized steps: logging, system management notification, transformations, etc.
  3. Model the exception hierarchy using a "simple living" approach: Don't automatically create new exception classes whenever new error types are discovered. Ask yourself if you are simply dealing with a variation of another type and if the client code is likely to explicitly catch it. Remember that exceptions are objects whose attributes can, at least to some extent, model the variation of different situations. Less than a handful of exception classes will most likely prove enough to satisfy a starting point, and only those that are likely to be handled need specialized attributes.
  4. Give meaningful messages to end users: Unhandled exceptions represent unpredictable events and bugs. Tell this to the user and save the details for the technical staff.

Although needs and constraints, exception hierarchies, and notification mechanisms will differ across projects, many elements remain constant. So why not go whole hog and implement common policies in a framework? A framework followed by simple rules of usage is the best way to enforce a policy. Executable artifacts talk to developers, and it is easier to preach architectural principles with a jar file and some Javadoc than with whitepapers and slide shows.

However, you cannot ask the development team to postpone error handling until a policy and complementing framework support is ready. Error handling must already be determined when the first source file is created. A good place to start is by defining the fundamental exception hierarchy.

A basic exception hierarchy

Our first practical task is to define an exception hierarchy common enough to be used across projects. The base class for our own unchecked exceptions is UnrecoverableException, a name that, for historical reasons, remains slightly misleading. You might consider a better title for your own hierarchies.

When you want to get rid of a checked exception, one that, conceivably, clients will always be able to handle, WrappedException offers a simple, generic transport mechanism: wrap and throw. The WrappedException keeps the cause as an internal reference, which works well when the classes for the original exception are still available. When this is not the case, use SerializableException, which resembles WrappedException except that it can be used when no assumptions are made on available libraries at the client side.

Although we prefer and recommend unchecked exceptions, you might keep checked exceptions as an option. The InstrumentedException interface acts as an interface for both checked and unchecked exceptions that follow a certain pattern of attribute implementation. The interface allows exception consumers to consistently inspect the source—whether it inherits from a checked or an unchecked base.

The class diagram below shows our basic exception hierarchy.

Figure 1. A basic exception hierarchy

At this point, we have a strategy and a set of exceptions that can be thrown. It is time to build the safety net.

The last line of defense

The article "Create an Application-Wide User Session" presented Rampart, a layered architecture consisting of an enterprise information system tier, a business logic tier made from stateless session beans, and a client tier with both Web and standalone J2SE clients. Exceptions can be thrown from all tiers in this architecture, and can either be handled on site or bubble up until they reach the end of the call chain. Then what? Both J2SE and J2EE application servers guard themselves against offensive behavior by catching stray Errors and RuntimeExceptions, and by dumping the stack trace to System.out, logging it, or performing some other default action. In any case, if a user is presented with any kind of output, mostly likely, it will prove absolutely meaningless, and, worse, the error will probably have disrupted program stability. We must install our own rampart to provide a more sound exception-handling mechanism for this last line of defense.

Consider Figure 2:

Figure 2. Exception paths in the sample architecture

Exceptions may occur on the server side in the EJB (Enterprise JavaBeans) tier and in the Web tier, or on the standalone client. In the first case, exceptions stay in the VM from which they originated as they make their way up to the Web tier. This is where we install our top-level exception handler.

In the standalone case, exceptions eventually reach the rim of the EJB container and travel along the RMI (remote method invocation) connection to the client tier. Care must be taken not to send any exceptions belonging to classes that live only on the server side, e.g., from object-relational mapping frameworks or the like. The EJB exception handler handles this responsibility by using SerializableException as a vehicle. On the client side, a top-level Swing exception handler catches any stray errors and takes appropriate action.

Exception-handler framework

An exception handler in the Rampart framework is a class that implements the ExceptionHandler interface. This interface's only method takes two arguments: Throwable, to handle, and the current Thread. For convenience, the framework contains an implementation, ExceptionHandlerBase, which tastes Throwable and delegates handling to dedicated abstract methods for the flavors RuntimeException, Error, Throwable, and the Rampart-specific Unrecoverable. Subclasses then provide implementations for these methods and handle each situation differently.

The class diagram below shows the exception-handler hierarchy with its three default exception handlers.

Figure 3. Exception-handler hierarchy. Click on thumbnail to view full-sized image.

Some among The Order of the Unchecked believe that Sun should have added hooks into all containers in the J2EE architecture on a per-application basis. This would have allowed custom error-handling schemes, security, and more to be gracefully installed, without reliance on vendor-specific schemes and frameworks. Unfortunately, Sun failed to provide such mechanisms in the EJB specification; so, we pull out the AOP hammer from our toolbox and add exception handling as around-aspects. AspectWerkz, our chosen AOP framework, uses the following aspect for that task:

 

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

The actual handler is configurable and obtained through the ConfigHelper class. If a RuntimeException or Error is thrown during execution of the bean business logic, the handler will be asked to handle it.

The DefaultEJBExceptionHandler serializes the stack trace of any exception not originating from Sun's core packages into a dedicated SerializableException, which, on the plus side, allows the stack trace of exceptions whose classes don't exist on remote clients to be propagated over anyway. On the downside, the original exception is lost in translation.

EJB containers faithfully take any RuntimeException or Error and wrap it in a java.rmi.RemoteException, if the client is remote, and in a javax.ejb.EJBException, otherwise. In the interest of keeping causes precise and stack traces at a minimum, the framework peels off these transport exceptions inside client BusinessDelegates and rethrows the original.

A Rampart BusinessDelegate class exposes an EJB-agnostic interface to clients, while wrapping local and remote EJB interfaces internally. BusinessDelegate classes are generated via XDoclet from EJB implementation classes and follow the structure shown in Figure 4's UML diagram:

Figure 4. An example BusinessDelegate
1 2 3 Page
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more