【问题标题】:How to use Spring managed Hibernate interceptors in Spring Boot?如何在 Spring Boot 中使用 Spring 托管的 Hibernate 拦截器?
【发布时间】:2014-10-06 16:07:00
【问题描述】:

是否可以在 Spring Boot 中集成 Spring 管理的 Hibernate 拦截器 (http://docs.jboss.org/hibernate/orm/4.3/manual/en-US/html/ch14.html)?

我正在使用 Spring Data JPA 和 Spring Data REST,需要一个 Hibernate 拦截器来处理实体上特定字段的更新。

使用标准 JPA 事件无法获取旧值,因此我认为我需要使用 Hibernate 拦截器。

【问题讨论】:

标签: hibernate spring-data spring-data-jpa spring-boot spring-data-rest


【解决方案1】:

添加一个也是 Spring Bean 的 Hibernate 拦截器并不是特别简单的方法,但如果它完全由 Hibernate 管理,您可以轻松地添加一个拦截器。为此,请将以下内容添加到您的 application.properties

spring.jpa.properties.hibernate.ejb.interceptor=my.package.MyInterceptorClassName

如果您需要拦截器也成为一个 bean,您可以创建自己的 LocalContainerEntityManagerFactoryBean。 Spring Boot 1.1.4 中的 EntityManagerFactoryBuilder 对属性的泛型有一点限制,因此您需要强制转换为 (Map),我们将考虑在 1.2 中修复它。

@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
        EntityManagerFactoryBuilder factory, DataSource dataSource,
        JpaProperties properties) {
    Map<String, Object> jpaProperties = new HashMap<String, Object>();
    jpaProperties.putAll(properties.getHibernateProperties(dataSource));
    jpaProperties.put("hibernate.ejb.interceptor", hibernateInterceptor());
    return factory.dataSource(dataSource).packages("sample.data.jpa")
            .properties((Map) jpaProperties).build();
}

@Bean
public EmptyInterceptor hibernateInterceptor() {
    return new EmptyInterceptor() {
        @Override
        public boolean onLoad(Object entity, Serializable id, Object[] state,
                String[] propertyNames, Type[] types) {
            System.out.println("Loaded " + id);
            return false;
        }
    };
}

【讨论】:

  • 感谢 Phil,但由于它们不是 Spring 管理的,不幸的是,我无法以透明的方式调用其他注入的组件(如邮件发件人)
  • 再次感谢菲尔,我会尝试这种技术。顺便说一句,我可以跟踪 1.2 修复的问题吗?否则我可以自己提出问题。
  • 链接问题已在 1.2 中修复,请参阅 this commit
  • @PhilWebb 有更多 2016 年的方法吗?或者可能是注入的EntityListener
  • 使用“hibernate.session_factory.interceptor”而不是已弃用的“hibernate.ejb.interceptor”。
【解决方案2】:

使用 Spring Boot 2 的解决方案

@Component
public class MyInterceptorRegistration implements HibernatePropertiesCustomizer {

    @Autowired
    private MyInterceptor myInterceptor;

    @Override
    public void customize(Map<String, Object> hibernateProperties) {
        hibernateProperties.put("hibernate.session_factory.interceptor", myInterceptor);
    }
}
  • 我使用的是 Spring Boot 2.1.7.RELEASE。
  • 您可以使用hibernate.ejb.interceptor 代替hibernate.session_factory.interceptor。这两个属性都起作用可能是因为向后兼容的要求。

为什么选择 HibernatePropertiesCustomizer 而不是 application.properties

一个建议的答案是在 application.properties/yml 的 spring.jpa.properties.hibernate.ejb.interceptor 属性中指明您的拦截器。如果您的拦截器位于将由多个应用程序使用的库中,则此想法可能不起作用。你希望你的拦截器通过添加一个依赖项到你的库中来激活,而不需要每个应用程序改变它们的 application.properties

【讨论】:

  • hibernate.ejb.interceptor 还会在 Springboot 2 中引发 deprecated 警告
【解决方案3】:

以几个线程作为参考,我最终得到了以下解决方案:

我正在使用 Spring-Boot 1.2.3.RELEASE(这是目前的 ga)

我的用例是this bug (DATAREST-373) 中描述的。

我需要能够在创建时对User@Entity的密码进行编码,并在保存时具有特殊逻辑。使用@HandleBeforeCreate 并检查@Entity id 的0L 相等性非常简单。

为了保存,我实现了一个Hibernate Interceptor,它扩展了一个EmptyInterceptor

@Component
class UserInterceptor extends EmptyInterceptor{

    @Autowired
    PasswordEncoder passwordEncoder;

    @Override
    boolean onFlushDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState, String[] propertyNames, Type[] types) {

        if(!(entity instanceof User)){
            return false;
        }

        def passwordIndex = propertyNames.findIndexOf { it == "password"};

        if(entity.password == null && previousState[passwordIndex] !=null){

            currentState[passwordIndex] = previousState[passwordIndex];

        }else{
            currentState[passwordIndex] = passwordEncoder.encode(currentState[passwordIndex]);
        }

        return true;

    }
}

使用 spring boot 文档说明

在创建本地 EntityManagerFactory 时,spring.jpa.properties.* 中的所有属性都作为普通 JPA 属性(去除前缀)传递。

正如许多参考资料所述,我们可以在 Spring-Boot 配置中使用 spring.jpa.properties.hibernate.ejb.interceptor 定义我们的拦截器。但是我无法让@Autowire PasswordEncoder 工作。

所以我求助于使用HibernateJpaAutoConfiguration 并覆盖protected void customizeVendorProperties(Map&lt;String, Object&gt; vendorProperties)。这是我的配置。

@Configuration
public class HibernateConfiguration extends HibernateJpaAutoConfiguration{


    @Autowired
    Interceptor userInterceptor;


    @Override
    protected void customizeVendorProperties(Map<String, Object> vendorProperties) {
        vendorProperties.put("hibernate.ejb.interceptor",userInterceptor);
    }
}

自动装配 Interceptor 而不是让 Hibernate 实例化它是让它工作的关键。

现在困扰我的是逻辑一分为二,但希望一旦 DATAREST-373 得到解决,那么这将是不必要的。

【讨论】:

  • 扩展 HibernateJpaAutoConfiguration 以添加休眠属性在 Spring boot 2 中再次不起作用。
  • 这是我发现的最接近的答案。我做了与您完全相同的思考过程,但似乎customVendorProperties 在较新版本的Spring boot(> 2)中不再存在。说起来,@Lekkie,您是否找到了能够将 Spring 依赖注入到拦截器中的解决方案?
【解决方案4】:

我的一个简单的 spring boot 休眠监听器文件示例(spring-boot-starter 1.2.4.RELEASE)

import org.hibernate.event.service.spi.EventListenerRegistry;
import org.hibernate.event.spi.*;
import org.hibernate.internal.SessionFactoryImpl;
import org.hibernate.jpa.HibernateEntityManagerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;

import javax.annotation.PostConstruct;
import javax.inject.Inject;
import javax.persistence.EntityManagerFactory;

@Component
public class UiDateListener implements PostLoadEventListener, PreUpdateEventListener {
    @Inject EntityManagerFactory entityManagerFactory;

    @PostConstruct
    private void init() {
        HibernateEntityManagerFactory hibernateEntityManagerFactory = (HibernateEntityManagerFactory) this.entityManagerFactory;
        SessionFactoryImpl sessionFactoryImpl = (SessionFactoryImpl) hibernateEntityManagerFactory.getSessionFactory();
        EventListenerRegistry registry = sessionFactoryImpl.getServiceRegistry().getService(EventListenerRegistry.class);
        registry.appendListeners(EventType.POST_LOAD, this);
        registry.appendListeners(EventType.PRE_UPDATE, this);
    }

    @Override
    public void onPostLoad(PostLoadEvent event) {
        final Object entity = event.getEntity();
        if (entity == null) return;

        // some logic after entity loaded
    }

    @Override
    public boolean onPreUpdate(PreUpdateEvent event) {
        final Object entity = event.getEntity();
        if (entity == null) return false;

        // some logic before entity persist

        return false;
    }
}

【讨论】:

  • 这对我有用 - 除了 EntityManager.merge() 出于某种原因不会触发我的 onPostUpdate 或 onPreUpdate。
  • HibernateEntityManagerFactory 已弃用。
【解决方案5】:

我在 Spring 4.1.1、Hibernate 4.3.11 应用程序中遇到了类似的问题 - 而不是 Spring Boot。

我发现的解决方案(在阅读 Hibernate EntityManagerFactoryBuilderImpl 代码后)是,如果您将 bean 引用而不是类名传递给实体管理器定义的 hibernate.ejb.interceptor 属性,Hibernate 将使用已实例化的 bean。

所以在应用程序上下文中的 entityManager 定义中,我有这样的内容:

<bean id="auditInterceptor" class="com.something.AuditInterceptor" />

<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean" 
          ...> 
        <property name="jpaProperties"> 
            <map>
                ...
                <entry key="hibernate.ejb.interceptor">
                    <ref bean="auditInterceptor" />
                </entry>
                ...
            </map>
        </property> 
    </bean> 

auditInterceptor 由 Spring 管理,因此可以使用自动装配和其他 Spring 特性。

【讨论】:

  • 您是否有使用 application.properties 文件而不是 xml 的等价物?
【解决方案6】:

你好

阅读:https://github.com/spring-projects/spring-boot/commit/59d5ed58428d8cb6c6d9fb723d0e334fe3e7d9be(使用:HibernatePropertiesCustomizer 接口)

对于简单的拦截器:

为了在您的应用程序中配置它,您只需添加:spring.jpa.properties.hibernate.ejb.interceptor = path.to.interceptor(在 application.properties 中)。拦截器本身应该是@Component

只要拦截器实际上不使用任何 bean。否则会有点复杂,但我很乐意提供解决方案。

不要忘记添加 application-test.properties,一个 EmptyInterceptor 在测试中不使用日志系统(或任何你想使用它的东西)(这不会很有帮助)。

希望这对你有用。

最后一点:始终更新您的 Spring / Hibernate 版本(尽可能使用最新版本),您会发现大多数代码将变得多余,因为新版本会尽可能地减少配置可能。

【讨论】:

  • 您好,您有以下示例: - 只要拦截器实际上不使用任何 bean。否则它会有点复杂,但我很乐意提供解决方案。 -
【解决方案7】:

我遇到了同样的问题,最后创建了一个小型弹簧库来处理所有设置。

https://github.com/teastman/spring-data-hibernate-event

如果您使用的是 Spring Boot,则只需添加依赖项:

<dependency>
  <groupId>io.github.teastman</groupId>
  <artifactId>spring-data-hibernate-event</artifactId>
  <version>1.0.0</version>
</dependency>

然后在任意方法中添加注解@HibernateEventListener,第一个参数是你要监听的实体,第二个参数是你要监听的Hibernate事件。我还添加了静态 util 函数 getPropertyIndex 以更轻松地访问您要检查的特定属性,但您也可以只查看原始 Hibernate 事件。

@HibernateEventListener
public void onUpdate(MyEntity entity, PreUpdateEvent event) {
  int index = getPropertyIndex(event, "name");
  if (event.getOldState()[index] != event.getState()[index]) {
    // The name changed.
  }
}

【讨论】:

    【解决方案8】:

    在研究了两天关于如何将 Hibernate 拦截器与 Spring Data JPA 集成后,我发现了另一种方法,我的解决方案是 java 配置和 xml 配置之间的混合,但 this 帖子非常有用。所以我的最终解决方案是:

    AuditLogInterceptor 类:

    public class AuditLogInterceptor extends EmptyInterceptor{
    
        private int updates;
    
        //interceptor for updates
        public boolean onFlushDirty(Object entity,
                                Serializable id,
                                Object[] currentState,
                                Object[] previousState,
                                String[] propertyNames,
                                Type[] types) {
    
            if ( entity instanceof Auditable ) {
                updates++;
                for ( int i=0; i < propertyNames.length; i++ ) {
                    if ( "lastUpdateTimestamp".equals( propertyNames[i] ) ) {
                        currentState[i] = new Date();
                        return true;
                    }
                }
            }
            return false;
       }
    
    }
    

    数据源 Java 配置:

    @Bean
    DataSource dataSource() {
    
        //Use JDBC Datasource 
        DataSource dataSource = new DriverManagerDataSource();
    
            ((DriverManagerDataSource)dataSource).setDriverClassName(jdbcDriver);
            ((DriverManagerDataSource)dataSource).setUrl(jdbcUrl);
            ((DriverManagerDataSource)dataSource).setUsername(jdbcUsername);
            ((DriverManagerDataSource)dataSource).setPassword(jdbcPassword);                    
    
        return dataSource;
    }
    

    添加拦截器的实体和事务管理器

    <bean id="entityManagerFactory"
             class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"
             p:persistenceUnitName="InterceptorPersistentUnit" p:persistenceXmlLocation="classpath:audit/persistence.xml"
             p:dataSource-ref="dataSource" p:jpaVendorAdapter-ref="jpaAdapter">
             <property name="loadTimeWeaver">
                <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver"/>
             </property>              
    </bean>
    
    <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"
                     p:entityManagerFactory-ref="entityManagerFactory" />
    
    <bean id="jpaAdapter"
                     class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"
                     p:database="ORACLE" p:showSql="true" />
    

    持久化配置文件

         <persistence-unit name="InterceptorPersistentUnit">
    
                 <class>com.app.CLASSTOINTERCEPT</class>           
    
                 <shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode>
    
                 <properties>
                 <property name="hibernate.ejb.interceptor"
                          value="com.app.audit.AuditLogInterceptor" />
                 </properties>
         </persistence-unit>
    

    【讨论】:

      【解决方案9】:

      使用标准 JPA 事件无法获取旧值,因此我认为我需要使用 Hibernate 拦截器。

      不,可以在不使用拦截器且仅使用 JPA 的情况下获取旧值。

      假设您要审核的实体的基类是 Auditable&lt;T&gt;,因此,您可以在 Auditable&lt;T&gt; 实体中声明类型为 Auditable&lt;T&gt;@Transient 变量,您可以使用COPY(见下文)当实体将旧值加载到持久上下文中且在更新之前的旧值。

       /**
       * Extend this class if you want your entities to be audited.
       */
      @Getter
      @Setter
      @MappedSuperclass
      @EntityListeners(AuditListener.class)
      public abstract class Auditable implements Serializable {
      
          @JsonIgnore
          @Transient
          private Auditable oldState;
      }
      

      您可以在Auditable 基本实体中包含@PostLoad,或者我更喜欢在传递给@EntityListeners 的侦听器AuditListener 中包含它。

      public class AuditListener {
      
          /**
           * Triggered when an entity is loaded to the persistent.
           *
           * @param entity the one which is loaded
           */
          @PostLoad
          public void onPostLoad(final Auditable entity) {
              //Here, you have access to the entity before it gets updated and 
              //after it's loaded to the context, so now you can have a new copy 
              //and set it to that Transient variable so you make sure it not 
              //gets persisted by JPA.
              entity.setOldState(SerializationUtils.clone(entity));
          }
      
          /**
           * Triggered when an entity updated and before committed the 
           * transaction.
           *
           * @param entity the one which is updated
           */
          @PostUpdate
          public void onPostUpdate(final Auditable entity) {
              //Here, you have both copies the old and the new, thus you can 
              //track the changes and save or log them where ever you would like.
          }
      }
      

      【讨论】:

        【解决方案10】:

        由于拦截器没有注册为spring bean,所以可以使用一个util可以获取ApplicationContext实例,像这样:

        @Component
        public class SpringContextUtil implements ApplicationContextAware {
        
           private static ApplicationContext applicationContext;
        
           @Override
           public void setApplicationContext(ApplicationContext applicationContext) 
           throws BeansException {
              SpringContextUtil.applicationContext=applicationContext;
           }
        
           public static ApplicationContext getApplicationContext() {
              return applicationContext;
           }
        }
        

        然后就可以在拦截器中调用服务了,像这样:

        public class SimpleInterceptor extends EmptyInterceptor {
        
           @Override
           public String onPrepareStatement(String sql) {
               MyService myService=SpringContextUtil.getApplicationContext().getBean(MyService.class);
               myService.print();
            return super.onPrepareStatement(sql);
           }
         }
        

        【讨论】:

          猜你喜欢
          • 2019-07-07
          • 2018-08-09
          • 2017-02-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2021-03-27
          • 2017-11-28
          • 1970-01-01
          相关资源
          最近更新 更多