56

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.

Marcel Overdijk
  • 11,041
  • 17
  • 71
  • 110

12 Answers12

62

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 managed 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;
        }
    };
}
Koen.
  • 25,449
  • 7
  • 83
  • 78
Phil Webb
  • 8,119
  • 1
  • 37
  • 37
  • Thanks Phil, but as they are not Spring managed I cannot in a transparant manner call other injected components (like a mail sender) unfortunately – Marcel Overdijk Aug 13 '14 at 22:12
  • Thanks again Phil, I will try this technique. Btw is there an issue I can track for the 1.2 fix? Otherwise I can raise an issue myself. – Marcel Overdijk Aug 14 '14 at 13:44
  • The generics issue can be tracked here: https://github.com/spring-projects/spring-boot/issues/1376 – Phil Webb Aug 14 '14 at 20:51
  • How is this as of 1.3? According to the linked issued it doesn't seem to have been changed. – user3748908 Feb 05 '16 at 17:15
  • 1
    The linked issue was fixed in 1.2, See [this commit](https://github.com/spring-projects/spring-boot/commit/b3ba86390fee1296c46f038967cf759daa323368) – Phil Webb Feb 08 '16 at 18:40
  • 9
    @PhilWebb Is there a more 2016 way of doing this? Or maybe an injected `EntityListener`? – Snekse Oct 18 '16 at 16:29
  • Note this only works for the session factory scoped interceptor (not session scoped) – Eduardo Nov 28 '17 at 12:07
  • 1
    Use "hibernate.session_factory.interceptor" instead of the deprecated "hibernate.ejb.interceptor". – Datz May 18 '20 at 06:43
49

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.

Paulo Merson
  • 13,270
  • 8
  • 79
  • 72
23

Taking the several threads as reference I ended up with the following solution:

I am using Spring-Boot 1.2.3.RELEASE (which is the current ga at the moment)

My use case was that described in this bug (DATAREST-373).

I needed to be able to encode the password of a User @Entity upon create, and have special logic upon save. The create was very straightforward using @HandleBeforeCreate and checking the @Entity id for 0L equality.

For the save I implemented a Hibernate Interceptor which extends an 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;

    }
}

Using spring boot the documentation states that

all properties in spring.jpa.properties.* are passed through as normal JPA properties (with the prefix stripped) when the local EntityManagerFactory is created.

As many references stated, we can defined our interceptor using spring.jpa.properties.hibernate.ejb.interceptor in our Spring-Boot configuration. However I couldn't get the @Autowire PasswordEncoder to work.

So I resorted to using HibernateJpaAutoConfiguration and overriding protected void customizeVendorProperties(Map<String, Object> vendorProperties). Here is my configuration.

@Configuration
public class HibernateConfiguration extends HibernateJpaAutoConfiguration{


    @Autowired
    Interceptor userInterceptor;


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

Autowiring the Interceptor instead of allowing Hibernate to instantiate it was the key to getting it to work.

What bothers me now is that the logic is split in two, but hopefully once DATAREST-373 is resolved then this wont be necessary.

buræquete
  • 14,226
  • 4
  • 44
  • 89
Fotis Paraskevopoulos
  • 1,001
  • 2
  • 12
  • 19
  • 1
    extending HibernateJpaAutoConfiguration to add hibernate properties doesnt work in Spring boot 2 again. – Lekkie Aug 07 '18 at 13:59
  • this is the closest answer I've found to work. I did exactly the same thoughts process as you, but it seems that customizeVendorProperties does not exist anymore in newer versions of Spring boot ( > 2). Speaking by, @Lekkie, did you found a solution to be able to use Spring dependency injection into the Interceptor ? – Alex Dec 19 '18 at 12:11
10

My simple one file example of hibernate listeners for 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;
    }
}
Enginer
  • 3,048
  • 1
  • 26
  • 22
5

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.

BhathiyaW
  • 356
  • 3
  • 11
5

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.

Rareș Flueraș
  • 324
  • 5
  • 4
  • Hi, do you have an example of: - 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. - – Martin Irigaray Apr 09 '20 at 02:13
4

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.
  }
}
Tyler Eastman
  • 366
  • 2
  • 5
2

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>
kristianva
  • 65
  • 2
2

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

No, it's possible to get the old values without using interceptors and only by using JPA.

Let's say that the base class of your entities you would like to audit is Auditable<T>, Thus, you can declare a @Transient variable of Type Auditable<T> inside your Auditable<T> entity that you can populate it with a COPY (see how a little below) of the old values when the entity gets loaded with the old values to the persistent context and before it gets updated.

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

You can have @PostLoad inside the Auditable base entity or I prefer to have it inside the listener AuditListener that is passed to @EntityListeners.

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.
    }
}
1

in Spring Boot ,you can easily register your Hibernate Custom Interceptor .
implement HibernatePropertiesCustomizer interface and override customize method to add your custum inteceptor to hibernateProperties :

 @Component
 public class MyCustomInterceptor extends EmptyInterceptor implements HibernatePropertiesCustomizer {

    @Override
    public void customize(Map<String, Object> hibernateProperties) {
        hibernateProperties.put("hibernate.session_factory.interceptor", this);
    }

    @Override
    public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, org.hibernate.type.Type[] types) {
        
        System.out.println("onSave");

        return super.onSave(entity, id, state, propertyNames, types);
    }
}
0

Because the interceptor do not register as a spring bean,you can use a util which can get ApplicationContext instance,like this:

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

Then you can call the service in the interceptor,like this:

public class SimpleInterceptor extends EmptyInterceptor {

   @Override
   public String onPrepareStatement(String sql) {
       MyService myService=SpringContextUtil.getApplicationContext().getBean(MyService.class);
       myService.print();
    return super.onPrepareStatement(sql);
   }
 }
Null
  • 41
  • 2
0

Refining Paulo Merson's answer, another option is to register your HibernatePropertiesCustomizer (a FunctionalInterface) within an @Configuration class using a lambda expression:

@Bean
public HibernatePropertiesCustomizer hibernatePropertiesCustomizer(MyInterceptor interceptor) {
  return props -> props.put("hibernate.session_factory.interceptor", interceptor);
} 
Brice Roncace
  • 10,110
  • 9
  • 60
  • 69