More on CDI interceptor bindings

Posted by    |       CDI

In reply to this post, Matt writes:

Where I still have a bit of trouble with this is if we implement each of the six EJB TransactionAttributeTypes with separate interceptors and bindings, and package them in a .jar file... I'm ok with having some kind of enablement in the beans.xml file for my .war, but to have the user enter all six interceptors is awkward for a library developer to force on someone...

For a couple of hours I thought he had me on this one, but it turns out that CDI has a fairly neat solution.

What Matt is trying to do is have a different annotation for each transaction propagation style, for example @RequiresTransaction, @RequiresNewTransaction, @MandatoryTransaction, etc, instead of having a single @Transactional annotation which specifies the propagation style using an annotation member. He's worried that CDI will force him to use a separate interceptor for each annotation, and then declare all these interceptor classes in beans.xml.

But there's a better way :-)

First, let's create an interceptor binding type that specifies the propagation style using a member. Note that we're not going to use this directly in our beans.

@Inherited 
@InterceptorBinding 
@Target({TYPE, METHOD}) 
@Retention(RUNTIME) 
public @interface Transactional {
    @Nonbinding
    TransactionPropagation value() default REQUIRED;
}

Where TransactionPropagation is an enumeration of propagation styles:

public enum TransactionPropagation { REQUIRED, REQUIRES_NEW, MANDATORY, ... }

CDI interceptor bindings can be inherited by other interceptor bindings. This feature allows us to use our @Transactional annotation as a meta-annotation. So let's apply @Transactional to Matt's annotations, for example:

@Inherited 
@Transactional(REQUIRED)
@InterceptorBinding 
@Target({TYPE, METHOD}) 
@Retention(RUNTIME) 
public @interface RequiresTransaction {}
@Inherited
@Transactional(REQUIRES_NEW)
@InterceptorBinding 
@Target({TYPE, METHOD}) 
@Retention(RUNTIME) 
public @interface RequiresNewTransaction {}

We use these interceptor bindings by annotating the bean class, for example:

@RequiresTransaction
public class Users {
    @RequiresNewTransaction
    public void login() { ... }
}

An annotation at the method level should override an annotation at the class level.

Now we can implement transaction management using a single interceptor class:

@Transactional @Interceptor
class TransactionInterceptor {

    @AroundInvoke
    public Object manageTransaction(InvocationContext ctx) throws Exception {
        TransactionPropagation tp = getTransactionPropagation(ctx.getMethod());
        ...
    }
    
    /**
     * Get the TransactionPropagation for the meta-annotation
     */
    private TransactionPropagation getTransactionPropagation(Method m) {
        //first look at method-level annotations, since they take priority
        for (Annotation a: m.getAnnotations()) {
            if (a.annotationType().isAnnotationPresent(Transactional.class)) {
                return a.annotationType().getAnnotation(Transactional.class).value();
            }
        }
        //now try class-level annotations
        for (Annotation a: m.getDeclaringClass().getAnnotations()) {
            if (a.annotationType().isAnnotationPresent(Transactional.class)) {
                return a.annotationType().getAnnotation(Transactional.class).value();
            }
        }
        return null;
    }
    
}

This interceptor class that implements all of the transaction propagation styles. Neat, huh?


Back to top