簡體   English   中英

如何在 Spring Boot 中使用 Spring managed Hibernate 攔截器?

[英]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;
        }
    };
}

使用 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);
    }
}
  • 我正在使用 Spring Boot 2.1.7.RELEASE。
  • 取而代之的hibernate.session_factory.interceptor可以使用hibernate.ejb.interceptor 由於向后兼容性要求,這兩個屬性都可能起作用。

為什么 HibernatePropertiesCustomizer 而不是 application.properties

一個建議的答案是在 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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM