Java EE - Add CDI Interceptor programmatically
Introduction
Java EE interceptors may be used along with CDI managed beans. This means that one may use an interceptor in order to intercept calls to CDI managed beans methods and do some pre or post processing inside the interceptor (ex: audit method calls, check for authorization, etc.).
CDI extensions provide a powerful infrastructure that allows an application to capture container initialization lifecycle events and act accordingly to those events (ex: alter managed beans metadata, change managed beans property values, intercept injection of managed beans into other managed beans, among other operations).
In this article we will see how to programmatically add an interceptor to a CDI managed bean by the means of a CDI extension.
This tutorial considers the following environment:
- Ubuntu 12.04
- JDK 1.7.0.21
- Glassfish 4.0
The interceptor
In this article we will use an illustrative Audit interceptor which definition follows next (we will not cover the Java EE interceptor infrastructure in this article since it is not its main focus):
package com.byteslounge.audit; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.TYPE; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import javax.interceptor.InterceptorBinding; @Inherited @InterceptorBinding @Retention(RetentionPolicy.RUNTIME) @Target({ METHOD, TYPE }) public @interface Audit { }
First we defined the Audit interceptor annotation. Now we define the interceptor itself:
package com.byteslounge.audit; import java.io.Serializable; import javax.interceptor.AroundInvoke; import javax.interceptor.Interceptor; import javax.interceptor.InvocationContext; @Audit @Interceptor public class AuditInterceptor implements Serializable { private static final long serialVersionUID = 1L; @AroundInvoke public Object auditMethodCall(InvocationContext invocationContext) throws Exception { System.out.println("Intercepting call to method: " + invocationContext.getMethod().getName()); return invocationContext.proceed(); } }
Now one may use the interceptor by annotating a managed bean with @Audit:
@Audit public class TestBean { }
Every method call made to TestBean will now be intercepted by our AuditInterceptor.
What if we need to wire the interceptor programmatically, i.e. the TestBean class is not annotated with @Audit? We will see how to do the interceptor wiring programmatically in the next section.
Wiring the CDI interceptor programmatically
As we have said in the introduction, a CDI extension may be used, among other things, to intercept container lifecycle initialization events and change managed beans metadata.
Every CDI managed bean that is processed by the container during container initialization will trigger a ProcessAnnotatedType event. We will implement a CDI extension that will listen for this event and check if the type being processed is TestBean. If this is the case, we will add the @Audit annotation to the managed bean metadata.
package com.byteslounge.extension; import java.lang.annotation.Annotation; import javax.enterprise.event.Observes; import javax.enterprise.inject.spi.AnnotatedType; import javax.enterprise.inject.spi.Extension; import javax.enterprise.inject.spi.ProcessAnnotatedType; import com.byteslounge.audit.Audit; import com.byteslounge.beans.TestBean; public class AuditExtension implements Extension { public <T> void processAnnotatedType( @Observes ProcessAnnotatedType<T> processAnnotatedType) { AnnotatedType<T> annotatedType = processAnnotatedType .getAnnotatedType(); if (annotatedType.getJavaClass().equals(TestBean.class)) { Annotation auditAnnotation = new Annotation() { @Override public Class<? extends Annotation> annotationType() { return Audit.class; } }; AnnotatedTypeWrapper<T> wrapper = new AnnotatedTypeWrapper<T>( annotatedType, annotatedType.getAnnotations()); wrapper.addAnnotation(auditAnnotation); processAnnotatedType.setAnnotatedType(wrapper); } } }
The first thing we need in our extension is to implement the javax.enterprise.inject.spi.Extension interface.
As we have said before, we are listening to the ProcessAnnotatedType event so we will intercept the initialization - or processing - of every CDI bean that is managed by the container.
We check the type of the managed bean being processed. If the bean type is TestBean we add the @Audit annotation to the bean annotation set.
Note that we are using a wrapper. This is because a CDI managed bean annotation set is implemented by an unmodifiable set. In order to solve this we create a wrapper that will delegate all calls to the wrapped object except the ones that are related with the annotation set. In this later case the wrapper will use its own annotation set that contains the original set plus the annotations added in the extension.
Finally we override the annotated type being processed with the wrapper: processAnnotatedType.setAnnotatedType(wrapper).
The wrapper definition follows next:
package com.byteslounge.extension; import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.util.HashSet; import java.util.Set; import javax.enterprise.inject.spi.AnnotatedConstructor; import javax.enterprise.inject.spi.AnnotatedField; import javax.enterprise.inject.spi.AnnotatedMethod; import javax.enterprise.inject.spi.AnnotatedType; public class AnnotatedTypeWrapper<T> implements AnnotatedType<T> { private final AnnotatedType<T> wrapped; private final Set<Annotation> annotations; public AnnotatedTypeWrapper(AnnotatedType<T> wrapped, Set<Annotation> annotations) { this.wrapped = wrapped; this.annotations = new HashSet<>(annotations); } public void addAnnotation(Annotation annotation) { annotations.add(annotation); } @Override public <A extends Annotation> A getAnnotation(Class<A> annotationType) { return wrapped.getAnnotation(annotationType); } @Override public Set<Annotation> getAnnotations() { return annotations; } @Override public Type getBaseType() { return wrapped.getBaseType(); } @Override public Set<Type> getTypeClosure() { return wrapped.getTypeClosure(); } @Override public boolean isAnnotationPresent( Class<? extends Annotation> annotationType) { for (Annotation annotation : annotations) { if (annotationType.isInstance(annotation)) { return true; } } return false; } @Override public Set<AnnotatedConstructor<T>> getConstructors() { return wrapped.getConstructors(); } @Override public Set<AnnotatedField<? super T>> getFields() { return wrapped.getFields(); } @Override public Class<T> getJavaClass() { return wrapped.getJavaClass(); } @Override public Set<AnnotatedMethod<? super T>> getMethods() { return wrapped.getMethods(); } }
As we have said before, we defined a wrapper that will be set as the annotated type being processed by the container. The wrapper will delegate all calls to the wrapped object except the ones that are related with the annotation set, which will be handled by the wrapper itself (the wrapper annotation set contains the original annotation set plus the @Audit annotation that was added by the CDI extension).
CDI extension configuration
The last step is the CDI extension configuration.
In order to properly register a CDI extension one must create a file named javax.enterprise.inject.spi.Extension and place it inside /META-INF/services folder.
The file content must have the fully qualified name of the extension:
com.byteslounge.extension.AuditExtension
Article source code
The source code covering the article content is available for download at the end of this page. We used a JSF view in order to access a getter of a TestBean CDI managed bean and checked that the interceptor is called as expected, so the programmatic interceptor wiring was correctly done.