[英]Spring Boot & Spring Data: how are Hibernate Sessions managed?
[英]How to use Spring managed Hibernate interceptors in Spring Boot?
是否可以在 88183218512 中集成 Spring 托管 Hibernate 攔截器( http://docs.jboss.org/hibernate/orm/4.3/manual/en-US/html/ch14.html )?
我正在使用 Spring 數據 JPA 和 Spring 數據 REST 並且需要一個 Hibernate 攔截器來處理實體上特定字段的更新。
對於標准的 JPA 事件,不可能獲得舊值,因此我認為我需要使用 Hibernate 攔截器。
添加也是 Spring Bean 的 Hibernate 攔截器並沒有特別簡單的方法,但如果它完全由 Hibernate 管理,您可以輕松添加攔截器。 為此,將以下內容添加到您的application.properties
:
spring.jpa.properties.hibernate.ejb.interceptor=my.package.MyInterceptorClassName
如果您需要 Interceptor 也是一個 bean,您可以創建自己的LocalContainerEntityManagerFactoryBean
。 Spring Boot 1.1.4 中的EntityManagerFactoryBuilder
對屬性的泛型有點過於嚴格,因此您需要強制轉換為(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;
}
};
}
@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);
}
}
hibernate.session_factory.interceptor
可以使用hibernate.ejb.interceptor
。 由於向后兼容性要求,這兩個屬性都可能起作用。 一個建議的答案是在 application.properties/yml 的spring.jpa.properties.hibernate.ejb.interceptor
屬性中指明您的攔截器。 如果您的攔截器位於多個應用程序將使用的庫中,則此想法可能不起作用。 你希望你的攔截器只通過向你的庫添加一個依賴項來激活,而不需要每個應用程序改變它們的application.properties 。
以幾個線程為參考,我最終得到了以下解決方案:
我正在使用 Spring-Boot 1.2.3.RELEASE(這是目前的 ga)
我的用例是在這個 bug (DATAREST-373) 中描述的。
我需要能夠在創建時對User
@Entity
的密碼進行編碼,並在保存時具有特殊的邏輯。 使用@HandleBeforeCreate
並檢查@Entity
id 是否為0L
相等,創建非常簡單。
為了保存,我實現了一個Hibernate Interceptor ,它擴展了一個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;
}
}
使用 spring boot 文檔指出
當創建本地 EntityManagerFactory 時, spring.jpa.properties.* 中的所有屬性都作為普通的 JPA 屬性(去掉前綴)傳遞。
正如許多參考資料所述,我們可以在 Spring-Boot 配置中使用spring.jpa.properties.hibernate.ejb.interceptor
定義我們的攔截器。 但是我無法讓@Autowire PasswordEncoder
工作。
所以我求助於使用HibernateJpaAutoConfiguration並覆蓋protected void customizeVendorProperties(Map<String, Object> vendorProperties)
。 這是我的配置。
@Configuration
public class HibernateConfiguration extends HibernateJpaAutoConfiguration{
@Autowired
Interceptor userInterceptor;
@Override
protected void customizeVendorProperties(Map<String, Object> vendorProperties) {
vendorProperties.put("hibernate.ejb.interceptor",userInterceptor);
}
}
自動裝配Interceptor
而不是讓 Hibernate 實例化它是讓它工作的關鍵。
現在困擾我的是邏輯分為兩部分,但希望一旦 DATAREST-373 得到解決,那么這將是不必要的。
我的一個簡單的 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;
}
}
我在 Spring 4.1.1、Hibernate 4.3.11 應用程序中遇到了類似的問題 - 而不是 Spring Boot。
我發現的解決方案(在閱讀 Hibernate EntityManagerFactoryBuilderImpl 代碼后)是,如果您將 bean 引用而不是類名傳遞給實體管理器定義的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 由 Spring 管理,因此它可以使用自動裝配和其他 Spring 性質的行為。
你好,
讀一讀: https : //github.com/spring-projects/spring-boot/commit/59d5ed58428d8cb6c6d9fb723d0e334fe3e7d9be (使用:HibernatePropertiesCustomizer 接口)
或者
對於簡單的攔截器:
為了在您的應用程序中配置它,您只需添加: spring.jpa.properties.hibernate.ejb.interceptor = path.to.interceptor (在 application.properties 中)。 攔截器本身應該是@Component 。
只要攔截器實際上不使用任何 beans 。 否則它有點復雜,但我很樂意提供解決方案。
不要忘記在 application-test.properties 中添加一個EmptyInterceptor ,以便在測試中不使用日志記錄系統(或任何你想使用它的東西)(這不會很有幫助)。
希望這對你有用。
最后一點:始終更新您的 Spring / Hibernate 版本(盡可能使用最新版本),您會發現大多數代碼將變得多余,因為新版本試圖盡可能減少配置。
在研究了兩天關於如何將 Hibernate Interceptors 與 Spring Data JPA 集成后,我找到了另一種方法,我的解決方案是 java 配置和 xml 配置之間的混合,但這篇文章非常有用。 所以我的最終解決方案是:
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;
}
}
數據源 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;
}
添加攔截器的實體和事務管理器
<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" />
持久化配置文件
<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>
我遇到了同樣的問題,並最終創建了一個小型 spring 庫來處理所有設置。
https://github.com/teastman/spring-data-hibernate-event
如果您使用的是 Spring Boot,則只需添加依賴項:
<dependency>
<groupId>io.github.teastman</groupId>
<artifactId>spring-data-hibernate-event</artifactId>
<version>1.0.0</version>
</dependency>
然后將注解@HibernateEventListener 添加到任何方法中,其中第一個參數是您要偵聽的實體,第二個參數是您要偵聽的 Hibernate 事件。 我還添加了靜態 util 函數 getPropertyIndex 以更輕松地訪問您要檢查的特定屬性,但您也可以只查看原始 Hibernate 事件。
@HibernateEventListener
public void onUpdate(MyEntity entity, PreUpdateEvent event) {
int index = getPropertyIndex(event, "name");
if (event.getOldState()[index] != event.getState()[index]) {
// The name changed.
}
}
使用標准 JPA 事件不可能獲得舊值,因此我認為我需要使用 Hibernate 攔截器。
不,可以在不使用攔截器的情況下僅使用 JPA 來獲取舊值。
假設您要審計的實體的基類是Auditable<T>
,因此,您可以在Auditable<T>
實體中聲明一個類型為Auditable<T>
的@Transient
變量,您可以使用COPY填充它(請參閱下面的內容)當實體將舊值加載到持久上下文中並且在更新之前。
/**
* Extend this class if you want your entities to be audited.
*/
@Getter
@Setter
@MappedSuperclass
@EntityListeners(AuditListener.class)
public abstract class Auditable implements Serializable {
@JsonIgnore
@Transient
private Auditable oldState;
}
您可以將@PostLoad
放在Auditable
基本實體中,或者我更喜歡將它放在傳遞給@EntityListeners
的偵聽AuditListener
@EntityListeners
。
public class AuditListener {
/**
* Triggered when an entity is loaded to the persistent.
*
* @param entity the one which is loaded
*/
@PostLoad
public void onPostLoad(final Auditable entity) {
//Here, you have access to the entity before it gets updated and
//after it's loaded to the context, so now you can have a new copy
//and set it to that Transient variable so you make sure it not
//gets persisted by JPA.
entity.setOldState(SerializationUtils.clone(entity));
}
/**
* Triggered when an entity updated and before committed the
* transaction.
*
* @param entity the one which is updated
*/
@PostUpdate
public void onPostUpdate(final Auditable entity) {
//Here, you have both copies the old and the new, thus you can
//track the changes and save or log them where ever you would like.
}
}
因為攔截器沒有注冊為 spring bean,所以你可以使用一個可以獲取ApplicationContext
實例的 util,如下所示:
@Component
public class SpringContextUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
SpringContextUtil.applicationContext=applicationContext;
}
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
}
然后就可以在攔截器中調用服務了,像這樣:
public class SimpleInterceptor extends EmptyInterceptor {
@Override
public String onPrepareStatement(String sql) {
MyService myService=SpringContextUtil.getApplicationContext().getBean(MyService.class);
myService.print();
return super.onPrepareStatement(sql);
}
}
提煉 Paulo Merson 的回答,另一種選擇是使用 lambda 表達式在@Configuration
class 中注冊您的HibernatePropertiesCustomizer
(一個FunctionalInterface
):
@Bean
public HibernatePropertiesCustomizer hibernatePropertiesCustomizer(MyInterceptor interceptor) {
return props -> props.put("hibernate.session_factory.interceptor", interceptor);
}
在Spring Boot中,您可以輕松注冊您的Hibernate 自定義攔截器。
實現HibernatePropertiesCustomizer
接口並覆蓋customize
方法以將自定義custum inteceptor
添加到hibernateProperties
:
@Component
public class MyCustomInterceptor extends EmptyInterceptor implements HibernatePropertiesCustomizer {
@Override
public void customize(Map<String, Object> hibernateProperties) {
hibernateProperties.put("hibernate.session_factory.interceptor", this);
}
@Override
public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, org.hibernate.type.Type[] types) {
System.out.println("onSave");
return super.onSave(entity, id, state, propertyNames, types);
}
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.