0

Note: it's not a duplicated question. Read the whole post ;)
To prevent XY problem, here's the full story:
I need to write an Envers revision listener to calculate and persist the diff of the most recent change of the entity in a spring Application:

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

}

and to get EntityManager bean I have the 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"));
    }

}

Everything works just fine. The problem is when we have integration tests using Spring Test framework. Depending on how Spring creates test contexts and caches beans, it's possible that the bean lookup class returns an entity manager from another context, not the current context. It happens in our case when a passing test fails if you run it among other tests (more info here).

Now questions:
1. Is there any way to define MyRevisionListener as a Spring bean, so that I can cleanly inject the persistence context?
2. If the above is a NO, then how can I properly get the persistence context in a static context/method?

Thanks.

Rad
  • 4,292
  • 8
  • 33
  • 71
  • Wonder why downvoted? – Rad Jun 06 '18 at 11:17
  • If it is only problematic when a test fails, could that be because something isn't being properly cleared or torn down from the prior test therefore tainting the issue of the follow-up test? Perhaps a bit more detail about that could address the problem. – Naros Jun 06 '18 at 13:23
  • What I understand is: Spring creates Context A for Test A (in this context the static field `instance` is pointing to Bean A in Context A), the test passes, then Spring creates Context B for Test B since it has different configuration and needs a different context (in this context the static field `instance` is pointing to Bean B in Context B). Now Spring pick up Context A for Test C since it has the same configuration and can use Context A for the test (now the static field `instance` is pointing to Bean B in Context B). Does it make sense to you? – Rad Jun 06 '18 at 13:32
  • 1
    Roman is absolutely right in his answer. I have touched on how to get spring beans in the `RevisionListener` in the past and the most efficient way is to inject whatever you need into a thread-local variable and access that in the listener. A great example is spring security's `SecurityContextHolder`. In Hibernate 5.3, we exposed the `RevisionListener` to be a managed bean, which means it can now be a `@Component` or whatever just like a spring-bean; however Spring has not yet implemented their support for this, see https://jira.spring.io/browse/SPR-16305 – Naros Jun 06 '18 at 14:06
  • Thanks @Naros, that's awesome :) – Rad Jun 07 '18 at 10:05

3 Answers3

2

If you have two spring contexts you probably want to get the one in which the listener was invoked. Given that listener is stateless singleton the only way to establish the relation between listener invocation and the spring context it was done in is via calling thread context.

That is somewhere up the stack you need to save the entity manager to a thread local and then get it from there in the listener.

The good place to do it would be start of the spring transaction or entity manager creation.

  • I'm not sure I understand how exactly to implement it. Nevertheless I'm guessing using a test listener would be cleaner. Please correct me if I'm wrong. – Rad Jun 07 '18 at 10:04
1

I guess you could inject the EntityManager into the test and use that to set it in the EntityManagerBeanLookup.

But I think a cleaner solution is to mark the context as dirty so Spring creates a fresh one. See How to force a fresh version of the Spring context BEFORE the test executes

Jens Schauder
  • 77,657
  • 34
  • 181
  • 348
  • Yes, that was how I proved the issue. Adding it to all tests was not practical. `DirtiesContext` was not interesting either since it increases the testing time. – Rad Jun 07 '18 at 10:03
0

I ended up using a test listener (since we're already using spring-test with our test listeners):

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

}
Rad
  • 4,292
  • 8
  • 33
  • 71