簡體   English   中英

將 Spring 依賴項注入 JPA EntityListener

[英]Injecting a Spring dependency into a JPA EntityListener

我正在嘗試將 Spring 依賴項注入JPA EntityListener 這是我的監聽器類:

@Configurable(autowire = Autowire.BY_TYPE, dependencyCheck = true)
public class PliListener {

    @Autowired
    private EvenementPliRepository evenementPliRepository;

    @PostPersist
    void onPostPersist(Pli pli) {
        EvenementPli ev = new EvenementPli();
        ev.setPli(pli);
        ev.setDateCreation(new Date());
        ev.setType(TypeEvenement.creation);
        ev.setMessage("Création d'un pli");
        System.out.println("evenementPliRepository: " + evenementPliRepository);
        evenementPliRepository.save(ev);
    }


}

這是我的實體類:

@RooJavaBean
@RooToString
@RooJpaActiveRecord
@EntityListeners(PliListener.class)
public class Pli implements Serializable{
...

但是,我的依賴項(即evenementPliRepository始終為 null

有人可以幫忙嗎?

在無狀態 bean 上注入依賴項的一個技巧是將依賴項定義為“靜態”,創建一個 setter 方法,以便 Spring 可以注入依賴項(將其分配給靜態依賴項)。

將依賴聲明為靜態。

static private EvenementPliRepository evenementPliRepository;

創建一個方法,以便 Spring 可以注入它。

@Autowired
public void init(EvenementPliRepository evenementPliRepository) 
{
    MyListenerClass.evenementPliRepository = evenementPliRepository;
    logger.info("Initializing with dependency ["+ evenementPliRepository +"]"); 
}

更多詳細信息,請訪問: http : //blog-en.lineofsightnet.com/2012/08/dependency-injection-on-stateless-beans.html

這實際上是一個老問題,但我找到了一個替代解決方案:

public class MyEntityListener {
    @Autowired
    private ApplicationEventPublisher publisher;

    @PostPersist
    public void postPersist(MyEntity target) {
        SpringBeanAutowiringSupport.processInjectionBasedOnCurrentContext(this);

        publisher.publishEvent(new OnCreatedEvent<>(this, target));
    }

    @PostUpdate
    public void postUpdate(MyEntity target) {
        SpringBeanAutowiringSupport.processInjectionBasedOnCurrentContext(this);

        publisher.publishEvent(new OnUpdatedEvent<>(this, target));
    }

    @PostRemove
    public void postDelete(MyEntity target) {
        SpringBeanAutowiringSupport.processInjectionBasedOnCurrentContext(this);

        publisher.publishEvent(new OnDeletedEvent<>(this, target));
    }
}

可能不是最好的,但比沒有 AOP + 編織的靜態變量更好。

我用@Component注解注解了監聽器,然后創建了一個非靜態的setter來分配注入的Spring bean,效果很好

我的代碼看起來像:

@Component
public class EntityListener {

    private static MyService service;

    @Autowired
    public void setMyService (MyService service) {
        this.service=service;
    }


    @PreUpdate
    public void onPreUpdate() {

        service.doThings()

    }

    @PrePersist
    public void onPersist() {
       ...
    }


}

這個解決方案呢?

@MappedSuperclass
@EntityListeners(AbstractEntityListener.class)
public abstract class AbstractEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id")
    private Long id;

    @Column(name = "creation_date")
    private Date creationDate;

    @Column(name = "modification_date")
    private Date modificationDate;

}

那么聽者...

@Component
public class AbstractEntityListener {

    @Autowired
    private DateTimeService dateTimeService;

    @PreUpdate
    public void preUpdate(AbstractEntity abstractEntity) {
        AutowireHelper.autowire(this, this.dateTimeService);
            abstractEntity.setModificationDate(this.dateTimeService.getCurrentDate());
    }

    @PrePersist
    public void prePersist(AbstractEntity abstractEntity) {
        AutowireHelper.autowire(this, this.dateTimeService);
        Date currentDate = this.dateTimeService.getCurrentDate();
        abstractEntity.setCreationDate(currentDate);
        abstractEntity.setModificationDate(currentDate);
    }
}

還有幫手...

    /**
     * Helper class which is able to autowire a specified class. It holds a static reference to the {@link org
     * .springframework.context.ApplicationContext}.
     */
    public final class AutowireHelper implements ApplicationContextAware {

        private static final AutowireHelper INSTANCE = new AutowireHelper();
        private static ApplicationContext applicationContext;

        private AutowireHelper() {
        }

        /**
         * Tries to autowire the specified instance of the class if one of the specified beans which need to be autowired
         * are null.
         *
         * @param classToAutowire the instance of the class which holds @Autowire annotations
         * @param beansToAutowireInClass the beans which have the @Autowire annotation in the specified {#classToAutowire}
         */
        public static void autowire(Object classToAutowire, Object... beansToAutowireInClass) {
            for (Object bean : beansToAutowireInClass) {
                if (bean == null) {
                    applicationContext.getAutowireCapableBeanFactory().autowireBean(classToAutowire);
                }
            }
        }

        @Override
        public void setApplicationContext(final ApplicationContext applicationContext) {
            AutowireHelper.applicationContext = applicationContext;
        }

        /**
         * @return the singleton instance.
         */
        public static AutowireHelper getInstance() {
            return INSTANCE;
        }

    }

為我工作。

來源: http : //guylabs.ch/2014/02/22/autowiring-pring-beans-in-hibernate-jpa-entity-listeners/

從 Spring V5.1(和 Hibernate V5.3)開始,它應該開箱即用,因為 Spring 注冊為這些類的提供者。 查看SpringBeanContainer 的文檔

我開始走上使用 AOP 將 spring bean 注入實體偵聽器的路徑。 經過一天半的研究和嘗試不同的東西后,我發現了這個鏈接,它說:

不可能將 spring 管理的 bean 注入 JPA EntityListener 類。 這是因為 JPA 偵聽器機制應該基於無狀態類,因此這些方法實際上是靜態的,並且不受上下文影響。 ... 再多的 AOP 也拯救不了你,沒有任何東西被注入到代表偵聽器的“對象”中,因為實現實際上並沒有創建實例,而是使用了類方法。

在這一點上,我重新組合並偶然發現了 EclipseLink DescriptorEventAdapter 使用這些信息,我創建了一個擴展描述符適配器的偵聽器類。

public class EntityListener extends DescriptorEventAdapter {
    private String injectedValue;

    public void setInjectedValue(String value){
        this.injectedValue = value;
    }

    @Override
    public void aboutToInsert(DescriptorEvent event) {
       // Do what you need here
    }
}

為了使用該類,我可以在實體類上使用 @EntityListeners 注釋。 不幸的是,這種方法不允許 Spring 控制我的監聽器的創建,因此不允許依賴注入。 相反,我在我的班級中添加了以下“init”函數:

public void init() {
    JpaEntityManager entityManager = null;

    try {
        // Create an entity manager for use in this function
        entityManager = (JpaEntityManager) entityManagerFactory.createEntityManager();
        // Use the entity manager to get a ClassDescriptor for the Entity class
        ClassDescriptor desc = 
            entityManager.getSession().getClassDescriptor(<EntityClass>.class);
        // Add this class as a listener to the class descriptor
        desc.getEventManager().addListener(this);
    } finally {
        if (entityManager != null) {
            // Cleanup the entity manager
            entityManager.close();
        }
    }
}

添加一點 Spring XML 配置

<!-- Define listener object -->
<bean id="entityListener" class="EntityListener " init-method="init">
    <property name="injectedValue" value="Hello World"/>
    <property name="entityManagerFactory" ref="emf"/>
</bean>  

現在我們有這樣一種情況,Spring 創建一個實體偵聽器,將所需的任何依賴項注入它,然后偵聽器對象將自己注冊到它打算偵聽的實體類。

我希望這有幫助。

我測試了https://guylabs.ch/2014/02/22/autowiring-pring-beans-in-hibernate-jpa-entity-listeners/ 中建議的方法並工作。 不是很干凈,但可以完成工作。 對我來說稍微修改的 AutowireHelper 類看起來像這樣:

import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class AutowireHelper implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    private AutowireHelper() {
    }

    public static void autowire(Object classToAutowire) {
        AutowireHelper.applicationContext.getAutowireCapableBeanFactory().autowireBean(classToAutowire);
    }

    @Override
    public void setApplicationContext(final ApplicationContext applicationContext) {
        AutowireHelper.applicationContext = applicationContext;
    }
}

然后從實體偵聽器調用它,如下所示:

public class MyEntityAccessListener {

    @Autowired
    private MyService myService;


    @PostLoad
    public void postLoad(Object target) {

        AutowireHelper.autowire(this);

        myService.doThings();
        ...
    }

    public void setMyService(MyService myService) {
        this.myService = myService;
    }
}

JPA 偵聽器的問題在於:

  1. 它們不是由 Spring 管理的(所以沒有注入)

  2. 它們是(或可能)Spring 的應用程序上下文准備好之前創建的(因此我們不能在構造函數調用中注入 bean)

我處理這個問題的解決方法:

1) 使用公共靜態LISTENERS字段創建Listener類:

public abstract class Listener {
    // for encapsulation purposes we have private modifiable and public non-modifiable lists
    private static final List<Listener> PRIVATE_LISTENERS = new ArrayList<>();
    public static final List<Listener> LISTENERS = Collections.unmodifiableList(PRIVATE_LISTENERS);

    protected Listener() {
        PRIVATE_LISTENERS.add(this);
    }
}

2) 我們想要添加到Listener.LISTENERS所有 JPA 監聽Listener.LISTENERS都必須擴展這個類:

public class MyListener extends Listener {

    @PrePersist
    public void onPersist() {
        ...
    }

    ...
}

3) 現在我們可以在 Spring 的 Application Context 准備好之后獲取所有監聽器並注入 bean

@Component
public class ListenerInjector {

    @Autowired
    private ApplicationContext context;

    @EventListener(ContextRefreshedEvent.class)
    public void contextRefreshed() {
       Listener.LISTENERS.forEach(listener -> context.getAutowireCapableBeanFactory().autowireBean(listener));
    }

}

嘗試像這樣使用ObjectFactory

@Configurable
public class YourEntityListener {
    @Autowired
    private ObjectFactory<YourBean> yourBeanProvider;

    @PrePersist
    public void beforePersist(Object target) {
       YourBean yourBean = yourBeanProvider.getObject();
       // do somthing with yourBean here
    }
}

我在 spring-data-jpa 的org.springframework.data.jpa.domain.support.AuditingEntityListener找到了這個解決方案。

演示: https : //github.com/eclipseAce/inject-into-entity-listener

我相信這是因為這個偵聽器 bean 不受 Spring 的控制。 Spring 沒有實例化它,Spring 怎么知道如何找到那個 bean 並進行注入?

我還沒有嘗試過,但似乎您可以使用帶有 Spring 的 Configurable 注釋的 AspectJ Weaver 來讓 Spring 控制非 Spring 實例化的 bean。

http://static.springsource.org/spring/docs/3.1.2.RELEASE/spring-framework-reference/html/aop.html#aop-using-aspectj

另外一個選擇:

創建一個服務以使 ApplicationContext 可訪問:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;

import lombok.Setter;

@Service
class ContextWrapper {

    @Setter
    private static ApplicationContext context;

    @Autowired
    public ContextWrapper(ApplicationContext ac) {
        setContext(ac);
    }

    public static ApplicationContext getContext() {
        return context;
    }

}

用它:

...    
public class AuditListener {

    private static final String AUDIT_REPOSITORY = "AuditRepository";

    @PrePersist
    public void beforePersist(Object object){
        //TODO:
    }

    @PreUpdate
    public void beforeUpdate(Object object){
        //TODO:
    }

    @PreRemove
    public void beforeDelete(Object object) {
        getRepo().save(getAuditElement("DEL",object));
    }

    private Audit getAuditElement(String Operation,Object object){

        Audit audit = new Audit();
        audit.setActor("test");
        Timestamp timestamp = new Timestamp(System.currentTimeMillis());
        audit.setDate(timestamp);

        return audit;
    }

    private AuditRepository getRepo(){
        return ContextWrapper.getContext().getBean(AUDIT_REPOSITORY, AuditRepository.class);
    }
}

這個類是作為 jpa 的偵聽器創建的:

...
@Entity
@EntityListeners(AuditListener.class)
@NamedQuery(name="Customer.findAll", query="SELECT c FROM Customer c")
public class Customer implements Serializable {
    private static final long serialVersionUID = 1L;
...

由於偵聽器不受 Spring 的控制,因此它無法訪問上下文 bean。 我嘗試了多個選項(@Configurable (...)),除了創建一個靜態訪問上下文的類之外,沒有一個有效。 已經陷入困境,我認為這是一個優雅的選擇。

在我看來,最自然的方式是介入 EntityListener 的實例化過程。 這種方式在 Hibernate 5.3 之前的版本和 5.3 之后的版本中顯着不同。

1)在 5.3 之前的 Hibernate 版本中org.hibernate.jpa.event.spi.jpa.ListenerFactory負責 EntityListener 實例化。 如果您提供自己的基於 CDI 的javax.enterprise.inject.spi.BeanManager則可以攔截此工廠的實例化。 CDI 接口(對於 Spring DI 世界是不必要的)冗長,但實現 Spring BeanFactory 支持的 CDI Bean 管理器並不困難。

@Component
public class SpringCdiBeanManager implements BeanManager {

    @Autowired
    private BeanFactory beanFactory;

    @Override
    public <T> AnnotatedType<T> createAnnotatedType(Class<T> type) {
        return new SpringBeanType<T>(beanFactory, type);
    }

    @Override
    public <T> InjectionTarget<T> createInjectionTarget(AnnotatedType<T> type) {
       return (InjectionTarget<T>) type;
    }
    ...
    // have empty implementation for other methods 
}

依賴於類型的SpringBeanType<T>將如下所示:

public class  SpringBeanType <T> implements AnnotatedType<T>, InjectionTarget<T>{

    private BeanFactory beanFactory;
    private Class<T> clazz;

    public SpringBeanType(BeanFactory beanFactory, Class<T> clazz) {
        this.beanFactory = beanFactory;
        this.clazz = clazz;
    }

    @Override
    public T produce(CreationalContext<T> ctx) {
        return beanFactory.getBean(clazz);
    }
    ...
    // have empty implementation for other methods 
}

現在,剩下的唯一事情就是在屬性名稱javax.persistence.bean.manager下將我們的BeanManager實現注入到 Hibernate 配置設置中。 可能有很多方法可以做到這一點,讓我只介紹其中一種:

@Configuration
public class HibernateConfig {

    @Autowired
    private SpringCdiBeanManager beanManager;

    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter jpaVendorAdapter = new HibernateJpaVendorAdapter(){
            @Override
            public Map<String, Object> getJpaPropertyMap(){
                Map<String, Object> jpaPropertyMap = super.getJpaPropertyMap();
                jpaPropertyMap.put("javax.persistence.bean.manager", beanManager);
                return jpaPropertyMap;
            }
        };
        // ...
        return jpaVendorAdapter;
    }
}

請記住,有兩件事必須是 Spring bean:a) SpringCdiBeanManager ,以便BeanFactory可以注入/自動裝配到它; b) 您的 EntityListener 類,因此該行return beanFactory.getBean(clazz); 會成功。

2)在 Hibernate 5.3 及更高版本中,Spring bean 的事情要容易得多,正如@AdrianShum 非常正確地指出的那樣。 由於 5.3 Hibernate 使用org.hibernate.resource.beans.container.spi.BeanContainer概念,並且有 Spring Beans 的現成實現, org.springframework.orm.hibernate5.SpringBeanContainer 在這種情況下,只需遵循其javadoc

由於 Hibernate 5.3 版和 Spring 5.1 版(即 Spring Boot 2.1 版),有一個簡單的解決方案。 沒有 hack,不需要使用 AOP,沒有幫助類,沒有顯式的自動裝配,沒有 init 塊來強制注入。

你只需要:

  1. 像往常一樣,使偵聽器成為@Component並聲明自動裝配的 bean。
  2. 在 Spring 應用程序中配置 JPA 以使用 Spring 作為 bean 提供者。

這是(在 Kotlin 中)如何...

1) 實體監聽器

@Component
class EntityXyzListener(val mySpringBean: MySpringBean) {

    @PostLoad
    fun afterLoad(entityXyz: EntityXyz) {
        // Injected bean is available here. (In my case the bean is a 
        // domain service that I make available to the entity.)
        entityXyz.mySpringBean= mySpringBean
    }

}

2) JPA 數據源配置

在您的應用程序中訪問LocalContainerEntityManagerFactoryBean 然后將以下鍵值對添加到jpaPropertyMapAvailableSettings.BEAN_CONTAINER => 應用程序上下文的 bean 工廠。

在我的 Spring Boot 應用程序中,我已經有了下面的代碼來配置數據源(例如在這里找到的樣板代碼)。 我只需要添加的代碼行放BEAN_CONTAINER在屬性jpaPropertyMap

@Resource
lateinit var context: AbstractApplicationContext

@Primary
@Bean
@Qualifier("appDatasource")
@ConfigurationProperties(prefix = "spring.datasource")
fun myAppDatasource(): DataSource {
    return DataSourceBuilder.create().build()
}

@Primary
@Bean(name = ["myAppEntityManagerFactory"])
fun entityManagerFactoryBean(builder: EntityManagerFactoryBuilder): LocalContainerEntityManagerFactoryBean {
    val localContainerEntityManagerFactoryBean =
            builder
                    .dataSource(myAppDatasource())
                    .packages("com.mydomain.myapp")
                    .persistenceUnit("myAppPersistenceUnit")
                    .build()
    // the line below does the trick
    localContainerEntityManagerFactoryBean.jpaPropertyMap.put(
            AvailableSettings.BEAN_CONTAINER, SpringBeanContainer(context.beanFactory))
    return localContainerEntityManagerFactoryBean
}

我試圖將Spring依賴項注入到JPA EntityListener中 這是我的偵聽器類:

@Configurable(autowire = Autowire.BY_TYPE, dependencyCheck = true)
public class PliListener {

    @Autowired
    private EvenementPliRepository evenementPliRepository;

    @PostPersist
    void onPostPersist(Pli pli) {
        EvenementPli ev = new EvenementPli();
        ev.setPli(pli);
        ev.setDateCreation(new Date());
        ev.setType(TypeEvenement.creation);
        ev.setMessage("Création d'un pli");
        System.out.println("evenementPliRepository: " + evenementPliRepository);
        evenementPliRepository.save(ev);
    }


}

這是我的Entity類:

@RooJavaBean
@RooToString
@RooJpaActiveRecord
@EntityListeners(PliListener.class)
public class Pli implements Serializable{
...

但是,我的依賴項(即evenementPliRepository始終為null

誰能幫忙嗎?

基於 Paulo Merson 的回答,這里是如何使用JpaBaseConfiguration設置 SpringBeanContainer 的變體。 這是兩個步驟:

第 1 步:將偵聽器定義為 Spring 組件。 請注意,自動裝配通過構造函數注入工作。

@Component
public class PliListener {

    private EvenementPliRepository evenementPliRepository;

    public PliListener(EvenementPliRepository repo) {
        this.evenementPliRepository = repo;
    }

    @PrePersist
    public void touchForCreate(Object target) {
        // ...
    }

    @PostPersist
    void onPostPersist(Object target) {
        // ...
    }
}

第 2 步:設置SpringBeanContainer ,它可以在偵聽器中啟用自動裝配。 SpringBeanContainer JavaDoc可能值得一看。

@Configuration
public class JpaConfig extends JpaBaseConfiguration {
    
    @Autowired
    private ConfigurableListableBeanFactory beanFactory;

    protected JpaConfig(DataSource dataSource, JpaProperties properties,
            ObjectProvider<JtaTransactionManager> jtaTransactionManager) {
        super(dataSource, properties, jtaTransactionManager);
    }

    @Override
    protected AbstractJpaVendorAdapter createJpaVendorAdapter() {
        return new HibernateJpaVendorAdapter();
    }

    @Override
    protected Map<String, Object> getVendorProperties() {
        Map<String, Object> props = new HashMap<>();

        // configure use of SpringBeanContainer
        props.put(org.hibernate.cfg.AvailableSettings.BEAN_CONTAINER, 
            new SpringBeanContainer(beanFactory));
        return props;
    }

}

正如其他人所指出的,SpringBeanContainer 似乎是將 Spring 連接到 Hibernate 的 ManagedBeanRegistryImpl 的方式,后者負責在 Hibernate 創建它的回調對象時創建 EntityListeners 的實例。 創建 bean 的調用被委托給 SpringBeanContainer,它可以通過構造函數注入和自動裝配來創建 Spring bean。 例如 EntityListener 看起來像

public class MyEntityListener {

    @Autowired
    private AnotherBean anotherBean;

    private MyBean myBean;
    public InquiryEntityListener(MyBean myBean) {
        this.myBean = myBean;
    }

    public MyEntityListener() {
    }
}

請注意,EntityListener 不需要 @Component 注釋,因為這只會創建一個 Hibernate 不使用的額外實例。

但是,在使用 SpringBeanContainer 時,必須牢記一些重要的限制和注意事項。 在我們的用例中,EntityListener 的實例是在創建 Hibernate EntityManager 期間創建的。 由於這發生在 Spring 生命周期的早期,因此此時許多 bean 並不存在。 這導致了以下發現:

SpringBeanContainer 將僅自動裝配/構造器 bean 依賴項,這些依賴項在創建 EntityListener 時存在。 不存在的構造函數依賴會導致調用默認構造函數。 使用 SpringBeanContainer 時本質上存在競爭條件。

解決此問題的方法是將 DefaultListableBeanFactory 實例注入 EntityListener。 稍后,當調用 EntityListeners 生命周期方法(即 @PostLoad、@PostPersist 等)時,可以將所需 bean 的實例從 BeanFactory 中拉出,因為此時 Spring 已經創建了 bean。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

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