CDI Transaction Interceptor

If you use EJB transaction you know that every method could be wrapped in a transaction. But if you use CDI you have to manage the transactions yourself. To make this easy I created a Interceptor that wraps a UserTransaction and will join the EntityManager in the Transaction. I’ll show you how.
First you have to define an annotation for the interceptor, so that CDI knows when to use what interceptor (kind of qualifier).

In this post I will use @UseTransaction annotation.
First define a annotation, it have to look like this:

/**
* Annotation to use {@link TransactionInterceptor}
*
* @author marcus
*/
@Inherited
@InterceptorBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface UseTransaction {

}

After that you have to implement the interceptor.
The Interceptor will looks like this.

/**
 * JTA Transaction interceptor.
 *
 * @author marcus
 */
@UseTransaction @Interceptor
public class TransactionInterceptor {

  private static final Logger LOG = LoggerFactory.getLogger(TransactionInterceptor.class);
  private static final String TRANSACTION_JNDI = "java:comp/UserTransaction";

 /**
  * Wrap method
  * @param ctx InvocationContext from Interceptor.
  * @return result
  * @throws Exception if something goes wrong.
  */
  @AroundInvoke
  public Object wrapTransaction(InvocationContext ctx) throws Exception {

    LOG.trace("Intercepting method");
    UserTransaction tx = getTransaction();
    if (tx == null) {
      // We are in a EJB transaction or no UserTransaction available. Don't wrap
      return ctx.proceed();
    }
    // Check nested transaction
    if (tx.getStatus() != Status.STATUS_ACTIVE) {
      // Start transaction
      LOG.trace("Starting UserTransaction");
      tx.begin();
      // Join EntityManager in this transaction
      joinEntityManagerTransaction();
      // Proceed intercepted method and store result.
      Object result = ctx.proceed();
      // Commit transaction
      tx.commit();
      LOG.trace("UserTransaction committed");
      // Return result
      return result;
    } else {
      return ctx.proceed();
    }
  }

 /**
  * @return Current active entity manager or null if none is found.
  */
  protected EntityManager getEntityManager() {

    try {
      // Get EntityManager by CDI producer (you have to define a Producer for EntityManager.
      BeanManager manager = CDI.current().getBeanManager();
      Bean<?> bean = manager.resolve(manager.getBeans(EntityManager.class));
      Object obj = manager.getReference(bean, EntityManager.class, manager.createCreationalContext(bean));

      return (EntityManager)obj;
    } catch (Exception e) {
      // Error while retrieving EntityManager.
      return null;
    }
  }

  protected UserTransaction getTransaction() {
    try {
      Context context = new InitialContext();
      Object obj = context.lookup(TRANSACTION_JNDI);
      if (obj instanceof UserTransaction) {
        return (UserTransaction)obj;
      }
    } catch (Exception e) {
      // Ignore and return null
    }
    return null;
  }

  private void joinEntityManagerTransaction() {
    EntityManager em = getEntityManager();
    if (em != null) {
      em.joinTransaction();
    }
  }

}

Interceptors have to be registered, to do this you have to edit the beans.xml and add the Interceptor. Please setup to the correct package of what you use. nl.turabdin.blog.cdi is just for this example.
The complete beans.xml

   
       nl.turabdin.blog.cdi.TransactionInterceptor
   

To use the interceptor annotate with @UseTransaction any method that have some logic that needs transactions.
For example:

@UseTransaction
public void doFancyDbStuff(Entity entity) {
  ...
}

This will only work in CDI beans, other places won’t work (and this is not visible because the method is just executed without interceptor)

Have fun!

Leave a Reply

Your email address will not be published. Required fields are marked *