Rounding Up Exceptions
Categories: Java
I stumbled upon a new technique today for dealing with java interfaces for interprocess communication. Whether it is a truly new idea I don’t know, but it is new to me. Part of the code already existed, but the “rounding up” concept is my own. This trick helps the client application in a client/server pair avoid deserialization exceptions when the server throws unknown exception types.
A java server may publish an interface like the following for client applications to use:
public interface SomeService {
SomeResultType doSomething(SomeParamType1 p1, SomeParamType p2) throws SomeException1, SomeException2;
...
}
The interface, together with all the referenced types (return-type, parameter types, exception types and all their transitive dependencies) is then bundled in a jarfile for clients to use. Both ends then use the shared types together with some implementation that does marshalling/transmission/unmarshalling of the specified types (eg Java RMI). Ideally, the tool providing network communication on the client side actually generates a proxy object implementing the interface, so client code can interact with the proxy almost the same way it would interact with a local implementation of the service.
The client can expect that the server has a copy of all the specified types, and (more importantly) the server can presume that the client has a copy of all the needed types. When the client loads the interface class, then the JVM will immediately resolve all the needed types, and immediately throw a NoClassDefException if something is missing - making it very clear where the problem is.
However there is a catch: the param/return/exception types only specify upper bounds for the types; unless these types are final concrete classes, then:
- the parameters passed by the client to the server can be subclasses of the specified types (which could cause an error when unmarshalling the data on the server);
- the server could return a subtype of the declared result type;
- the server could throw an exception that is a subtype of one of the declared types;
- the server could throw a RuntimeException
The first is not a problem; if the client invents its own types then it is fairly obvious that the server won’t understand them.
The second is an issue that can really only be solved by careful programming. However in client/server interfaces, both parameter and result objects are typically data transfer objects with little/no logic in them, and therefore subclassing is rare.
The last two, however are very tricky. It is quite likely that the server will throw different exception types under different conditions. And on the server side, it is difficult to keep track of this; complex business logic in services can call dozens of helper objects to perform the service, any of which might throw exceptions, and the java compiler will happily compile code that throws a subtype of the declared exceptions. It is also possible that code maintenance years later may add new exception types - which the compiler will be happy about. Yet, if some rare condition occurs which causes this exception to be thrown, and the client does not have the thrown class in its classpath, then deserialization of the response fails.
Declaring the thrown exceptions (SomeException1, SomeException2 in the example) as final will help with this - but at the expense of limiting the use of exceptions in the server. In addition, an exception can have a “cause” object of any type, which might not be known at the client end.
An elegant solution is to add code on the server-side that catches all exceptions thrown by the service implementation, and replaces any exceptions not explicitly declared in the interface with the nearest ancestor type which is declared in the service interface (and therefore must be available to the client - or classloading at the client side would have failed). So in the example, if the service implementation throws SomeException1SubType, then this is replaced with an instance of SomeException1 instead. The client side can then be sure it can deserialize the exception, and can catch it using the base type (as usual), while the server side is free to subclass exceptions however it wishes without worrying if these can propagate to the client.
RuntimeExceptions can potentially also be remapped to something that the client is known to support; in general, just ‘rounding up’ to one of a few known types is sufficient, as the client can’t be expected to handle many different runtime exception types.
Catching/rewriting the exceptions can be done through an AOP interceptor, or through more manual means. The code to “round up” exception objects is only a dozen lines or so; it isn’t particularly fast as it needs to use reflection, but considering (a) this is only needed when the service fails, and (b) this is an inherently slow network-based call anyway, performance isn’t a significant concern here.
Finding the right type can be done as follows:
...
// One way to determine the set of allowed exception types is to allow only those which this service method
// is declared to throw [which requires the interface to declare concrete exception types]. It is also
// possible to have a pool of allowed exception types (ie those that are known to be in the jarfile published
// for use by clients) and then select the subset which are subclasses of the exception types declared on this
// method).
Class<?> allowedTypes = method.getExceptionTypes();
Class<?> throwAs = getNearestAncestor(allowedTypes, thrownException.getClass());
Throwable replacementException = coerceExceptionTo(thrownException, throwAs);
throw replacementException;
}
Class<?> getNearestAncestor(Class<?>[] types, Class<?> childType) {
Class<?> bestMatch = null;
for(Class<?> candidate : types) {
if (candidate == childType) {
bestMatch = candidate;
break;
}
if (candidate.isAssignableFrom(childType)) {
if ((bestMatch == null) || candidate.isAssignableFrom(bestMatch)) {
bestMatch = candidate;
}
}
}
return bestMatch;
}
Throwable coerceExceptionTo(Throwable t, Class<?> newType) {
if (t.getClass() == newType) {
return t;
}
// do some reflection here to create a new instance of newType and copy over the
// exception message and possibly stacktrace.
}
Some care also has to be taken to deal with exception cause objects; they may be simply thrown away or replaced with some common type. Cause objects are not typically an important part of an interface intended for remoting.
The most significant drawback is that the service interface has to declare concrete exception types rather than just interfaces. One solution for this (which also helps with the “create a new instance of newType” part above) is to require “difficult” exception types (eg abstract types, or ones with nonstandard constructors) to declare a method that clones all data in the exception other than the cause. An abstract base exception type can then choose to provide an implementation, or declare it abstract.