您的当前位置:首页正文

【JPA】将Spring依赖项注入JPA EntityListener

2024-11-26 来源:个人技术集锦

背景说明

在项目开发中,用到了JPA监听器EntityListener,并且需要将Spring依赖注入(inject)到JPA实体监听器中。但是,在调用实体监听器回调方法时,使用该Bean实例却报空指针异常错误。
于是翻阅点资料和源码,发现虽然实体监听器加上了@Component,Spring也会实例化并且注入它的相关依赖,但是JPA真正使用的实体监听器仍将由new操作(诸如Hibernate之类的框架)执行,并不会直接取用Spring容器中的Bean实例,所以才会出现上述与预期不符的情况出现。

框架版本

SpringBoot2.2.13.RELEASE
Spring5.2.20
Spring Data JPA2.2.12.RELEASE
Hibernate5.4.27.Final
javax.persistence:2.2

示例代码

① 实体类

@Entity
@Table(...)
@EntityListeners({AuditingEntityListener.class})
public class EntityModel{
  ...
  @CreatedBy
  private String createBy;
  @CreatedDate
  private Date createdAt;
  ...
}

② 监听器类

@Component
public class AuditingEntityListener {

    @Autowired
    private AuditorAware<String> auditorAware;

    @PrePersist
    public void onCreate(Object object) {
        try {
            BeanUtils.setProperty(object, "createdAt", new Date());
            BeanUtils.setProperty(object, "createBy", auditorAware.getCurrentAuditor());
        } cache(Exception e) {
            //....
        }
    }
}

③ 操作人接口

public class MyAuditorAware implements AuditorAware<String> {
    /**
     * Returns the current auditor of the application.
     *
     * @return the current auditor.
     */
    @Override
    public Optional<String> getCurrentAuditor() {
        return Optional.of("测试用户");
    }
}

解决方案

方案一:静态依赖

这个非常好理解,即将Bean和其依赖项的依赖关系定义为“静态”,创建一个setter方法,以便Spring可以注入其依赖项。示例如下:
① 将依赖项声明为静态的

private static AuditorAware<String> auditorAware;

② 新增init方法,以便Spring容器可以注入依赖项

@Autowired
public void init(AuditorAware<String> auditorAware) {
    AuditingEntityListener.auditorAware = auditorAware;
    LOGGER.info("Initializing AuditingEntityListener.auditorHandler with dependency [" + auditorAware + "]");
}

完整代码

@Component // 必须加
public class AuditingEntityListener {

    private static AuditorAware<String> auditorAware;

    @Autowired
    public void init(AuditorAware<String> auditorAware) {
        AuditingEntityListener.auditorAware = auditorAware;
        LOGGER.info("Initializing AuditingEntityListener.auditorHandler with dependency [" + auditorAware + "]");
    }

    @PrePersist
    public void onCreate(Object object) {
        try {
            BeanUtils.setProperty(object, "createdAt", new Date());
            BeanUtils.setProperty(object, "createBy", auditorAware.getCurrentAuditor());
        } cache(Exception e) {
            //....
        }
    }
}

方案二:使用ApplicationContext获取Bean实例

这个方案就是在回调函数调用时,直接从SpringContext中获取依赖项 【函数变量】。示例如下:
① SpringBeanUtil工具类:

@Component
public class SpringBeanUtil implements ApplicationContextAware {

    private static ApplicationContext context;

    private SpringBeanUtil() {
    }

    public static <T> T getBean(Class<T> clazz) throws BeansException {
        Assert.state(context != null, "Spring context in the SpringBeanUtil is not been initialized yet!");
        return context.getBean(clazz);
    }

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

② 监听器类

// @Component 无需加该注解
public class AuditingEntityListener {

    @PrePersist
    public void onCreate(Object object) {
        try {
            // 借助工具类从SpringContext获取实例
            AuditorAware<String> auditorAware = SpringBeanUtil.getBean(AuditorAware.class);

            BeanUtils.setProperty(object, "createdAt", new Date());
            BeanUtils.setProperty(object, "createBy", auditorAware.getCurrentAuditor());
        } cache(Exception e) {
            //....
        }
    }
}

方案三:回调方法使用时注入依赖项

这个方案就是在回调函数调用时,先注入依赖项 【类变量】,和方案二有异曲同工之妙。示例如下:
① AutowireHelper工具类:

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

② 监听器类

@Component // 必须加该注解
public class AuditingEntityListener {

    @Autowired
    private AuditorAware<String> auditorAware;

    @PrePersist
    public void onCreate(Object object) {
        try {
            // 借助工具类执行注入操作,实现自动装配
            AutowireHelper.autowire(this);

            BeanUtils.setProperty(object, "createdAt", new Date());
            BeanUtils.setProperty(object, "createBy", auditorAware.getCurrentAuditor());
        } cache(Exception e) {
            //....
        }
    }
}

方案四:JPA数据源配置

该方案就是将SpringBeanContainer注册到Hibernate,我们只需要通过设置适当的配置属性告诉 Hibernate使用Spring作为bean提供者即可。
① EntityManagerFactory配置

@Configuration
public class JpaConfig {

    @Bean(name = "entityManagerFactory")
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource, 
                                                                       EntityManagerFactoryBuilder builder, 
                                                                       ConfigurableListableBeanFactory beanFactory) {

        return builder.dataSource(dataSource) //
                .packages("com.xxx.jpa") //
                .persistenceUnit("myPersistenceUnit") //
                .properties(Map.of(AvailableSettings.BEAN_CONTAINER, new SpringBeanContainer(beanFactory))) // 设置使用Spring作为bean提供者
                .build();
    }
}

另外一种写法:

@Bean(name = "entityManagerFactory")
public LocalContainerEntityManagerFactoryBean customCartEntityManagerFactory(DataSource customCartDataSource,
                                                                             EntityManagerFactoryBuilder builder,
                                                                             ConfigurableListableBeanFactory beanFactory) {
    LocalContainerEntityManagerFactoryBean mf = builder
            .dataSource(customCartDataSource)
            .packages("com.my.domain")
            .persistenceUnit("myPersistenceUnit")
            .build();
    mf.getJpaPropertyMap().put(AvailableSettings.BEAN_CONTAINER, new SpringBeanContainer(beanFactory));
    return mf;
}

② 监听器类
如示例代码,无需任何修改。

从Hibernate 5.3开始,ManagedBeanRegistry可以直接使用Spring容器自动注入。详见:

方案五:高级做法-AOP编程织入

该方案主要借助@Configurable和Spring的AspectJ weaver作为java agent来实现。
① 标记监听器类为可配置

/**
 * This bean will NOT be instanciated by Spring but it should be configured by Spring
 * because of the {@link Configurable}-Annotation.
 * <p>
 * The configuration only works if the <code>AuditingEntityListener</code> is not used as an <code>EntityListener</code>
 * via the {@link javax.persistence.EntityListeners}-Annotation.
 *
 */
@Configurable // 标记可配置
public class AuditingEntityListener {

    @Nullable
    private ObjectFactory<AuditorAware> auditorAware;

    /**
     * Configures the {@link AuditorAware} to be used to set the current auditor on the domain types touched.
     *
     * @param auditorAware must not be {@literal null}.
     */
    public void setAuditorAware(ObjectFactory<AuditorAware> auditorAware) {

        Assert.notNull(auditorAware, "AuditorAware must not be null!");
        this.auditorAware = auditorAware;
    }

    @PrePersist
    public void onCreate(Object object) {
        try {
            BeanUtils.setProperty(object, "createdAt", new Date());
            BeanUtils.setProperty(object, "createBy", auditorAware.getCurrentAuditor());
        } cache(Exception e) {
            //....
        }
    }
}

② 切面实现

class JpaAuditingRegistrar extends AuditingBeanDefinitionRegistrarSupport {
	...
	...
	@Override
	public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry registry) {

		Assert.notNull(annotationMetadata, "AnnotationMetadata must not be null!");
		Assert.notNull(registry, "BeanDefinitionRegistry must not be null!");

		registerBeanConfigurerAspectIfNecessary(registry);
		super.registerBeanDefinitions(annotationMetadata, registry);
		registerInfrastructureBeanWithId(
				BeanDefinitionBuilder.rootBeanDefinition(AuditingBeanFactoryPostProcessor.class).getRawBeanDefinition(),
				AuditingBeanFactoryPostProcessor.class.getName(), registry);
	}
	...
	...
}

具体代码可查看:spring-data-jpa#JpaAuditingRegistrar以及spring-aspectjs#AnnotationBeanConfigurerAspect
以上做法参考:spring-data-jpaAuditingEntityListener的实现,可自行看源码。

小结

在编程的世界里,蕴含着丰富的知识等着我们去挖掘,遇到的问题是我们最好的老师,解决问题的过程是促进我们学习成长的宝贵经验。解决问题不是关键,关键是通过这个过程,我们学到了什么?是否搞清弄懂其原理。

显示全文