【问题标题】:How to obtain a spring bean in a static context/method (Hibernate Envers revision listener)如何在静态上下文/方法中获取 spring bean(Hibernate Envers 修订监听器)
【发布时间】:2018-11-15 22:24:09
【问题描述】:

注意:这不是一个重复的问题。阅读整篇文章;)
为了防止 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,我有实用程序/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 Test 框架进行集成测试时。根据 Spring 创建测试上下文和缓存 bean 的方式,bean 查找类可能会从另一个上下文返回实体管理器,而不是当前上下文。如果您在其他测试中运行它,当通过测试失败时,在我们的案例中就会发生这种情况(更多信息here)。

现在的问题:
1.有没有办法将MyRevisionListener定义为Spring bean,这样我就可以干净地注入持久化上下文了?
2. 如果以上为否,那么如何在静态上下文/方法中正确获取持久性上下文?

谢谢。

【问题讨论】:

  • 想知道为什么投反对票?
  • 如果只是在测试失败时才出现问题,那可能是因为之前的测试没有正确清除或拆除某些东西,从而污染了后续测试的问题吗?也许更详细一点可以解决这个问题。
  • 我的理解是:Spring为测试A创建上下文A(在这个上下文中,静态字段instance指向上下文A中的Bean A),测试通过,然后Spring为测试A创建上下文B测试 B,因为它具有不同的配置并且需要不同的上下文(在此上下文中,静态字段 instance 指向上下文 B 中的 Bean B)。现在 Spring 为测试 C 选择上下文 A,因为它具有相同的配置并且可以使用上下文 A 进行测试(现在静态字段 instance 指向上下文 B 中的 Bean B)。你觉得这有意义吗?
  • Roman 的回答绝对正确。过去我已经谈到如何在RevisionListener 中获取spring bean,最有效的方法是将所需的任何内容注入线程局部变量并在侦听器中访问它。一个很好的例子是 spring security 的SecurityContextHolder。在 Hibernate 5.3 中,我们将 RevisionListener 暴露为托管 bean,这意味着它现在可以是 @Component 或类似 spring-bean 的任何东西;但是 Spring 尚未实现对此的支持,请参阅 jira.spring.io/browse/SPR-16305
  • 谢谢@Naros,太棒了:)

标签: spring hibernate spring-data-jpa hibernate-envers


【解决方案1】:

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

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

执行此操作的好地方是启动 spring 事务或实体管理器创建。

【讨论】:

  • 我不确定我是否理解如何实现它。不过我猜使用测试监听器会更干净。如果我错了,请纠正我。
【解决方案2】:

我猜你可以将EntityManager 注入到测试中,并使用它在EntityManagerBeanLookup 中设置它。

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

【讨论】:

  • 是的,这就是我证明问题的方式。将其添加到所有测试中是不切实际的。 DirtiesContext 也没什么意思,因为它增加了测试时间。
【解决方案3】:

我最终使用了一个测试监听器(因为我们已经在我们的测试监听器中使用了 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;
    }

}

【讨论】:

    猜你喜欢
    • 2011-11-21
    • 2014-05-17
    • 1970-01-01
    • 2011-10-25
    • 1970-01-01
    • 1970-01-01
    • 2021-02-06
    • 2017-07-28
    • 1970-01-01
    相关资源
    最近更新 更多