How to use Spring managed Hibernate interceptors in Spring Boot?

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.


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.


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

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