From 91f4bc94558e161dd9d100fdd9db209a8f55fa8b Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 23 Mar 2026 21:22:21 -0700 Subject: [PATCH] Add 'spring.jpa.bootstrap' property Add a `spring.jpa.bootstrap` property that can be set to `async` to switch on background bootstrapping in classic JPA setup. Closes gh-49733 --- .../HibernateJpaAutoConfigurationTests.java | 91 +++++++++++++++++++ .../autoconfigure/JpaBaseConfiguration.java | 5 + .../boot/jpa/autoconfigure/JpaProperties.java | 32 +++++++ 3 files changed, 128 insertions(+) diff --git a/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java b/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java index 1a38445a321..5e0ab071723 100644 --- a/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java +++ b/module/spring-boot-hibernate/src/test/java/org/springframework/boot/hibernate/autoconfigure/HibernateJpaAutoConfigurationTests.java @@ -67,6 +67,8 @@ import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage; +import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; +import org.springframework.boot.diagnostics.FailureAnalyzedException; import org.springframework.boot.flyway.autoconfigure.FlywayAutoConfiguration; import org.springframework.boot.hibernate.SpringImplicitNamingStrategy; import org.springframework.boot.hibernate.autoconfigure.HibernateJpaAutoConfigurationTests.JpaUsingApplicationListenerConfiguration.EventCapturingApplicationListener; @@ -102,6 +104,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.support.SQLExceptionTranslator; import org.springframework.jdbc.support.SQLStateSQLExceptionTranslator; @@ -247,6 +250,59 @@ class HibernateJpaAutoConfigurationTests { .run((context) -> assertThat(context).doesNotHaveBean(OpenEntityManagerInViewInterceptor.class)); } + @Test + void whenBackgroundBootstrapingAndSingleAsyncTaksExecutorConfiguresBackgroundExecutor() { + this.contextRunner.withPropertyValues("spring.jpa.bootstrap=async") + .withUserConfiguration(SingleAsyncTaskExecutorConfiguration.class) + .run((context) -> assertThat( + context.getBean(LocalContainerEntityManagerFactoryBean.class).getBootstrapExecutor()) + .isInstanceOf(SimpleAsyncTaskExecutor.class)); + } + + @Test + void whenBackgroundBootstrapingAndApplicationTaksExecutorConfiguresBackgroundExecutor() { + this.contextRunner.withPropertyValues("spring.jpa.bootstrap=async") + .withUserConfiguration(MultipleAsyncTaskExecutorsConfiguration.class, + ApplicationTaskExecutorConfiguration.class) + .run((context) -> assertThat( + context.getBean(LocalContainerEntityManagerFactoryBean.class).getBootstrapExecutor()) + .isSameAs(context.getBean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME))); + } + + @Test + void whenBackgroundBootstrapingAndMissingTaksExecutorThrowsException() { + this.contextRunner.withPropertyValues("spring.jpa.bootstrap=async") + .run((context) -> assertThat(context).getFailure() + .rootCause() + .isInstanceOf(FailureAnalyzedException.class) + .message() + .contains("bootstrap executor is required when 'spring.jpa.bootstrap' is set to 'async'")); + } + + @Test + void whenBackgroundBootstrapingAndMultipleTaksExecutorThrowsException() { + this.contextRunner.withPropertyValues("spring.jpa.bootstrap=async") + .withUserConfiguration(MultipleAsyncTaskExecutorsConfiguration.class) + .run((context) -> assertThat(context).getFailure() + .rootCause() + .isInstanceOf(FailureAnalyzedException.class) + .message() + .contains("bootstrap executor is required when 'spring.jpa.bootstrap' is set to 'async'")); + } + + @Test + void whenBackgroundBootstrapingAndCustomizedBackgroundExecutorThrowsException() { + this.contextRunner.withPropertyValues("spring.jpa.bootstrap=async") + .withBean(EntityManagerFactoryBuilderCustomizer.class, this::bootstrapExecutorCustomizer) + .run((context) -> assertThat( + context.getBean(LocalContainerEntityManagerFactoryBean.class).getBootstrapExecutor()) + .isInstanceOf(SimpleAsyncTaskExecutor.class)); + } + + private EntityManagerFactoryBuilderCustomizer bootstrapExecutorCustomizer() { + return (builder) -> builder.setBootstrapExecutor(new SimpleAsyncTaskExecutor()); + } + @Test void customJpaProperties() { this.contextRunner @@ -1383,4 +1439,39 @@ class HibernateJpaAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class SingleAsyncTaskExecutorConfiguration { + + @Bean + SimpleAsyncTaskExecutor exampleTaskExecutor() { + return new SimpleAsyncTaskExecutor(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class MultipleAsyncTaskExecutorsConfiguration { + + @Bean + SimpleAsyncTaskExecutor exampleTaskExecutor1() { + return new SimpleAsyncTaskExecutor(); + } + + @Bean + SimpleAsyncTaskExecutor exampleTaskExecutor2() { + return new SimpleAsyncTaskExecutor(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class ApplicationTaskExecutorConfiguration { + + @Bean + SimpleAsyncTaskExecutor applicationTaskExecutor() { + return new SimpleAsyncTaskExecutor(); + } + + } + } diff --git a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/JpaBaseConfiguration.java b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/JpaBaseConfiguration.java index 032d26361d7..c4c14902b25 100644 --- a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/JpaBaseConfiguration.java +++ b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/JpaBaseConfiguration.java @@ -40,6 +40,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplicat import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.jpa.EntityManagerFactoryBuilder; +import org.springframework.boot.jpa.autoconfigure.JpaProperties.Bootstrap; import org.springframework.boot.persistence.autoconfigure.EntityScanPackages; import org.springframework.boot.transaction.autoconfigure.TransactionManagerCustomizers; import org.springframework.context.annotation.Bean; @@ -127,6 +128,10 @@ public abstract class JpaBaseConfiguration { @Nullable AsyncTaskExecutor bootstrapExecutor = determineBootstrapExecutor(taskExecutors); EntityManagerFactoryBuilder builder = new EntityManagerFactoryBuilder(jpaVendorAdapter, this::buildJpaProperties, persistenceUnitManager.getIfAvailable(), null, bootstrapExecutor); + if (this.properties.getBootstrap() == Bootstrap.ASYNC) { + builder.requireBootstrapExecutor( + new PropertyBasedRequiredBackgroundBootstrapping("spring.jpa.bootstrap", "async")); + } customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); return builder; } diff --git a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/JpaProperties.java b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/JpaProperties.java index c065424b2b4..5bcbc156318 100644 --- a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/JpaProperties.java +++ b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/JpaProperties.java @@ -77,6 +77,11 @@ public class JpaProperties { */ private @Nullable Boolean openInView; + /** + * Bootstrap method to use. + */ + private Bootstrap bootstrap = Bootstrap.DEFAULT; + public Map getProperties() { return this.properties; } @@ -129,4 +134,31 @@ public class JpaProperties { this.openInView = openInView; } + public Bootstrap getBootstrap() { + return this.bootstrap; + } + + public void setBootstrap(Bootstrap bootstrap) { + this.bootstrap = bootstrap; + } + + /** + * Bootstrap methods that can be used with JPA. + */ + public enum Bootstrap { + + /** + * Default JPA bootstrapping. + */ + DEFAULT, + + /** + * Asynchronous JPA bootstrapping. The ApplicationContext must either have a + * single AsyncTaskExecutor bean, or an AsyncTaskExecutor bean named + * 'applicationTaskExecutor'. + */ + ASYNC + + } + }