Search code examples
springhibernatespring-data-jpahibernate-envers

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


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.


Solution

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