Skip to content

Commit b4638b8

Browse files
committed
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
1 parent 7cbee70 commit b4638b8

File tree

2 files changed

+99
-3
lines changed

2 files changed

+99
-3
lines changed

Diff for: spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/DataSourceInitializedPublisher.java

+6-3
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,12 @@ public Object postProcessAfterInitialization(Object bean, String beanName)
8282
if (bean instanceof HibernateProperties) {
8383
this.hibernateProperties = (HibernateProperties) bean;
8484
}
85+
if (bean instanceof LocalContainerEntityManagerFactoryBean) {
86+
LocalContainerEntityManagerFactoryBean factory = (LocalContainerEntityManagerFactoryBean) bean;
87+
if (factory.getBootstrapExecutor() == null) {
88+
publishEventIfRequired(factory.getNativeEntityManagerFactory());
89+
}
90+
}
8591
return bean;
8692
}
8793

@@ -195,9 +201,6 @@ public void postProcessEntityManagerFactory(EntityManagerFactory emf) {
195201
bootstrapExecutor.execute(() -> DataSourceInitializedPublisher.this
196202
.publishEventIfRequired(emf));
197203
}
198-
else {
199-
DataSourceInitializedPublisher.this.publishEventIfRequired(emf);
200-
}
201204
}
202205

203206
}

Diff for: spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java

+93
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.io.IOException;
2020
import java.net.URL;
2121
import java.net.URLClassLoader;
22+
import java.util.ArrayList;
2223
import java.util.Arrays;
2324
import java.util.Collections;
2425
import java.util.Enumeration;
@@ -46,9 +47,11 @@
4647
import org.springframework.boot.autoconfigure.AutoConfigurations;
4748
import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage;
4849
import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration;
50+
import org.springframework.boot.autoconfigure.jdbc.DataSourceSchemaCreatedEvent;
4951
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
5052
import org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration;
5153
import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration;
54+
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfigurationTests.JpaUsingApplicationListenerConfiguration.EventCapturingApplicationListener;
5255
import org.springframework.boot.autoconfigure.orm.jpa.mapping.NonAnnotatedEntity;
5356
import org.springframework.boot.autoconfigure.orm.jpa.test.City;
5457
import org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration;
@@ -57,10 +60,13 @@
5760
import org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy;
5861
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
5962
import org.springframework.boot.test.context.runner.ContextConsumer;
63+
import org.springframework.context.ApplicationEvent;
64+
import org.springframework.context.ApplicationListener;
6065
import org.springframework.context.annotation.Bean;
6166
import org.springframework.context.annotation.Configuration;
6267
import org.springframework.orm.jpa.JpaTransactionManager;
6368
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
69+
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
6470

6571
import static org.assertj.core.api.Assertions.assertThat;
6672
import static org.assertj.core.api.Assertions.entry;
@@ -383,6 +389,52 @@ public void hibernatePropertiesCustomizerCanDisableBeanContainer() {
383389
.run((context) -> assertThat(context).doesNotHaveBean(City.class));
384390
}
385391

392+
@Test
393+
public void withSyncBootstrappingAnApplicationListenerThatUsesJpaDoesNotTriggerABeanCurrentlyInCreationException() {
394+
contextRunner()
395+
.withUserConfiguration(JpaUsingApplicationListenerConfiguration.class)
396+
.withPropertyValues("spring.datasource.initialization-mode=never")
397+
.run((context) -> {
398+
assertThat(context).hasNotFailed();
399+
assertThat(context
400+
.getBean(EventCapturingApplicationListener.class).events
401+
.stream()
402+
.filter(DataSourceSchemaCreatedEvent.class::isInstance))
403+
.hasSize(1);
404+
});
405+
}
406+
407+
@Test
408+
public void withAsyncBootstrappingAnApplicationListenerThatUsesJpaDoesNotTriggerABeanCurrentlyInCreationException() {
409+
contextRunner()
410+
.withUserConfiguration(AsyncBootstrappingConfiguration.class,
411+
JpaUsingApplicationListenerConfiguration.class)
412+
.withPropertyValues("spring.datasource.initialization-mode=never")
413+
.run((context) -> {
414+
assertThat(context).hasNotFailed();
415+
EventCapturingApplicationListener listener = context
416+
.getBean(EventCapturingApplicationListener.class);
417+
long end = System.currentTimeMillis() + 30000;
418+
while ((System.currentTimeMillis() < end)
419+
&& !dataSourceSchemaCreatedEventReceived(listener)) {
420+
Thread.sleep(100);
421+
}
422+
assertThat(listener.events.stream()
423+
.filter(DataSourceSchemaCreatedEvent.class::isInstance))
424+
.hasSize(1);
425+
});
426+
}
427+
428+
private boolean dataSourceSchemaCreatedEventReceived(
429+
EventCapturingApplicationListener listener) {
430+
for (ApplicationEvent event : listener.events) {
431+
if (event instanceof DataSourceSchemaCreatedEvent) {
432+
return true;
433+
}
434+
}
435+
return false;
436+
}
437+
386438
@Configuration
387439
@TestAutoConfigurationPackage(City.class)
388440
static class TestInitializedJpaConfiguration {
@@ -506,4 +558,45 @@ public Enumeration<URL> getResources(String name) throws IOException {
506558

507559
}
508560

561+
@org.springframework.context.annotation.Configuration
562+
static class JpaUsingApplicationListenerConfiguration {
563+
564+
@Bean
565+
public EventCapturingApplicationListener jpaUsingApplicationListener(
566+
EntityManagerFactory emf) {
567+
return new EventCapturingApplicationListener();
568+
}
569+
570+
static class EventCapturingApplicationListener
571+
implements ApplicationListener<ApplicationEvent> {
572+
573+
private final List<ApplicationEvent> events = new ArrayList<>();
574+
575+
@Override
576+
public void onApplicationEvent(ApplicationEvent event) {
577+
this.events.add(event);
578+
}
579+
580+
}
581+
582+
}
583+
584+
@Configuration
585+
static class AsyncBootstrappingConfiguration {
586+
587+
@Bean
588+
ThreadPoolTaskExecutor ThreadPoolTaskExecutor() {
589+
return new ThreadPoolTaskExecutor();
590+
}
591+
592+
@Bean
593+
public EntityManagerFactoryBuilderCustomizer asyncBoostrappingCustomizer(
594+
ThreadPoolTaskExecutor executor) {
595+
return (builder) -> {
596+
builder.setBootstrapExecutor(executor);
597+
};
598+
}
599+
600+
}
601+
509602
}

0 commit comments

Comments
 (0)