Creating New Interceptors |
- Interceptor Factories
- Implementing the NullInterceptor
- Implementing the hivemind.LoggingInterceptor service
- Implementing Interceptors with Parameters
- Conclusion
Interceptors are used to add behavior to a HiveMind service after the fact. An interceptor sits between the client code and the core service implementation; it implements the service interface. For each method in the service interface, the interceptor will re-invoke the method on the next object in the chain ... either another interceptor, or the core service implementation.
That's not useful ... but when the interceptor does something before and/or after re-invoking the method, it can easily add quite a bit of useful, robust functionality.
In fact, if you've heard about "Aspect Oriented Programming", interceptors are simply one kind of aspect, a method introduction, based on the service interface.
Be warned; interceptors are an example of programs writing other programs; it's a whole new level of abstraction and requires a bit of getting used to. Also, note that the term "interceptor" can mean two different, related things: a service interceptor factory or a fabricated class created by the factory; this should be obvious by the context.
Interceptor Factories
Interceptors are created, at runtime, by service interceptor factories. A service interceptor factory builds a custom class at runtime using the Javassist library. The class is then instantiated.
Interceptor factories are HiveMind services which implement the ServiceInterceptorFactory interface. This interface has a single method, createInterceptor() , which is passed:
- The InterceptorStack (an object used to manage the process of creating interceptors for a service)
- The Module which invoked the interceptor factory
- A list of parameters
Like service implementation factories, interceptor factories may take parameters; they may identify a <schema> which is used to convert any XML enclosed by the <interceptor> element into Java objects. Many interesting interceptors can be created without needing parameters to guide the fabrication of the interceptor class.
Implementing the NullInterceptor
To demonstrate how easy it is to create an interceptor, we'll start with a NullInterceptor. NullInterceptor does not add any functionality, it simply re-invokes each method on its inner. The inner is the next interceptor, or the core service implementation ... an interceptor doesn't know or care which.
Simple interceptors, those which do not take any parameters, are implemented by subclassing AbstractServiceInterceptorFactory. It does most of the work, organizing the process of creating the class and methods ... even adding a toString() method implementation automatically.
NullInterceptor Class
Most of the work for creating a standard service interceptor factory is taken care of by the AbstractServiceInterceptorFactory base class. All that's left is to define what happens for each method in the service interface.
package com.example.impl; import java.lang.reflect.Modifier; import org.apache.hivemind.service.ClassFab; import org.apache.hivemind.service.impl.AbstractServiceInterceptorFactory; public class NullInterceptor extends AbstractServiceInterceptorFactory { protected void addServiceMethodImplementation( ClassFab classFab, String methodName, Class returnType, Class[] parameterTypes, Class[] exceptionTypes) { classFab.addMethod( Modifier.PUBLIC, methodName, returnType, parameterTypes, exceptionTypes, "{ return ($r) _inner." + methodName + "($$); }"); } }
The addServiceMethodImplementation() method is invoked for each service method. It is passed the ClassFab, an object which represents a class being fabricated, which allows new fields, methods and constructors to be added.
ClassFab and friends are just a wrapper around the Javassist framework, a library used for runtime bytecode enhancement and other aspect oriented programming tasks. HiveMind uses only a small fraction of the capabilities of Javassist. Javassist's greatest feature is how new code is specified ... it looks like ordinary Java source code, with a few additions.
The _inner variable is a private instance variable, the inner for this interceptor. The ($r) reference means "cast to the return type for this method", and properly handles void methods. The $$ is a placeholder for a comma-seperated list of all the parameters to the method.
Put together, this simply says "reinvoke the method on the next instance."
AbstractServiceInterceptorFactory is responsible for creating the _inner variable and building the constructor which sets it up, as well as invoking the constructor on the completed interceptor class.
Declaring the Service
To use a service, it is necessary to declare the service in a module deployment descriptor. The AbstractServiceInterceptorFactory base class expects two properties to be set when the service is constructed, serviceId and factory:
service-point (id=NullInterceptor interface=org.apache.hivemind.ServiceInterceptorFactory) { invoke-factory (service-id=hivemind.BuilderFactory) { construct (class=com.example.impl.NullInterceptor service-id-property=serviceId) { set-service (property=factory service-id=hivemind.ClassFactory) } } }
Implementing the hivemind.LoggingInterceptor service
A more involved example is the LoggingInterceptor service, which adds logging capabilities to services. It's a bit more involved than NullInterceptor, and so overrides more methods of AbstractServiceInterceptorFactory.
AbstractLoggingInterceptor base class
In most cases, an abstract base class for the interceptor is provided; in this case, it is AbstractLoggingInterceptor. This class provides several protected methods used by fabricated interceptors. To help ensure that there are no conflicts between the method of the service interface and the methods provided by the super-class, the provided methods are named with a leading underscore. These methods are:
- _logEntry() to log entry to a method
- _logExit() to log exit from a method, with return value
- _logVoidExit() to log exit from a void method (no return value)
- _logException() to log an exception thrown when the method is executed
- _isDebugEnabled() to determine if debugging is enabled or disabled
In addition, there's a protected constructor, which takes an instance of org.apache.commons.logging.Log that must be invoked from the fabricated subclass.
Method getInterceptorSuperclass() is used to tell AbstractServiceInterceptorFactory which class to use as the base:
protected Class getInterceptorSuperclass() { return AbstractLoggingInterceptor.class; }
Creating the infrastructure
The method createInfrastructure() is used to add fields and constructors to the interceptor class.
protected void createInfrastructure(InterceptorStack stack, ClassFab classFab) { Class topClass = stack.peek().getClass(); classFab.addField("_inner", topClass); classFab.addConstructor( new Class[] { Log.class, topClass }, null, "{ super($1); _inner = $2; }"); }
Since, when a interceptor is created, the inner object has already been created, we can use its actual type for the _inner field. This results in a much more efficient method invocation than if _inner's type was the service interface.
Instantiating the Instance
The method instantiateInterceptor() is used to create a new instance from the fully fabricated class.
protected Object instantiateInterceptor(InterceptorStack stack, Class interceptorClass) throws Exception { Object stackTop = stack.peek(); Class topClass = stackTop.getClass(); Log log = LogFactory.getLog(stack.getServiceExtensionPointId()); Constructor c = interceptorClass.getConstructor(new Class[] { Log.class, topClass }); return c.newInstance(new Object[] { log, stackTop }); }
This implementation gets the top object from the stack (the inner object for this interceptor) and the correct Log instance (based on the service extension point id ... for the service being extended with the interceptor). The constructor, created by createInfrastructure() is accessed and invoked to create the interceptor.
Adding the Service Methods
The last, and most complex, part of this is the method which actually creates each service method.
protected void addServiceMethodImplementation( ClassFab classFab, String methodName, Class returnType, Class[] parameterTypes, Class[] exceptions) { boolean isVoid = (returnType == void.class); BodyBuilder builder = new BodyBuilder(); builder.begin(); builder.addln("boolean debug = _isDebugEnabled();"); builder.addln("if (debug)"); builder.add(" _logEntry("); builder.addQuoted(methodName); builder.addln(", $args);"); if (!isVoid) { builder.add(ClassFabUtils.getJavaClassName(returnType)); builder.add(" result = "); } builder.add("_inner."); builder.add(methodName); builder.addln("($$);"); if (isVoid) { builder.addln("if (debug)"); builder.add(" _logVoidExit("); builder.addQuoted(methodName); builder.addln(");"); } else { builder.addln("if (debug)"); builder.add(" _logExit("); builder.addQuoted(methodName); builder.addln(", ($w)result);"); builder.addln("return result;"); } builder.end(); MethodFab methodFab = classFab.addMethod( Modifier.PUBLIC, methodName, returnType, parameterTypes, exceptions, builder.toString()); builder.clear(); builder.begin(); builder.add("_logException("); builder.addQuoted(methodName); builder.addln(", $e);"); builder.addln("throw $e;"); builder.end(); String body = builder.toString(); int count = exceptions == null ? 0 : exceptions.length; for (int i = 0; i < count; i++) { methodFab.addCatch(exceptions[i], body); } // Catch and log any runtime exceptions, in addition to the // checked exceptions. methodFab.addCatch(RuntimeException.class, body); }
A bit more is going on here; since the method bodies for the fabricated methods are more complex, we're using the BodyBuilder class to help assemble them (but still keep the final method body neat and readable). BodyBuilder's begin() and end() methods take care of open and close braces, as well as indentation inside a code block.
When you implement logging in your own classes, you often invoke the method Log.isDebugEnabled() multiple times ... but in the fabricated class, the method is only invoked once and cached for the duration of the call ... a little efficiency gained back.
Likewise, if a method can throw an exception or return from the middle, its hard to be assured that you've logged every exit, or overy thrown exception; taking this code out into an interceptor class ensures that its done consistently and properly.
Implementing Interceptors with Parameters
Interceptor factories may take parameters ... but then their implementation can't be based on AbstractServiceInterceptorFactory. The hivemind.LoggingInterceptor is an example of such a factory (its parameters determine which methods do and don't get logging). The basic approach is the same ... you just need a little extra work to validate, interpret and use the parameters.
When would such as thing be useful? One example is declarative security; you could specify, on a method-by-method basis, which methods were restricted to which roles.
Conclusion
Interceptors are a powerful concept that allow you to add consistent, efficient, robust behavior to your services. It takes a little while to wrap your brain around the idea of classes writing the code for other classes ... but once you do, a whole world of advanced techniques opens up to you!