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

  1. 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.
  2. The Spring Data JPA team should consider fixing this. We can foresee at least two options here:
  3. Use a well-defined bean name for the SharedEntityManagerCreator instead of a generated unique name.
  4. Instead of directly using BeanDefinitionReaderUtils.registerWithGeneratedName() to register the SharedEntityManagerCreator, SDJ could detect if there is already a SharedEntityManagerCreator 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.