Reproducible with the latest Spring Data (Neumann-SR4) and Spring Framework 5.2.9. Not reproducible with the latest Spring Data (Neumann-SR4) and Spring Framework 5.2.5. Looks like a regression that was introduced by fixing https://github.com/spring-projects/spring-framework/issues/24852
The reproducer: https://github.com/iherasymenko/spring-data-jpa-child-context-pollution-reproducer.
Run all tests by executing gradlew test
— one test will fail.
Run gradlew test --tests com.example.Config2Test
and then gradlew test --tests com.example.ConfigTest
— both tests will succeed.
Comment From: iherasymenko
Created per the ask from Jens Schauder.
Comment From: schauder
This is a migrated issue from https://jira.spring.io/browse/DATAJPA-1797
Comment From: sbrannen
Tentatively slated for 5.3 GA to ensure this potential regression gets triaged in a timely manner.
Comment From: sbrannen
@iherasymenko, thank you for providing the example project. That's very helpful.
I can confirm that there is a change in behavior from Spring Framework 5.2.5 to 5.2.6, though I have not yet determined the root cause.
I'll post back with my findings later.
Comment From: iherasymenko
@sbrannen no problem. Thanks for starting looking into it so quickly. I did some bisecting myself prior to filling an issue and it seems like this commit has introduced the regression.
Comment From: sbrannen
Summary
The original title of this issue almost gets it right: Spring Data JPA pollutes child contexts with SharedEntityManagerCreator.
It turns out, however, that the inverse is true, and I have changed the title accordingly.
Spring Data JPA (SDJ) pollutes a parent ApplicationContext
with multiple SharedEntityManagerCreator
bean registrations if multiple child contexts are created with @Configuration
classes annotated with @EnableJpaRepositories
.
The easiest workaround for this in tests that suffer from this is to annotate each such test class with @DirtiesContext
. That makes sense since @EnableJpaRepositories
effectively dirties the parent context in such a scenario.
Details
One of the effects of annotating a @Configuration
class with @EnableJpaRepositories
is that SDJ will register an EntityManagerBeanDefinitionRegistrarPostProcessor
bean in the current ApplicationContext
. The EntityManagerBeanDefinitionRegistrarPostProcessor
will then register a SharedEntityManagerCreator
bean in the ApplicationContext
in the context hierarchy which declares infrastructure beans for an EntityManagerFactory
. In the tests in the provided example, that context happens to be the parent ApplicationContext
. When doing so, EntityManagerBeanDefinitionRegistrarPostProcessor
does not check for the existence of a SharedEntityManagerCreator
but rather always registers such a bean with a generated name using BeanDefinitionReaderUtils.registerWithGeneratedName()
.
If the @Autowired EntityManager entityManager
fields are commented out or removed from the test classes in the provided example and the test methods are modified as follows...
@RunWith(SpringRunner.class)
@ContextHierarchy({
@ContextConfiguration(classes = RootConfiguration.class),
@ContextConfiguration(classes = Config2Test.Config.class)
})
public class Config2Test {
@Autowired
ApplicationContext context;
@Test
public void testSomething() {
System.out.println("=== " + getClass().getSimpleName() + " ========================================");
context.getParent().getBeansOfType(EntityManager.class, true, false).entrySet()//
.forEach(entry -> System.out.println(entry.getKey() + " : " + entry.getValue()));
}
// @Configuration class
}
... then we see output similar to the following.
=== Config1Test ========================================
org.springframework.orm.jpa.SharedEntityManagerCreator#0 : Shared EntityManager proxy for target factory [org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean@3bae64d0]
=== Config2Test ========================================
org.springframework.orm.jpa.SharedEntityManagerCreator#0 : Shared EntityManager proxy for target factory [org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean@3bae64d0]
org.springframework.orm.jpa.SharedEntityManagerCreator#1 : Shared EntityManager proxy for target factory [org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean@3bae64d0]
=== Config3Test ========================================
org.springframework.orm.jpa.SharedEntityManagerCreator#0 : Shared EntityManager proxy for target factory [org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean@3bae64d0]
org.springframework.orm.jpa.SharedEntityManagerCreator#1 : Shared EntityManager proxy for target factory [org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean@3bae64d0]
org.springframework.orm.jpa.SharedEntityManagerCreator#2 : Shared EntityManager proxy for target factory [org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean@3bae64d0]
You can see that a SharedEntityManagerCreator
for the exact same LocalContainerEntityManagerFactoryBean
instance is registered in the parent context for each child context. Note that I introduced a third test class to better demonstrate the issue.
The reason the tests passed prior to Spring Framework 5.2.6 is that autowiring by type for the @Autowired EntityManager
field only worked due to a bug in DefaultListableBeanFactory
's implementation of getBeanNamesForType(Class<?>, boolean, boolean)
. Specifically, getBeanNamesForType()
maintained a stale by-type cache in which only the first SharedEntityManagerCreator
was cached. Thus there was only one autowire candidate. That bug was fixed in 5.2.6 in conjunction with #24852. After the fix, the example tests start to fail because there are now correctly two autowire candidates for the @Autowired EntityManager
field, and that results in the following exception.
org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'javax.persistence.EntityManager' available: expected single matching bean but found 2: org.springframework.orm.jpa.SharedEntityManagerCreator#0,org.springframework.orm.jpa.SharedEntityManagerCreator#1
at org.springframework.beans.factory.config.DependencyDescriptor.resolveNotUnique(DependencyDescriptor.java:220)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1284)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1226)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:640)
... 38 more
Recommendations
- For developers encountering this issue in integration tests using
@ContextHierarchy
, there is a simple workaround: annotate each such test class with@DirtiesContext
. That will remove all application contexts that make up the context hierarchy after each test class, thereby avoiding pollution of the parent context. - The Spring Data JPA team should consider fixing this. We can foresee at least two options here:
- Use a well-defined bean name for the
SharedEntityManagerCreator
instead of a generated unique name. - Instead of directly using
BeanDefinitionReaderUtils.registerWithGeneratedName()
to register theSharedEntityManagerCreator
, SDJ could detect if there is already aSharedEntityManagerCreator
bean with an identical bean definition (excluding the bean name) before creating another one with a different generated name.
In light of the above we are closing this issue and recommending that https://jira.spring.io/browse/DATAJPA-1797 be reopened.
Comment From: iherasymenko
Thank you very much, @sbrannen. I pinged @schauder on the Spring Data Jira.