-
Notifications
You must be signed in to change notification settings - Fork 38.9k
Description
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:
Lines 286 to 294 in b038beb
| 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":
wrapIfNecessary doesn't find any advisors (which is correct), and we cache this decision in the advicedBeans map here: Line 347 in b038beb
| this.advisedBeans.put(cacheKey, Boolean.FALSE); |
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:

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:
