Before I dive into the details of the interception, I want to outline the reasons why client-side call interception might be useful.
Background
In our current project we aspired a RESTful component-based architecture. For each of the components it should be possible to integrate them in a service oriented architecture via WebService, deploy them in a JEE environment accessing it as JNDI-locateable services or access them via a RESTful lightweight interface. Each component should be deployable standalone and should be operational as long as their lower-level component dependencies are satisfied. To be RESTful, our architecture followed the seven principles of REST, described in the famous thesis of Roy Fielding.With JEE we could easily add caching behavior on the service side of a service consumer-provider relationship by adding an interceptor that serves a call with data from a cache and therefore reducing processing resource usage on the service side.
On the consumer side there is no such mechanism as to transparently intercept a service call before it is actually sent to the service. Such a client-side call interception could be of use for a range of use-cases
- caching, to reduce network resource usage
- error handling and fault tolerance
- logging
- security
Adding Interceptor support
In order to add call interceptors to the service client, we have to extend the invocation handler that delegates the service method invocation to the actual service by adding a stack of interceptors that are invoked consecutively.When a JEE interceptors is invoked an InvocationContext is passed to the interceptor. The interceptor may decide to fully intercept the request and not proceeding the invocation further. This is behavior is needed for caching or security concern. Alternatively, interceptors may execute some additional operations before or after proceeding with the invocation which is useful for logging or error handling.
The InvocationContext is an interface introduced by JEE6. I used my own implementation of this interface where the getter methods defined by the interface simply return the values that are passed to the InvocationHandler of the dynamic proxy (see the ProxyServiceInvocationHandler of the previous blog post), the getTimer() method returns null, as I don't really need it. The main functionality of the InvocationContext however resides in the proceed() method. I used a stack (Deque) to maintain a list of registered interceptors of which each is checked whether it may intercept the method invocation or not.
The check is done in the method isInterceptedBy and does check the class of the interceptor instance as well as the method whether they both are annotated with the same the InterceptorBinding annotation. I deviated a bit from the JEE6 standard as I allowed an interceptor without an explicit interceptor binding to intercept any method invocation, but that is up to you.
The main method of my InvocationContext implementation are shown in the next listing
class ProxyInvocationContext implements InvocationContext { ... @Override public Object proceed() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { if (!interceptorInvocationStack.isEmpty()) { final Object interceptor = interceptorInvocationStack.removeFirst(); if (isInterceptedBy(interceptor)) { final Method aroundInvoke = interceptorMethods.get(interceptor); return aroundInvoke.invoke(interceptor, this); } } return this.method.invoke(target, this.parameters); } private boolean isInterceptedBy(final Object interceptor) { boolean hasNoBinding = true; for (final Annotation an : interceptor.getClass().getAnnotations()) { if (an.annotationType().getAnnotation(InterceptorBinding.class) != null) { hasNoBinding = false; if (getMethod().getAnnotation(an.annotationType()) != null) { return true; } } } return hasNoBinding; } }
Instantiating Interceptors in CDI Context using DeltaSpike
Now we want to create interceptors and put them into the interceptor stack. The JEE 6 standard already defines interceptors so we could simply re-use this specification. JEE6 compliant interceptors may be used in a CDI lifecycle, that means lifecycle methods annotated with @PostConstruct or @PreDestroy are executed as well as injection of dependencies.With the implementation of the InvocationContext described above, we need a collection of interceptor instances. In our project I used a configuration file (similar to the beans.xml where you have to define which interceptors should be loaded) where I defined the interceptor classes that should be loaded.
Creating an instance of a class by the fully qualified name of the class is trivial using reflection (i.e. Class.forName("...").newInstance()). But when you have a CDI container you want to let the container do the creation and further process the lifecycle of the the instance including dependency injection.
JEE6 itself does not provide the means for that but it defines an SPI. The Apache DeltaSpike project offers an implementation for that SPI (btw. it is developed by the same guys behind CDI in JEE6 itself). DeltaSpike allows to interact directly with the CDI container.
The following code snippet assumes, that you have already loaded the interceptor class and verified it is annotated with the @Interceptor annotation. It first checks, if a CDI environment is active, if not, the interceptor is instantiated the traditional way. If it is active, a BeanManager reference is obtained, an injection target is created using the interceptor class. This is needed to create the instance and inject dependencies to the instance. With the injection target, an interceptor Bean is created using the BeanBuilder of DeltaSpike. The Bean is a Contextual instance that is required by the manager to create a creational context. Having this context we can create a managed instance using the create method. DeltaSpikes contribution to the snippet is the BeanManagerProvider , the BeanBuilder and the DelegatingContextualLifecycle.
private <I> I createInterceptor(final Class<I> interceptorClass) throws InstantiationException, IllegalAccessException { final I interceptor; if (BeanManagerProvider.isActive()) { final BeanManager manager = BeanManagerProvider.getInstance().getBeanManager(); final InjectionTarget<I> target = manager.createInjectionTarget( manager.createAnnotatedType(interceptorClass)); final Bean<I> interceptorBean = new BeanBuilder<I>(manager) .beanClass(interceptorClass) .beanLifecycle(new DelegatingContextualLifecycle<I>(target)) .create(); interceptor = interceptorBean.create( manager.createCreationalContext(interceptorBean)); } else { interceptor = interceptorClass.newInstance(); } return interceptor; }
No comments:
Post a Comment