How to use Spring managed Hibernate interceptors in Spring Boot?
Solution 1
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;
}
};
}
Solution 2
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 usehibernate.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.
Solution 3
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.
Solution 4
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;
}
}
Solution 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.
Related videos on Youtube
Marcel Overdijk
Updated on October 19, 2021Comments
-
Marcel Overdijk over 2 years
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.
-
Ivan Aracki over 6 yearsthere is a way to do this using SpringAOP stackoverflow.com/a/46041239/2884309
-
-
Marcel Overdijk almost 10 yearsThanks 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 almost 10 yearsThanks 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.
-
Phil Webb almost 10 yearsThe generics issue can be tracked here: github.com/spring-projects/spring-boot/issues/1376
-
user3748908 over 8 yearsHow is this as of 1.3? According to the linked issued it doesn't seem to have been changed.
-
Phil Webb over 8 yearsThe linked issue was fixed in 1.2, See this commit
-
Glide about 8 yearsThis worked for me - except
EntityManager.merge()
doesn't trigger my onPostUpdate or onPreUpdate for some reason. -
Snekse over 7 years@PhilWebb Is there a more 2016 way of doing this? Or maybe an injected
EntityListener
? -
Eduardo over 6 yearsDo you have an equivelent for this using an application.properties file rather than xml?
-
Eduardo over 6 yearsNote this only works for the session factory scoped interceptor (not session scoped)
-
Lekkie almost 6 yearsextending HibernateJpaAutoConfiguration to add hibernate properties doesnt work in Spring boot 2 again.
-
humbaba almost 6 yearsHibernateEntityManagerFactory is deprecated.
-
Alex over 5 yearsthis 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 ?
-
ortonomy about 4 years
hibernate.ejb.interceptor
also throws adeprecated
warning in Springboot 2 -
Martin Irigaray about 4 yearsHi, 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. -
-
Datz about 4 yearsUse "hibernate.session_factory.interceptor" instead of the deprecated "hibernate.ejb.interceptor".