From b4638b82d07f09086dff037a5687eaa1157e5195 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 2 Oct 2018 12:35:49 +0100 Subject: [PATCH] Prevent ApplicationListener that depends on EMF from creating a cycle Previously, when an EntityManagerFactory was being initialized synchronously, the DataSourceSchemaCreatedEvent would be published during its initialization. This meant that an application listener that depends on the EntityManagerFactory would create a dependency cycle if it was a potential recipient of the event. For the synchronous case, this commit moves the publication of the event so that it occurs after the initialisation of the entity manager factory. This allows an application listener that is a potential recipient of the DataSourceSchemaCreatedEvent to depend on the EntityManagerFactory without creating a cycle. Closes gh-14651 --- .../jpa/DataSourceInitializedPublisher.java | 9 +- .../HibernateJpaAutoConfigurationTests.java | 93 +++++++++++++++++++ 2 files changed, 99 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/DataSourceInitializedPublisher.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/DataSourceInitializedPublisher.java index ebc24e6e1f..cc4a0c591b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/DataSourceInitializedPublisher.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/DataSourceInitializedPublisher.java @@ -82,6 +82,12 @@ class DataSourceInitializedPublisher implements BeanPostProcessor { if (bean instanceof HibernateProperties) { this.hibernateProperties = (HibernateProperties) bean; } + if (bean instanceof LocalContainerEntityManagerFactoryBean) { + LocalContainerEntityManagerFactoryBean factory = (LocalContainerEntityManagerFactoryBean) bean; + if (factory.getBootstrapExecutor() == null) { + publishEventIfRequired(factory.getNativeEntityManagerFactory()); + } + } return bean; } @@ -195,9 +201,6 @@ class DataSourceInitializedPublisher implements BeanPostProcessor { bootstrapExecutor.execute(() -> DataSourceInitializedPublisher.this .publishEventIfRequired(emf)); } - else { - DataSourceInitializedPublisher.this.publishEventIfRequired(emf); - } } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java index 3fb394edbe..975c980b9f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java @@ -19,6 +19,7 @@ package org.springframework.boot.autoconfigure.orm.jpa; import java.io.IOException; import java.net.URL; import java.net.URLClassLoader; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Enumeration; @@ -46,9 +47,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage; import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceSchemaCreatedEvent; import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfigurationTests.JpaUsingApplicationListenerConfiguration.EventCapturingApplicationListener; import org.springframework.boot.autoconfigure.orm.jpa.mapping.NonAnnotatedEntity; import org.springframework.boot.autoconfigure.orm.jpa.test.City; import org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration; @@ -57,10 +60,13 @@ import org.springframework.boot.orm.jpa.hibernate.SpringJtaPlatform; import org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ContextConsumer; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; @@ -383,6 +389,52 @@ public class HibernateJpaAutoConfigurationTests .run((context) -> assertThat(context).doesNotHaveBean(City.class)); } + @Test + public void withSyncBootstrappingAnApplicationListenerThatUsesJpaDoesNotTriggerABeanCurrentlyInCreationException() { + contextRunner() + .withUserConfiguration(JpaUsingApplicationListenerConfiguration.class) + .withPropertyValues("spring.datasource.initialization-mode=never") + .run((context) -> { + assertThat(context).hasNotFailed(); + assertThat(context + .getBean(EventCapturingApplicationListener.class).events + .stream() + .filter(DataSourceSchemaCreatedEvent.class::isInstance)) + .hasSize(1); + }); + } + + @Test + public void withAsyncBootstrappingAnApplicationListenerThatUsesJpaDoesNotTriggerABeanCurrentlyInCreationException() { + contextRunner() + .withUserConfiguration(AsyncBootstrappingConfiguration.class, + JpaUsingApplicationListenerConfiguration.class) + .withPropertyValues("spring.datasource.initialization-mode=never") + .run((context) -> { + assertThat(context).hasNotFailed(); + EventCapturingApplicationListener listener = context + .getBean(EventCapturingApplicationListener.class); + long end = System.currentTimeMillis() + 30000; + while ((System.currentTimeMillis() < end) + && !dataSourceSchemaCreatedEventReceived(listener)) { + Thread.sleep(100); + } + assertThat(listener.events.stream() + .filter(DataSourceSchemaCreatedEvent.class::isInstance)) + .hasSize(1); + }); + } + + private boolean dataSourceSchemaCreatedEventReceived( + EventCapturingApplicationListener listener) { + for (ApplicationEvent event : listener.events) { + if (event instanceof DataSourceSchemaCreatedEvent) { + return true; + } + } + return false; + } + @Configuration @TestAutoConfigurationPackage(City.class) static class TestInitializedJpaConfiguration { @@ -506,4 +558,45 @@ public class HibernateJpaAutoConfigurationTests } + @org.springframework.context.annotation.Configuration + static class JpaUsingApplicationListenerConfiguration { + + @Bean + public EventCapturingApplicationListener jpaUsingApplicationListener( + EntityManagerFactory emf) { + return new EventCapturingApplicationListener(); + } + + static class EventCapturingApplicationListener + implements ApplicationListener { + + private final List events = new ArrayList<>(); + + @Override + public void onApplicationEvent(ApplicationEvent event) { + this.events.add(event); + } + + } + + } + + @Configuration + static class AsyncBootstrappingConfiguration { + + @Bean + ThreadPoolTaskExecutor ThreadPoolTaskExecutor() { + return new ThreadPoolTaskExecutor(); + } + + @Bean + public EntityManagerFactoryBuilderCustomizer asyncBoostrappingCustomizer( + ThreadPoolTaskExecutor executor) { + return (builder) -> { + builder.setBootstrapExecutor(executor); + }; + } + + } + }