将 Spring 依赖项注入 JPA EntityListener

我正在尝试把 注入 Spring 依赖项变成 JPA 实体监听器。下面是我的听众类:

@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) 永远是无效的

有人能帮忙吗?

69825 次浏览

我相信这是因为这个侦听器 bean 不在 Spring 的控制之下。Spring 没有实例化它,Spring 怎么知道如何找到那个 bean 并进行注入呢?

我还没有尝试过这种方法,但是似乎可以使用 AspectJ Weaver 和 Spring 的 Configable 注释来让 Spring 控制非 Spring 实例化 bean。

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

在无状态 bean 上注入依赖项的技巧是将依赖项定义为“ static”,创建一个 setter 方法,这样 Spring 就可以注入依赖项(将其分配给静态依赖项)。

将依赖项声明为 static。

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

我开始沿着使用 AOP 向 Entity 侦听器注入 Spring bean 的道路前进。经过一天半的研究和尝试不同的事情,我偶然发现了这个 链接,它说:

不可能将 Spring 托管 bean 注入到 JPA EntityListener 类中。这是因为 JPA 侦听器机制应该基于无状态类,所以这些方法实际上是静态的,并且不知道上下文。... 无论多少 AOP 都不会拯救你,也不会向代表侦听器的“对象”注入任何东西,因为实现实际上并不创建实例,而是使用 class 方法。

此时,我重新组合并偶然发现了 EclipseLink描述事件适配器。利用这些信息,我创建了一个侦听器类,它扩展了 DescriptorAdapter。

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
}
}

为了使用这个类,我可以在我的实体类上使用@EntityListener 注释。不幸的是,这个方法不允许 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 创建一个实体侦听器,向它注入所需的任何依赖项,侦听器对象向它打算侦听的实体类注册自己。

希望这个能帮上忙。

那这个解决方案呢?

@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/

这实际上是一个古老的问题,但我找到了另一种解决方案:

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));
}
}

可能不是最好的,但比静态变量 w/o AOP + 编织要好。

我测试了 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;
}
}

另一个选择:

创建一个服务使 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 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。我已经尝试了多个选项(@Configure(...)) ,除了创建一个静态访问上下文的类之外,没有一个选项有效。我已经进退两难了,我认为这是一个优雅的选择。

JPA 监听器的问题在于:

  1. 它们不是由 Spring 管理的(所以不需要注射)

  2. 它们已经(或者可能已经)创建了 之前 Spring 的 Application Context 已经就绪(所以我们不能在构造函数调用中注入 bean)

我解决这个问题的方法:

1)使用 public static 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 侦听器都必须扩展这个类:

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));
}


}

我用 @ 组件注释了侦听器,然后创建了一个 非静态塞特来分配注入的 Springbean,它工作得很好

我的代码是这样的:

@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() {
...
}




}

由于 Spring V5.1(和 Hibernate V5.3) ,它应该能够立即工作,因为 Spring 注册为这些类的提供者。 请参阅 < a href = “ https://docs.spring.io/spring/docs/current/javadoc-api/org/springFramework/orm/hibernate5/SpringBeanContainer.html”rel = “ norefrer”> SpringBeanContainer 的文档

试着像这样使用 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

在我看来,最自然的方法是干预实例化 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
}

现在,唯一剩下的事情就是在 Hibernate 配置设置中注入属性名为 javax.persistence.bean.managerBeanManager实现。也许有很多方法可以做到这一点,让我只带来其中的一个:

@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;
}
}

只要记住两样东西必须是青豆: A) SpringCdiBeanManager,以便 BeanFactory可被注入/自动接驳至 SpringCdiBeanManager; B)您的 EntityListener 类,这样第 return beanFactory.getBean(clazz);行将成功。

2)正如@AdrianShum 非常正确地指出的那样,对于 Spring bean 来说,在 Hibernate 5.3及更高版本中事情要容易得多。因为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) ,有一个简单的解决方案。 没有黑客,不需要使用 AOP,没有帮助类,没有明确的自动装配,没有初始化块强制注入。

你只需要:

  1. 像往常一样,将侦听器设置为 @Component并声明自动连接的 bean。
  2. 在 Spring 应用程序中配置 JPA,使用 Spring 作为 bean 提供者。

这是怎么(在科特林) ..。

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。然后向 jpaPropertyMap添加以下键值对: AvailableSettings.BEAN_CONTAINER = > 应用程序上下文的 bean 工厂。

在我的 SpringBoot 应用程序中,我已经有了下面的代码来配置数据源(例如,找到 给你的样板代码)。我只需要添加将 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
}

以 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,它在侦听器中启用自动装配。

@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 创建它的回调对象时,ManagedBeanRegistryImpl 负责创建 EntityListener 的实例。创建 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 中。稍后,当 EntityListener 生命周期方法被调用时(例如@PostLoad,@PostPerist 等) ,所需 bean 的实例可以从 BeanFactory 中提取出来,因为此时已经由 Spring 创建了 bean。