我如何拦截JTA事务事件并获取对与事务关联的当前EntityManager的引用 [英] How can I intercept JTA transactions events and get a reference to the current EntityManager associated with the transaction

查看:157
本文介绍了我如何拦截JTA事务事件并获取对与事务关联的当前EntityManager的引用的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

长话短说:我们开发和维护一个库,可以使用JavaEE7 / CDI / JPA在其他项目中使用。应用程序将在Glassfish-4.0下运行,并使用Hibernate的JPA实现来实现底层PostgreSQL持久性。这是一个长期的迁移工作的一部分,用于将旧版应用程序写入JavaEE7 / CDI / JTA的新世界。
$ b

问题:为了审计目的,我们的库需要拦截所有数据库事务并在用户语句执行之前包含自定义SQL语句。此时,需要将当前用户名和IP地址插入临时数据库变量(供应商特定功能),以便数据库触发器可以读取它们以创建任何行修改的审计跟踪。 这篇特别的文章对提供替代方案非常有帮助,我们的团队触发了触发器因为以前建立的传统。



然而,我们对JTA如何处理交易事件感到非常失望。拦截交易的方式有很多种,但这种特殊情况似乎是不可能的。在旧的架构中,使用Spring的事务管理器,我们简单地使用了一个 Hibernate Interceptor 实现 Interceptor.afterTransactionBegin(...)。阅读官方JTA-1.2规范,我们发现它支持 Synchronization.beforeCompletion Synchronization.afterCompletion 。经过几个小时的调试会话后,我们清楚地注意到Hibernate的JTA实现正在使用这些工具。但JTA似乎缺少 beforeBegin afterBegin (恕我直言,这似乎缺乏常识)等事件。而且由于没有设施可以拦截这些,所以Hibernate完全符合JTA,它根本不符合。期间。



无论我们做什么,我们都找不到方法。例如,我们尝试拦截 @Transactional 注释,并在容器的JTA impl完成其工作以打开事务后运行我们的代码。但是我们缺乏动态获取与特定事务关联的EntityManager的能力。请记住:这是一个库,而不是Web应用程序本身。它不能对应用程序声明和使用哪个持久性单元做任何假设。而且,据我们所知,我们需要知道将哪个特定的持久单元名称注入到我们的代码中。我们正在努力为其他技术提供一个审计工具,使其尽可能透明。



所以我们虚心寻求帮助。如果任何人有解决方案,解决方法,无论什么意见,我们很乐意听到它。

解决方案

我自己在这篇文章中回答了这个问题,但是隐瞒了我们花了两周时间尝试不同策略来克服这个问题的事实。所以,我们决定使用我们的最终实现。



基本思想:创建您自己的 javax.persistence.spi .PersistenceProvider 通过扩展Hibernate给出的。对于所有特效,这是您的代码与Hibernate或任何其他供应商特定实现绑定的唯一一点。

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

@Override
public EntityManagerFactory createContainerEntityManagerFactory(PersistenceUnitInfo info,Map属性){
返回新的EntityManagerFactoryWrapper(super.createContainerEntityManagerFactory(info,properties, ));
}

}

这个想法是将hibernate的版本 EntityManagerFactory EntityManager 与您自己的实现。所以你需要创建实现这些接口的类并保持供应商特定的实现。



这是EntityManagerFactoryWrapper

  public class EntityManagerFactoryWrapper implements EntityManagerFactory {

private EntityManagerFactory emf;

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

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

//为接口
//实现所有其他方法//为原始emf提供回调。

EntityManagerWrapper是我们的拦截点。您将需要从界面实施所有方法。在每个可以修改实体的方法中,我们包含对自定义查询的调用,以便在数据库中设置局部变量。

 公共类EntityManagerWrapper实现EntityManager {

私有EntityManager em;
私人校长校长;

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 =;
//尝试查找上下文引用
if(principal == null){
principal = CDIBeanUtils.getBean(Principal.class);
}

//尝试从CAS认证中断言它
if(principal == null ||anonymous.equalsIgnoreCase(principal.getName())){$ b ()AssertionHolder.getAssertion()。getPrincipal();


if(principal!= null){
userid = principal.getName();
}
返回用户ID;


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

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


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

//继续执行所有可以更改
//实体的方法,以便在
之前设置setAuditVariables()//应用更改。
@Override
public void createNamedQuery(.....

下行:拦截查询(SET LOCAL)可能会在单个事务中多次运行,特别是如果在单个服务调用中发出多个语句。事实上它是一个简单的SET LOCAL,用于调用PostgreSQL的内存,由于没有涉及表,所以我们可以忍受性能问题。



现在只需要将Hibernate的持久性提供程序替换 persistence.xml

 <?xml version =1.0encoding =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 =petstoretransaction-type =JTA>
< provider> my.package.HibernatePersistenceProvider< / provider>
< jta-data-source> java:app / jdbc / exemplo< / jta-data-source>
<属性>
< property name =hibernate.transaction.jta.platformvalue =org.hibernate.service.jta.platform.internal.SunOneJtaPlatform/>
< property name =hibernate.dialectvalue =org.hibernate.dialect.PostgreSQLDialect/>
< / properties>
< / persistence-unit>

请注意,这是CDIBeanUtils,我们必须在一些特殊场合帮助bean管理器。在这种情况下,我们使用它来查找对HttpServletRequest和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;
}

}

公平地说,这不是准确截取Transactions事件。但我们能够在交易中包含我们需要的自定义查询。



希望这可以帮助其他人避免我们经历的痛苦。


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.

解决方案

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.

这篇关于我如何拦截JTA事务事件并获取对与事务关联的当前EntityManager的引用的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆