如何在Spring Boot中使用Springpipe理的Hibernate拦截器?
在Spring Boot中是否可以集成Springpipe理的Hibernate拦截器( http://docs.jboss.org/hibernate/orm/4.3/manual/en-US/html/ch14.html )?
我正在使用Spring Data JPA和Spring Data REST,并且需要Hibernate拦截器来处理实体上特定字段的更新。
使用标准的JPA事件不可能获得旧的值,因此我认为我需要使用Hibernate拦截器。
添加一个也是Spring Bean的Hibernate拦截器并不是一个特别简单的方法,但是如果它完全由Hibernate来pipe理的话,你可以很容易地添加一个拦截器。 为此,将以下内容添加到您的application.properties
:
spring.jpa.properties.hibernate.ejb.interceptor=MyInterceptorClassName
如果你需要拦截器也是一个bean,你可以创build你自己的LocalContainerEntityManagerFactoryBean
。 Spring Boot 1.1.4中的EntityManagerFactoryBuilder
对属性的generics有一点限制,所以你需要将其转换为(Map)
,我们将在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; } }; }
以几个线程作为参考,我结束了以下解决scheme:
我正在使用Spring-Boot 1.2.3.RELEASE(这是当前的ga)
我的用例是在这个bug(DATAREST-373)中描述的 。
我需要能够在创build时对User
@Entity
的密码进行编码,并在保存时具有特殊的逻辑。 创build是非常简单的使用@HandleBeforeCreate
和检查@ @Entity
ID为0L
相等。
为了保存,我实现了一个扩展了EmptyInterceptor的Hibernate Interceptor
@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; } }
使用spring启动文档说明
在创build本地EntityManagerFactory时,spring.jpa.properties。*中的所有属性都作为正常的JPA属性(剥离了前缀)传递。
正如许多参考文献所述,我们可以在Spring-Bootconfiguration中使用spring.jpa.properties.hibernate.ejb.interceptor
来定义我们的拦截器。 但是我无法使@Autowire PasswordEncoder
工作。
所以我采取了使用HibernateJpaAutoConfiguration和覆盖protected void customizeVendorProperties(Map<String, Object> vendorProperties)
。 这是我的configuration。
@Configuration public class HibernateConfiguration extends HibernateJpaAutoConfiguration{ @Autowired Interceptor userInterceptor; @Override protected void customizeVendorProperties(Map<String, Object> vendorProperties) { vendorProperties.put("hibernate.ejb.interceptor",userInterceptor); } }
自动assemblyInterceptor
而不是让Hibernate实例化它是实现它的关键。
现在困扰我的是逻辑被分成两部分,但是希望一旦DATAREST-373解决了,那么这将不是必须的。
我简单的一个文件的春季启动(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; } }
经过两天的研究,我发现了另一种方法,即如何将Hibernate Interceptor与Spring Data JPA集成,我的解决scheme是javaconfiguration和xmlconfiguration之间的混合,但是这个post非常有用。 所以我最终的解决scheme是:
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; } }
数据源Javaconfiguration:
@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; }
实体和事务pipe理器添加拦截器
<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" />
持久性configuration文件
<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>
我有一个类似的问题,春季4.1.1,hibernate4.3.11应用程序 – 而不是春季启动。
解决scheme我发现(在阅读Hibernate EntityManagerFactoryBuilderImpl代码之后)是,如果你将一个bean引用而不是一个类名传递给实体pipe理器定义的hibernate.ejb.interceptor
属性,那么Hibernate将使用那个已经实例化的bean。
所以在我的应用程序上下文中的我的entityManager定义,我有这样的东西:
<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>
auditInterceptor由Springpipe理,因此可以使用自动assembly和其他Spring自然行为。