¿Cómo utilizar los interceptores Hibernate administrados por Spring en Spring Boot?

¿Es posible integrar interceptores Hibernate administrados por Spring ( http://docs.jboss.org/hibernate/orm/4.3/manual/en-US/html/ch14.html ) en Spring Boot?

Estoy usando Spring Data JPA y Spring Data REST y necesito un interceptor de Hibernate para actuar en una actualización de un campo particular en una entidad.

Con eventos estándar JPA no es posible obtener los valores anteriores, y por lo tanto creo que necesito usar el interceptor Hibernate.

No hay una forma particularmente fácil de agregar un interceptor Hibernate que también sea un Spring Bean, pero puedes agregar fácilmente un interceptor si Hibernate lo maneja completamente. Para hacerlo, agregue lo siguiente a su application.properties :

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

Si necesita que el Interceptor también sea un frijol, puede crear su propio LocalContainerEntityManagerFactoryBean . El EntityManagerFactoryBuilder de Spring Boot 1.1.4 es un poco demasiado restrictivo con el genérico de las propiedades, por lo que necesita lanzar a (Map) , veremos cómo arreglarlo para 1.2.

 @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory( EntityManagerFactoryBuilder factory, DataSource dataSource, JpaProperties properties) { Map jpaProperties = new HashMap(); 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; } }; } 

Tomando varios hilos como referencia, terminé con la siguiente solución:

Estoy usando Spring-Boot 1.2.3.RELEASE (que es el GA actual en este momento)

Mi caso de uso fue el descrito en este error (DATAREST-373) .

Necesitaba poder codificar la contraseña de un @Entity User al crearla , y tenía una lógica especial al guardarla . La creación fue muy simple utilizando @HandleBeforeCreate y comprobando la id @Entity para la igualdad @Entity .

Para el guardado implementé un Interceptor Hibernate que extiende un 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; } } 

Usando el arranque de spring, la documentación indica que

todas las propiedades en spring.jpa.properties. * se pasan como propiedades JPA normales (con el prefijo eliminado) cuando se crea EntityManagerFactory local.

Como se mencionó en muchas referencias, podemos definir nuestro interceptor usando spring.jpa.properties.hibernate.ejb.interceptor en nuestra configuración Spring-Boot. Sin embargo, no pude conseguir que @Autowire PasswordEncoder funcionara.

Así que recurrí al uso de HibernateJpaAutoConfiguration y anulando protected void customizeVendorProperties(Map vendorProperties) . Aquí está mi configuración

 @Configuration public class HibernateConfiguration extends HibernateJpaAutoConfiguration{ @Autowired Interceptor userInterceptor; @Override protected void customizeVendorProperties(Map vendorProperties) { vendorProperties.put("hibernate.ejb.interceptor",userInterceptor); } } 

El autocableo del Interceptor lugar de permitir que Hibernate lo instanciara fue la clave para que funcione.

Lo que me molesta ahora es que la lógica se divide en dos, pero con suerte una vez que se resuelva DATAREST-373, no será necesario.

Mi sencillo archivo de un solo ejemplo de oyentes de hibernación para el arranque de spring (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; } } 

Encontré otro enfoque después de investigar dos días sobre cómo integrar interceptores Hibernate con Spring Data JPA, mi solución es un híbrido entre la configuración de Java y la configuración xml pero esta publicación fue muy útil. Entonces mi solución final fue:

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

Configuración de Datasource 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; } 

Administradores de entidades y transacciones que agregan el interceptor

        

archivo de configuración de persistencia

   com.app.CLASSTOINTERCEPT ENABLE_SELECTIVE     

Tuve un problema similar con una aplicación Spring 4.1.1, Hibernate 4.3.11, no con Spring Boot.

La solución que encontré (después de leer el código Hibernate EntityManagerFactoryBuilderImpl) fue que si pasa una referencia de bean en lugar de un nombre de clase a la propiedad hibernate.ejb.interceptor de la definición del administrador de entidades, Hibernate usará ese bean ya instanciado.

Entonces, en mi definición de entityManager en el contexto de la aplicación, tuve algo como esto:

     ...    ...    

El intérprete de auditoría es administrado por Spring, por lo tanto, el autoenlace y otros comportamientos de spring estarán disponibles para él.