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

The BusinessDelegate class exposes all business methods from the source EJB implementation class and delegates to the LocalProxy class or the RemoteProxy class as appropriate. Internally, the two proxies handle EJB-specific exceptions and, hence, shield the calling BusinessDelegate from implementation details. The code below is a method from some LocalProxy class:

 public java.lang.String someOtherMethod()  {
   try {
      return serviceInterface.someOtherMethod();
   } catch (EJBException e) {
      BusinessDelegateUtil.throwActualException(e);
   }
   return null; // Statement is never reached
}

The serviceInterface variable represents the EJB local interface. Any EJBException instances thrown by the container to indicate an unexpected error are caught and handled by the BusinessDelegateUtil class, in which the following action takes place:

 

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;

} }

The actual exception is extracted and rethrown to the top-level client exception handler. When the exception reaches the handler, the stack trace will be that of the original exception from the server containing the actual error. No superfluous client-side trace is added.

Swing exception handler

The JVM provides a default top-level exception handler for each control thread. When exercised, this handler dumps the stack trace of Error and RuntimeException instances onto System.err and kills the thread! This rather obstructive behavior is quite far from anything a user needs and not elegant from a debugging perspective. We want a mechanism that allows us to notify the user while retaining the stack trace and a unique request ID for later debugging. "Create an Application-Wide User Session for J2EE" describes how such a request ID becomes available at all tiers.

For J2SE versions up to 1.4, uncaught exceptions in Thread instances cause the owner ThreadGroup's uncaughtException() method to execute. A simple way to control exception handling in an application, then, is to simply extend the ThreadGroup class, overwrite the uncaughtException() method, and ensure all Thread instances start within an instance of the custom ThreadGroup class.

J2SE 5 provides an even sweeter mechanism by allowing the installation of an UncaughtExceptionHandler implementation on instances of the Thread class itself. The handler is used as a callback mechanism when uncaught exceptions reach the Thread instance's run method. We built the framework with J2SE 1.3+ compatibility in mind; hence we used the ThreadGroup inheritance approach:

 

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

} }

The SwingThreadGroup class shown in the code snippet above overrides the uncaughtException() method and passes the current Thread instance and the thrown Throwable to the configured exception handler.

A little more magic is needed before we can claim control of all stray exceptions in the client tier. For the scheme to work, all threads must be associated with our specialized SwingThreadGroup instance. This is accomplished by spawning a new master Thread instance and passing the SwingThreadGroup instance with a Runnable implementation, which executes the entire main program. All Thread instances spawned from the body of this new master Thread instance automatically join the SwingThreadGroup instance and, hence, use our new exception handler when unchecked exceptions are thrown.

Figure 5. Swing exception handler class hierarchy. Click on thumbnail to view full-sized image.

The framework implements this logic in the SwingExceptionHandlerController convenience class shown in Figure 5. Applications provide an implementation of the SwingMain interface along with an exception handler to the controller. The controller must then be started, and the old main Thread can join the new one and wait for termination. The code below shows how the demo application that accompanies this article accomplishes that task. The method createAndShowGUI() constitutes the actual application body, which initializes Swing components and transfers control to the user.

 

public DemoApp() { SwingExceptionHandlerController.setHandler(new DefaultSwingExceptionHandler()); SwingExceptionHandlerController.setMain(new SwingMain() {

public Component getParentComponent() { return frame; }

public void run() { createAndShowGUI(); } });

SwingExceptionHandlerController.start(); SwingExceptionHandlerController.join(); }

The last line of defense is now in place for the Swing layer, but we still need to provide a meaningful message to the user. The demo application supplies a rather basic implementation that simply displays a dialog box with an internationalized message and the unique request ID as a support ticket. At the same time, the exception is logged via log4j along with the unique request ID. A more sophisticated error handler might send an email, SNMP message, or the like to technical support with the ID in it. The point is that both client and server logs can be filtered on the support ticket ID to allow precise pinpointing of errors on a per-request basis.

Figure 6 shows merged Swing client and J2EE server logs to provide the precise flow for the request with ID 1cffeb4:feb53del38:-7ff6, where an exception was thrown. Note that the stack trace contains only information from the server side, where the exception was thrown.

Figure 6. Log trace from both client and server with request ID and server stack trace. Click on thumbnail to view full-sized image.

While the infrastructure for adding exception handling to standalone J2SE applications is basic, things are different as we move to Web-based clients.

WAR exception handler

Web applications are among the fortunate components in the J2EE world to have their own explicit way of installing exception-handling capabilities. Through the web.xml deployment descriptor, exceptions and HTTP errors can be mapped to error pages in the shape of servlets or JavaServer Pages (JSP) pages. Consider the following fragment from a sample web.xml file:

 

<servlet> <servlet-name>ErrorHandlerServlet</servlet-name> <servlet-class>dk.rhos.fw.rampart.util.errorhandling.ErrorHandlerServlet</servlet-class> </servlet>

<servlet-mapping> <servlet-name>ErrorHandlerServlet</servlet-name> <url-pattern>/errorhandler</url-pattern> </servlet-mapping>

<error-page> <exception-type>java.lang.Throwable</exception-type> <location>/errorhandler</location> </error-page>

These tags direct all uncaught exceptions to the URL /errorhandler, which in this case is mapped to the ErrorHandlerServlet class. The latter is a dedicated servlet whose sole raison d'etre is to act as a bridge between Web components and the exception-handling framework. When an uncaught exception from this Web application reaches the servlet container, a set of parameters with information about the exception will be added to the HttpServletRequest instance and passed to the ErrorHandlerServlet class's service method. The fragment below shows the service() method:

 

... 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.currentThread(), exception);

String responsePage = (String)ConfigHelper.getRequestContextFactory(). getRequestContext(). getAttribute(ExceptionConstants.CONST_RESPONSEPAGE);

if(responsePage == null) { responsePage = "/error.jsp"; }

httpServletResponse.setStatus(HttpServletResponse.SC_OK);

RequestDispatcher dispatcher = httpServletRequest.getRequestDispatcher(responsePage); try { dispatcher.include(httpServletRequest, httpServletResponse); } catch (Exception e) { log.error("Failed to dispatch error to responsePage " + responsePage, e); } }

In the service() method, first, the actual exception from the HttpServletRequest instance is retrieved through the key javax.servlet.error.exception. Next, the exception handler instance is retrieved. From there on, the handler is invoked, and the HttpServletRequest instance is forwarded to the page specified by the key rampart.servlet.exception.responsepage.

The DefaultWARExceptionHandler class looks up an internationalized text based on the exception message and redirects output response to the JSP /error.jsp. This page is then free to display the message to the user, including the current request ID as a support ticket. A much more sophisticated mechanism can easily be implemented by simply extending or replacing this handler.

Wrap up

Often, exception handling is not handled stringently enough, thereby complicating debugging and error tracking, and, many times, disrupting the overall user experience. It is therefore crucial to have policies and frameworks in place before system development starts. Adding this aspect as an afterthought is doable, but time consuming and expensive.

This article has armed you with a starting point for defining an exception policy and introduced you to a simple, yet extensible, unchecked exception hierarchy. We have walked through business and client tiers of a sample J2EE architecture and shown how you can install top-level exception handlers for each tier to provide a last line of defense. The framework code has given you a way to precisely pinpoint errors on a per-user request basis through the unique request ID attached to exceptions and log entries.

So download the framework, try it out, modify it to your heart's delight, and get those exceptions under control.

Jens Byager works as principal consultant in the healthcare division of TietoEnator, the largest IT-services company in Scandinavia. He has a background as software engineer and architect within the banking industry, telecommunications, and logistics. He holds an A.P. in computer science from Niels Brock Copenhagen Business College and a B.S. in computer science from the University of Aalborg, Denmark. Kåre Kjelstrøm is the cofounder of Silverbullet, a Danish consulting company that helps design and architect enterprise applications. He worked at Trifork on the EAS J2EE Application Server and as a J2EE consultant for several customers in the areas of shipping and healthcare. For a couple of years, Kjelstrøm lived in Silicon Valley, where he worked as architect and developer in the areas of e-business process automation, Web services management, and J2EE application server appliances. He holds a B.S. and an M.S. in computer science from the University of Aarhus, Denmark. Kjelstrøm maintains an active Weblog at Drops of a Vapor Trail.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more