tutorial - ¿Cómo utilizar los interceptores Hibernate administrados por Spring en Spring Boot?
spring boot with hibernate (5)
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 this 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
<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" />
archivo de configuración de persistencia
<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>
¿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.
Mi sencillo archivo de un solo ejemplo de oyentes de hibernación para el arranque de primavera (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;
}
}
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 primavera, 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<String, Object> vendorProperties)
. Aquí está mi configuración
@Configuration
public class HibernateConfiguration extends HibernateJpaAutoConfiguration{
@Autowired
Interceptor userInterceptor;
@Override
protected void customizeVendorProperties(Map<String, Object> 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.
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:
<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>
El intérprete de auditoría es administrado por Spring, por lo tanto, el autoenlace y otros comportamientos de primavera estarán disponibles para él.
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<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;
}
};
}