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

In 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 the Hibernate JPA implementation for the PostgreSQL base strength. This is part of a long-term migration to rewrite old applications written in Spring / Struts / Hibernate into the new world of JavaEE7 / CDI / JTA.

Problem: For auditing purposes, our library needs to intercept all database transactions and include custom SQL statements before executing custom statements. At this point, the current username and IP address must be inserted into the temporary database variable (a special function of the provider) so that the database trigger can read them in order to create an audit trail for any row modification. This particular post was very helpful, suggesting alternatives , and our team went down the trigger road due to a previously established heritage.

HOWEVER: We are deeply disappointed in how the JTA handles transactional events. There are many ways to intercept transactions, but this particular case seems impossible. In the old architecture, using the Spring transaction manager, we simply used the Hibernate Interceptor , which implements Interceptor.afterTransactionBegin (...) . After reading the official JTA-1.2 specification , we found that it supports Synchronization.beforeCompletion and Synchronization.afterCompletion . After several hours of debugging sessions, we clearly noted that using JTA in Hibernate takes advantage of these features. But the JTA seems to be missing events like beforeBegin and afterBegin (which IMHO apparently lacks common sense). And since there is no way to intercept them, Hibernate is fully compliant with the JTA, and it just won't. Period.

No matter what we do, we cannot find a way. We tried, for example, to intercept the @Transactional annotations and run our code immediately after the JTA impl container does its job to open the transaction. But we lack the ability to dynamically acquire the EntityManager associated with this particular transaction. Remember: this is a library, not a web application. He cannot make any assumptions about which storage units are stored and used by the expression. And, as far as we can tell, we need to know which concrete name of the resistant unit to enter in our code. We are trying to provide audit services to other topics that are as transparent as possible.

So, we humbly ask for help. If someone has a solution, a workaround, regardless of opinion, we will be happy to hear it.

+5
source share
1 answer

It was quickly responsible here in this post, but hiding the fact that we spent more than two weeks trying to develop strategies to overcome this. So, here is our final implementation, which we decided to use.

Main idea: Create your own implementation of javax.persistence.spi.PersistenceProvider , expanding the value specified by Hibernate. For all effects, this is the only time your code will be tied to Hibernate or any other specific vendor 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 link the EntityManagerFactory and EntityManager hibernation versions to your own implementation. Therefore, you need to create classes that implement these interfaces and save the implementation of a specific provider inside.

This is 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. 

EntityManagerWrapper is a capture point. You will need to implement all the methods from the interface. For each method in which an object can be modified, we include a user request call to set local variables in 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 requests (SET LOCAL) will most likely be executed several times within the same transaction, especially if there are several statements for a single service call. Given the circumstances, we decided to store it this way because it is a simple SET LOCAL in a PostgreSQL memory call. Since there are no tables involved, we can live with a performance hit.

Now just replace the Hibernate 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 CDIBeanUtils, we need help with the bean manager in some special cases. In this case, we use it to search for links 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 does not completely intercept transaction events. But we can include custom queries that we need inside the transaction.

Hope this helps others avoid the pain we have been through.

+3
source

Source: https://habr.com/ru/post/1201344/


All Articles