Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I intercept JTA transactions events and get a reference to the current EntityManager associated with the transaction

Long story short: We develop and maintain a library that can be used in other projects using JavaEE7/CDI/JPA. Applications will run under Glassfish-4.0 and use Hibernate's JPA implementation for an underlying PostgreSQL persistence. This is part of a long term migration effort to rewrite old applications that were written in Spring/Struts/Hibernate into the new world of JavaEE7/CDI/JTA.

The problem: For audit purposes, our library needs to intercept all database transactions and include custom SQL statements before the user statements are executed. At this point, the current username and IP address need to be inserted into a temporary database variable (vendor specific feature) so that a database trigger can read them to create the audit trail for any row modification. This particular post was very helpful providing alternatives, and our team went down the trigger road due to a previously established legacy.

HOWEVER: We are deeply disappointed at how JTA handles transaction events. There are numerous ways to intercept transactions, but this particular case seems to be down right impossible. In the old architecture, using Spring's transaction manager, we simply used a Hibernate Interceptor implementing Interceptor.afterTransactionBegin(...). Reading up on the official JTA-1.2 spec, we found that it does have support for Synchronization.beforeCompletion and Synchronization.afterCompletion. After several hours of debugging sessions we clearly noted that Hibernate's implementation of JTA is using these facilities. But JTA seems to be lacking events like beforeBegin and afterBegin (which IMHO seems to be a lack of common sense). And since there are no facilities to intercept those, Hibernate complies fully with JTA and it simply won't. Period.

No matter what we do, we can't find a way. We tried, for instance, to intercept @Transactional annotations and run our code just after the container's JTA impl does its job to open the transaction. But we lack the ability to dynamically acquire the EntityManager associated with that particular transaction. Remember: this is a library, not the web application itself. It cannot make any assumptions about which Persistence Units are declared and used by the application. And, as far as we can tell, we need to know which specific Persistent Unit name to inject it into our code. We are trying to provide an audit facility to other temas that is as transparent as possible.

So we humbly ask for help. If anyone out there has a solution, workaround, whatever opinion, we'll be glad to hear it.

like image 254
JulioHM Avatar asked Aug 28 '14 15:08

JulioHM


People also ask

What is JTA transaction manager?

The JTA specifies standard Java interfaces between a transaction manager and the parties involved in a distributed transaction system: the application, the application server, and the resource manager that controls access to the shared resources affected by the transactions.

Which method of EntityManager would begin the transaction perform your operations and then either commit or rollback your transaction in JPA?

UserTransaction interface defines methods to begin, commit, and roll back transactions. Inject an instance of UserTransaction by creating an instance variable annotated with @Resource: @Resource UserTransaction utx; To begin a transaction, call the UserTransaction.

What is the difference between JTA and JPA?

JPA (Java Persistence API) is the Java ORM standard/specification for storing, accessing, and managing Java objects in a relational database. Hibernate is an implementation of the Java Persistence API (JPA) specification. JTA (Java Transaction API) is the Java standard/specification for distributed transactions.

What is difference between EntityManagerFactory and EntityManager?

EntityManagerFactory vs EntityManagerWhile EntityManagerFactory instances are thread-safe, EntityManager instances are not. The injected JPA EntityManager behave just like an EntityManager fetched from an application server's JNDI environment, as defined by the JPA specification.


1 Answers

This was quickly answered here in this post by myself, but hiding the fact that we spent over two weeks trying different strategies to overcome this. So, here goes our final implementation we decided to use.

Basic idea: Create your own implementation of javax.persistence.spi.PersistenceProvider by extending the one given by Hibernate. For all effects, this is the only point where your code will be tied to Hibernate or any other vendor specific implementation.

public class MyHibernatePersistenceProvider extends org.hibernate.jpa.HibernatePersistenceProvider {

    @Override
    public EntityManagerFactory createContainerEntityManagerFactory(PersistenceUnitInfo info, Map properties) {
        return new EntityManagerFactoryWrapper(super.createContainerEntityManagerFactory(info, properties));
    }

}

The idea is to wrap hibernate's versions of EntityManagerFactory and EntityManager with your own implementation. So you need to create classes that implement these interfaces and keep the vendor specific implementation inside.

This is the EntityManagerFactoryWrapper

public class EntityManagerFactoryWrapper implements EntityManagerFactory {

    private EntityManagerFactory emf;

    public EntityManagerFactoryWrapper(EntityManagerFactory originalEMF) {
        emf = originalEMF;
    }

    public EntityManager createEntityManager() {
        return new EntityManagerWrapper(emf.createEntityManager());
    }

    // Implement all other methods for the interface
    // providing a callback to the original emf.

The EntityManagerWrapper is our interception point. You will need to implement all methods from the interface. At every method where an entity can be modified, we include a call to a custom query to set local variables at the database.

public class EntityManagerWrapper implements EntityManager {

    private EntityManager em;
    private Principal principal;

    public EntityManagerWrapper(EntityManager originalEM) {
        em = originalEM;
    }

    public void setAuditVariables() {
        String userid = getUserId();
        String ipaddr = getUserAddr();
        String sql = "SET LOCAL application.userid='"+userid+"'; SET LOCAL application.ipaddr='"+ipaddr+"'";
        em.createNativeQuery(sql).executeUpdate();
    }

    protected String getUserAddr() {
        HttpServletRequest httprequest = CDIBeanUtils.getBean(HttpServletRequest.class);
        String ipaddr = "";
        if ( httprequest != null ) {
            ipaddr = httprequest.getRemoteAddr();
        }
        return ipaddr;
    }

    protected String getUserId() {
        String userid = "";
        // Try to look up a contextual reference
        if ( principal == null ) {
            principal = CDIBeanUtils.getBean(Principal.class);
        }

        // Try to assert it from CAS authentication
        if (principal == null || "anonymous".equalsIgnoreCase(principal.getName())) {
            if (AssertionHolder.getAssertion() != null) {
                principal = AssertionHolder.getAssertion().getPrincipal();
            }
        }
        if ( principal != null ) {
            userid = principal.getName();
        }
        return userid;
    }

    @Override
    public void persist(Object entity) {
        if ( em.isJoinedToTransaction() ) {
            setAuditVariables();
        }
        em.persist(entity);
    }

    @Override
    public <T> T merge(T entity) {
        if ( em.isJoinedToTransaction() ) {
            setAuditVariables();
        }
        return em.merge(entity);
    }

    @Override
    public void remove(Object entity) {
        if ( em.isJoinedToTransaction() ) {
            setAuditVariables();
        }
        em.remove(entity);
    }

    // Keep implementing all methods that can change
    // entities so you can setAuditVariables() before
    // the changes are applied.
    @Override
    public void createNamedQuery(.....

Downside: Interception queries (SET LOCAL) will likely run several times inside a single transaction, specially if there are several statements made on a single service call. Given the circumstances, we decided to keep it this way due to the fact that it's a simple SET LOCAL in memory call to PostgreSQL. Since there are no tables involved, we can live with the performance hit.

Now just replace Hibernate's persistence provider inside persistence.xml:

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
             version="2.1">
<persistence-unit name="petstore" transaction-type="JTA">
        <provider>my.package.HibernatePersistenceProvider</provider>
        <jta-data-source>java:app/jdbc/exemplo</jta-data-source>
        <properties>
            <property name="hibernate.transaction.jta.platform" value="org.hibernate.service.jta.platform.internal.SunOneJtaPlatform" />
            <property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect"/>
        </properties>
</persistence-unit>

As a side note, this is the CDIBeanUtils we have to help with the bean manager on some special occasions. In this case, we are using it to look up a reference to HttpServletRequest and Principal.

public class CDIBeanUtils {

    public static <T> T getBean(Class<T> beanClass) {

        BeanManager bm = CDI.current().getBeanManager();

        Iterator<Bean<?>> ite = bm.getBeans(beanClass).iterator();
        if (!ite.hasNext()) {
            return null;
        }
        final Bean<T> bean = (Bean<T>) ite.next();
        final CreationalContext<T> ctx = bm.createCreationalContext(bean);
        final T t = (T) bm.getReference(bean, beanClass, ctx);
        return t;
    }

}

To be fair, this is not exactly intercepting Transactions events. But we are able to include the custom queries we need inside the transaction.

Hopefully this can help others avoid the pain we went through.

like image 190
JulioHM Avatar answered Sep 29 '22 08:09

JulioHM