How to use Spring managed Hibernate interceptors i

2020-01-24 20:17发布

Is it possible to integrate Spring managed Hibernate interceptors (http://docs.jboss.org/hibernate/orm/4.3/manual/en-US/html/ch14.html) in Spring Boot?

I'm using Spring Data JPA and Spring Data REST and need an Hibernate interceptor to act on an update of a particular field on an entity.

With standard JPA events it's not possible to get the old values, and hence I think I need to use the Hibernate interceptor.

9条回答
聊天终结者
2楼-- · 2020-01-24 20:41

Solution using 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);
    }
}
  • I'm using Spring Boot 2.1.7.RELEASE.
  • Instead of hibernate.session_factory.interceptor you can use hibernate.ejb.interceptor. Both properties work probably because of a backwards compatibility requirement.

Why HibernatePropertiesCustomizer instead of application.properties

One suggested answer is to indicate your interceptor in the spring.jpa.properties.hibernate.ejb.interceptor property in application.properties/yml. This idea may not work if your interceptor is in a lib that will be used by several applications. You want your interceptor to be activated by just adding a dependency to your lib, without requiring each application to alter their application.properties.

查看更多
淡お忘
3楼-- · 2020-01-24 20:41

I ran into this same issue and wound up creating a small spring library to handle all of the setup.

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

If you're using Spring Boot, you just add the dependency:

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

Then add the annotation @HibernateEventListener to any method where the first parameter is the entity you want to listen to, and the second parameter is the Hibernate event that you want to listen for. I've also added the static util function getPropertyIndex to more easily get access to the specific property you want to check, but you can also just look at the raw Hibernate event.

@HibernateEventListener
public void onUpdate(MyEntity entity, PreUpdateEvent event) {
  int index = getPropertyIndex(event, "name");
  if (event.getOldState()[index] != event.getState()[index]) {
    // The name changed.
  }
}
查看更多
老娘就宠你
4楼-- · 2020-01-24 20:42

I had a similar problem with a Spring 4.1.1, Hibernate 4.3.11 application - not Spring Boot.

Solution I found (after reading Hibernate EntityManagerFactoryBuilderImpl code) was that if you pass in a bean reference instead of a class name to hibernate.ejb.interceptor property of the entity manager definition, Hibernate will use that already instantiated bean.

So in my entityManager definition in application context I had something like this:

<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> 

The auditInterceptor is managed by Spring, therefore autowiring and other Spring-natured behaviours will be available to it.

查看更多
Bombasti
5楼-- · 2020-01-24 20:44

Hello,

Give this a read: https://github.com/spring-projects/spring-boot/commit/59d5ed58428d8cb6c6d9fb723d0e334fe3e7d9be (use: HibernatePropertiesCustomizer interface)

OR

For simple Interceptor:

In order to configure this in your application you simply need to add: spring.jpa.properties.hibernate.ejb.interceptor = path.to.interceptor (in application.properties). The interceptor itself should be @Component.

As long as the interceptor doesn't actually use any beans. Otherwise it is a bit more complicated but I would be more than happy to offer the solution.

Don't forget to add in application-test.properties, an EmptyInterceptor to not use the logging system (or whatever you want to use it for) in tests (which wouldn't be very helpful).

Hope this was of use to you.

As a final note: always update your Spring / Hibernate versions (use the latest as possible) and you will see that most code will become redundant as newer versions try to reduce the configurations as much as possible.

查看更多
手持菜刀,她持情操
6楼-- · 2020-01-24 20:44

I found another approach after researching two days about how integrate Hibernate Interceptors with Spring Data JPA, my solution is a hybrid between java configuration and xml configuration but this post was very useful. So my final solution was:

AuditLogInterceptor class:

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;
   }

}

Datasource Java Configuration:

@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;
}

Entity and Transaction Managers adding the Interceptor

<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 configuration file

     <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>
查看更多
Viruses.
7楼-- · 2020-01-24 20:53

There's not a particularly easy way to add a Hibernate interceptor that is also a Spring Bean but you can easily add an interceptor if it's manged entirely by Hibernate. To do that add the following to your application.properties:

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

If you need the Interceptor to also be a bean you can create your own LocalContainerEntityManagerFactoryBean. The EntityManagerFactoryBuilder from Spring Boot 1.1.4 is a little too restrictive with the generic of the properties so you need cast to (Map), we'll look at fixing that for 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;
        }
    };
}
查看更多
登录 后发表回答