如何在静态 context/method 中获取 spring bean(Hibernate Envers 修订监听器)

How to obtain a spring bean in a static context/method (Hibernate Envers revision listener)

注意:这不是重复的问题。阅读全文 post ;)
为了防止 XY 问题,这里是完整的故事:
我需要编写一个 Envers 修订侦听器来计算并保留 spring 应用程序中实体最近更改的差异:

public class MyRevisionListener implements EntityTrackingRevisionListener {

    @Override
    public void newRevision(Object revision) {
        ...
    }

    @Override
    public void entityChanged(Class entityClass,
                              String entityName,
                              Serializable entityId,
                              RevisionType revisionType,
                              Object revisionEntity) {
        EntityManager em = EntityManagerBeanLookup.getInstance().get();
        int id = ((DefaultRevisionEntity) revisionEntity).getId();
        List<?> revisions = AuditReaderFactory.get(em)
                .createQuery()
                .forRevisionsOfEntity(entityClass, false, true)
                .add(AuditEntity.id().eq(entityId))
                .add(AuditEntity.revisionNumber().le(id + 1))
                .addOrder(AuditEntity.revisionNumber().desc())
                .setMaxResults(2)
                .getResultList();

        checkArgument(revisions.size() > 0, "Need at least one revision: %s", revisions);

        // continue with calculating and persisting the diff;
    }

}

为了得到 EntityManager bean,我有 utility/bean EntityManagerBeanLookup:

@Component
public class EntityManagerBeanLookup implements Supplier<EntityManager> {

    @Getter
    private static EntityManagerBeanLookup instance;

    @Getter
    private EntityManager entityManager;

    @PersistenceContext
    public void setEntityManager(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    @Autowired
    public void setInstance(EntityManagerBeanLookup instance) {
        EntityManagerBeanLookup.instance = instance;
    }

    @Override
    public EntityManager get() {
        return ofNullable(getInstance())
                .map(EntityManagerBeanLookup::getEntityManager)
                .orElseThrow(() -> new IllegalStateException("Not initialized yet"));
    }

}

一切正常。问题是当我们使用 Spring 测试框架进行集成测试时。根据 Spring 创建测试上下文和缓存 bean 的方式,bean 查找 class returns 实体管理器可能来自另一个上下文,而不是当前上下文。在我们的案例中,如果您在其他测试中 运行 通过测试失败(更多信息 )。

现在提问:
1. 有什么方法可以将 MyRevisionListener 定义为 Spring bean,以便我可以干净地注入持久性上下文?
2. 如果以上为否,那么如何才能在静态 context/method 中正确获取持久性上下文?

谢谢。

如果您有两个 spring 上下文,您可能希望获取调用侦听器的上下文。鉴于侦听器是无状态单例,在侦听器调用和它在其中完成的 spring 上下文之间建立关系的唯一方法是通过调用线程上下文。

在堆栈的某个位置,您需要将实体管理器保存到本地线程,然后在侦听器中从那里获取它。

这样做的好地方是 spring 事务或实体管理器创建的开始。

我想您可以将 EntityManager 注入测试并使用它在 EntityManagerBeanLookup 中设置它。

但我认为更简洁的解决方案是将上下文标记为脏,因此 Spring 创建一个新的。参见 How to force a fresh version of the Spring context BEFORE the test executes

我最终使用了一个测试监听器(因为我们已经在使用 spring-test 测试监听器):

public class StaticBeanLookupResetTestListener extends AbstractTestExecutionListener {

    @Override
    public void prepareTestInstance(TestContext testContext) {
        testContext.getApplicationContext()
                .getBeansOfType(StaticBeanLookup.class) // The marker interface
                .forEach(this::resetSelfReference);
    }

    @SneakyThrows
    private void resetSelfReference(String name, StaticBeanLookup bean) {
        Method getter = bean.getClass().getDeclaredMethod("setInstance", type);
        getter.setAccessible(true);
        getter.invoke(bean, bean);
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }

}