Java Tip 91: Use nested exceptions in a multitiered environment

Automatically capture the context of remotely thrown exceptions through nesting

In this brave new world of business computing, multitiered applications based on RMI, or Remote Method Invocation, running across many computers have overrun the realm of the business application, also commonly known as the enterprise application. Logically separate tiers, such as a GUI tier and a database tier, divide responsibility and isolate the tasks of an enterprise application into well-defined areas that you can develop independently. A popular breakdown today is a three-tiered model: a client tier, which is solely responsible for presentation and user handling of business data; the middle tier, which implements the business logic; and the database tier, which stores and retrieves data. Unfortunately, multitiered applications introduce new debugging challenges over simpler, single-tiered designs.

Handling exceptions generated on remote tiers is one of the challenges enterprise programmers face. An exception bubbling up through the murky depths of multitier code can't convey the context in which it was thrown, except through its own exception type and error message. Those who haven't experienced the thrills of remote developing might ask, "What about the stack trace?" (The stack trace refers to the list of methods the executing thread has traversed to the current moment -- extremely valuable debugging information that every Java exception stores.) Unfortunately, the RMI framework throws it away when the exception crosses the RMI boundary.

Why is it thrown away? The stack trace is considered a transient property of an exception. When objects are sent across RMI, they are serialized -- converted into a stream of bytes that you can easily transmit over a network wire. This means that any object to be transmitted over RMI, including exceptions, must be serializable (by implementing the java.io.Serializable interface). Java programmers use the keyword transient to describe any field of a class that is considered temporary, such as a temporary file handle. Unfortunately for you, transient fields like the exception's stack trace are not serialized, and thus not reproduced on the new tier.

This makes it difficult to tell when and where a remote exception was thrown, especially when it's a runtime exception like NullPointerException. Without a stack trace, it's guesswork. You must either use a remote debugger (usually not an option at the customer's site) or rely on debugging messages. In terms of giving Joe End-User an error message, forget it. You don't even know what the problem is.

An example

Imagine you're in charge of developing an application to manage and configure a security system, which includes several kinds of remote sensors, cameras, and other devices. Each device type has unique configuration commands. Each type reports security information. Information about these devices must be persistent (stored in a database), such as the physical location of the devices, their configuration information, the IP address of the devices, and so forth. Finally, all of the devices must be managed from a remote user interface. This entails configuring the devices, presenting information generated by the devices, and storing information about them.

This design problem begs for a multitier approach. You decide that you'll use a Java applet for a client. You'll write a custom application server, in Java, that accepts RMI requests from the applet. So, you need a database and a way to talk to the security devices, which use their own Wacky Security-Device Protocol (WSDP). To that end, you'll write a class that knows how to talk to them via WSDP, which I call a WSDPBridge. For a technical reason, which I'll leave unspecified (OK, it makes the example better), the WSDPBridge must exist on another machine from the server, so the server and bridge also communicate through RMI.

To sum up, the design has four components: a Java applet for the client, a database for persisting configuration information, a bridge to talk to the security devices, and a custom server that coordinates all of the above. See Figure 1.

Figure 1. Security device management system as a multitier application

The problem

OK, enough setup. Here's the problem. Let's say the user wants to configure a device using the client interface. The client code invokes a remote method on the server that triggers a mess of activity, which can cause a number of errors to occur along the way: the RMI connection from the client to the server can fail; database calls can fail; the RMI connection from the server to the WSDPBridge can fail; communication from the WSDPBridge to the security device can fail; an invalid configuration state can occur. All of these conditions are represented by exceptions: RemoteException, SQLException, WSDPBridgeException, IOException, and InvalidConfigurationException.

The problem lies with the fact that these exceptions are all generated on different remote tiers. The SQLException occurs on the server; the WSDPBridgeExceptions occur on the WSDPBridge (oddly enough), as well as the IOException and InvalidConfigurationException. The RemoteException can happen in two different places: either on the client while trying to connect to the server or on the server while trying to connect to the WSDPBridge. Further, the stack trace is lost as soon as any of these exceptions are serialized.

How can the client handle all these exceptions? A common strategy is for the client applet to catch all exceptions and display the error message in a dialog box. For example, a client might invoke a configure attempt like this:

try {

// we've already looked up the server

rmiServer.configureDevice(device, cfg_param, cfg_value);

} catch (Exception e) {

// handleError might pop up a dialog box

handleError(e.getMessage());

...

}

This approach is extremely simple and too limited to be of much use. If you're lucky, the caught exception was thrown by your code, and you've defined unique strings in your exception to help you determine the context. Unfortunately, you must also deal with runtime and third-party exceptions (perhaps you're using a third-party WSDPBridge). In these cases, you might have no idea what the context of the exception is. It must be inferred, which makes debugging a big problem. For example, you can't tell whether that RemoteException was signaling a faulty connection between the client and server or between the server and bridge. Don't you wish you had that stack trace?

Finally, the lack of context makes generating meaningful error messages nearly impossible. Under this approach, client interfaces are usually forced to limp along with a woefully bland error message such as "Configuration failed." It may also mumble something about contacting the system administrator.

A solution

An elegant solution to the above problem is to use so-called nested exceptions along with a cleanly defined exception structure (which I'll cover below). Enter the NestingException class. It allows an exception to be stored as a read-only property, overrides the getMessage() method to append the nested exception's message, and provides a way to sneak that stack trace past the RMI border. If only immigration were this easy.

import java.io.StringWriter; import java.io.PrintWriter;

public class NestingException extends Exception { // the nested exception

private Throwable nestedException; // String representation of stack trace - not transient!

private String stackTraceString; // convert a stack trace to a String so it can be serialized

static public String generateStackTraceString(Throwable t) {

StringWriter s = new StringWriter();

t.printStackTrace(new PrintWriter(s));

return s.toString();

} // java.lang.Exception constructors

public NestingException() {} public NestingException(String msg) {

super(msg);

} // additional c'tors - nest the exceptions, storing the stack trace

public NestingException(Throwable nestedException) {

this.nestedException = nestedException;

stackTraceString = generateStackTraceString(nestedException);

} public NestingException(String msg, Throwable nestedException) {

this(msg);

this.nestedException = nestedException;

stackTraceString = generateStackTraceString(nestedException);

} // methods

public Throwable getNestedException() {return nestedException;} // descend through linked-list of nesting exceptions, & output trace

// note that this displays the 'deepest' trace first

public String getStackTraceString() {

// if there's no nested exception, there's no stackTrace

if (nestedException == null)

return null; StringBuffer traceBuffer = new StringBuffer(); if (nestedException instanceof NestingException) {

traceBuffer.append(((NestingException)nestedException).getStackTraceString());

traceBuffer.append("-------- nested by:\n");

} traceBuffer.append(stackTraceString);

return traceBuffer.toString();

} // overrides Exception.getMessage()

public String getMessage() {

// superMsg will contain whatever String was passed into the

// constructor, and null otherwise.

String superMsg = super.getMessage(); // if there's no nested exception, do like we would always do

if (getNestedException() == null)

return superMsg; StringBuffer theMsg = new StringBuffer(); // get the nested exception's message

String nestedMsg = getNestedException().getMessage(); if (superMsg != null)

theMsg.append(superMsg).append(": ").append(nestedMsg);

else

theMsg.append(nestedMsg); return theMsg.toString();

} // overrides Exception.toString()

public String toString() {

StringBuffer theMsg = new StringBuffer(super.toString()); if (getNestedException() != null)

theMsg.append("; \n\t---> nested ").append(getNestedException()); return theMsg.toString();

} }

You can see that it overrides the two constructors of the java.lang.Exception, and adds two of its own, which take the nested exception as an argument and store it. The overridden getMessage() method builds a message by concatenating the message from each nested exception in the chain. Finally, a static method helps fool RMI into passing you the stack trace by converting it into a nontransient String. This method is called when the NestedException is constructed. The instance method getStackTraceString() provides a way to get the complete stack trace, all the way down the nested chain.

Applying nesting exceptions

Let's look at how the client looks, using nesting exceptions. Instead of catching just the base Exception, you first catch ConfigureException, which extends the NestingException. I'll look closer at the ConfigureException below, but the client code looks like this:

try {

rmiServer.configureDevice(device, cfg_param, cfg_value);

} catch (ConfigureException e) {

// handleError might pop up a dialog box

handleError(e.getMessage());

...

} catch (Exception e) {

// handle RemoteException & runtime exceptions

}

Here is the server implementation of configureDevice(). This is where the magic happens. Note that the server throws only two exceptions: the RemoteException (which all remote methods must throw) and the newly defined ConfigureException. Pay attention to what happens when you catch the various exceptions that can be thrown.

public void configureDevice(Device dev, String param, String value)

throws RemoteException, ConfigureException {

try {

// perform database look-up for device's IP address

dev.setIPAddress(DBLib.getDeviceIP(dev));

} catch (Exception e) {

// catching java.sql.SQLException & runtime exceptions

throw new ConfigureException("device IP lookup failed", e);

} try {

// rmi lookup of WSDPBridge

rmiWSDPBridge = (WSDPBridge) Registry.lookup(..);

rmiWSDPBridge.configure(dev, param, value);

} catch (Exception e) {

// catching RemoteException, WSDPBridgeException, & runtime

throw new ConfigureException("configuration failed", e);

} }

Here, you're catching the various exceptions that can occur, and then stuffing them right into the newly created ConfigureException, nice and neat. Now you only have to throw one type of exception (plus the RemoteException) to represent all the things that can go wrong on this tier. Yet, you hold on to the lower-level exception, which will prove valuable, as you will see.

Looking at configureDevice(), it throws only the ConfigureException (and the RemoteException), yet it performs operations that throw three implementation-specific exceptions (not to mention the happy runtime exceptions). Instead of passing all these different exceptions up the stack (by declaring them in the throws clause of the method), you nest the lower-level exceptions inside the high-level ConfigureException, then throw the ConfigureException instead. This abstracts the details of the implementation from the caller (the client), which is good design, and resonates nicely with Java's bent towards interface-driven architecture.

1 2 Page 1