Skip to content

Shared EntityManager returned by AbstractEntityManagerFactoryBean cannot be advised by AspectJ interceptor #35974

@kzander91

Description

@kzander91

Worked in Spring 6, broken in Spring 7:

The new shared EntityManager that is returned by AbstractEntityManagerFactoryBean.getObject(EntityManager.class) is not intercepted by AspectJ interceptors.
An aspect like this:

@Aspect
@Component
class EmInterceptor {

    private static final Logger log = LoggerFactory.getLogger(EmInterceptor.class);

    @Before("execution(* jakarta.persistence.EntityManager.create*Query(..))")
    public void before() {
        log.info("Interceptor called");
    }

}

no longer applies in Spring 7, but used to apply in Spring 6.

Reproducer: demo15.zip
The project defines an @Before advice like above and invokes a Spring Data JPA repository method, and creates a query directly on the injected EntityManager. The advice logs "Interceptor called" when invoked.

Extract and run ./mvnw spring-boot:run. Note the logs:

Started Demo15Application in 1.609 seconds (process running for 1.778)
Calling repository
Interceptor called
Calling EntityManager
Interceptor called
Closing JPA EntityManagerFactory for persistence unit 'default'

Now switch to Spring Boot 4.0.0 in the pom and run again:

Started Demo15Application in 1.745 seconds (process running for 1.955)
Calling repository
Interceptor called
Calling EntityManager
Closing JPA EntityManagerFactory for persistence unit 'default'

We see that with Spring 6, the interceptor is called twice, so it applied to the EntityManager used by Spring Data JPA, and to the EntityManager obtained from the application context.
With Spring 7, only the EntityManager used by Spring Data JPA is used, but not the one obtained from the application context.

My analysis

During singleton pre-instantiation, the bean named entityManagerFactory, backed by LocalContainerEntityManagerFactoryBean is instantiated and post-processed by AbstractAutoProxyCreator here:

public @Nullable Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
if (bean != null) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
if (this.earlyBeanReferences.remove(cacheKey) != bean) {
return wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}

Note that the cacheKey is "entityManagerFactory":
Image
wrapIfNecessary doesn't find any advisors (which is correct), and we cache this decision in the advicedBeans map here:

Later, when my code calls getBean(EntityManager.class) (or, in a real application, when the EntityManager is injected into another bean), we obtain that instance through LocalContainerEntityManagerFactoryBean.getObject(EntityManager.class), which is then also passed into AbstractAutoProxyCreator:
Image
Note that the cacheKey is again "entityManagerFactory". The effect is that the cached decision that was made earlier for the factory bean is now being used for the instance created by the factory bean. Thus, advisors targeting EntityManager are skipped.


Debugging the same sequence with Spring 6 shows that the EntityManager has a different cacheKey and thus advisors are checked for eligibility:
Image

Metadata

Metadata

Assignees

Labels

in: coreIssues in core modules (aop, beans, core, context, expression)type: regressionA bug that is also a regression

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions