diff --git a/module/spring-boot-data-jpa/src/main/java/org/springframework/boot/data/jpa/autoconfigure/DataJpaRepositoriesAutoConfiguration.java b/module/spring-boot-data-jpa/src/main/java/org/springframework/boot/data/jpa/autoconfigure/DataJpaRepositoriesAutoConfiguration.java index 5bcaed3c0fc5..c4886199a283 100644 --- a/module/spring-boot-data-jpa/src/main/java/org/springframework/boot/data/jpa/autoconfigure/DataJpaRepositoriesAutoConfiguration.java +++ b/module/spring-boot-data-jpa/src/main/java/org/springframework/boot/data/jpa/autoconfigure/DataJpaRepositoriesAutoConfiguration.java @@ -16,12 +16,8 @@ package org.springframework.boot.data.jpa.autoconfigure; -import java.util.Map; - import javax.sql.DataSource; -import org.jspecify.annotations.Nullable; - import org.springframework.boot.LazyInitializationExcludeFilter; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -38,7 +34,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.ImportSelector; -import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.core.type.AnnotationMetadata; import org.springframework.data.envers.repository.config.EnableEnversRepositories; import org.springframework.data.envers.repository.support.EnversRevisionRepositoryFactoryBean; @@ -83,8 +78,7 @@ public final class DataJpaRepositoriesAutoConfiguration { @Bean @ConditionalOnProperty(name = "spring.data.jpa.repositories.bootstrap-mode", havingValue = "deferred") - EntityManagerFactoryBuilderCustomizer entityManagerFactoryBootstrapExecutorCustomizer( - Map taskExecutors) { + EntityManagerFactoryBuilderCustomizer entityManagerFactoryBootstrapExecutorCustomizer() { return (builder) -> builder.requireBootstrapExecutor(() -> BootstrapExecutorRequiredException .ofProperty("spring.data.jpa.repositories.bootstrap-mode", "deferred")); } @@ -94,13 +88,6 @@ static LazyInitializationExcludeFilter eagerJpaMetamodelCacheCleanup() { return (name, definition, type) -> "org.springframework.data.jpa.util.JpaMetamodelCacheCleanup".equals(name); } - private @Nullable AsyncTaskExecutor determineBootstrapExecutor(Map taskExecutors) { - if (taskExecutors.size() == 1) { - return taskExecutors.values().iterator().next(); - } - return taskExecutors.get(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME); - } - static class JpaRepositoriesImportSelector implements ImportSelector { private static final boolean ENVERS_AVAILABLE = ClassUtils.isPresent( 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 5bbb2992cf15..5a6329ac679d 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 @@ -303,6 +303,15 @@ private EntityManagerFactoryBuilderCustomizer bootstrapExecutorCustomizer() { return (builder) -> builder.setBootstrapExecutor(new SimpleAsyncTaskExecutor()); } + @Test + void whenAsyncTaskExecutorIsDefinedInJpaDependentConfigurationDoesNotTriggerABeanCurrentlyInCreationException() { + this.contextRunner.withUserConfiguration(AsyncTaskExecutorDependingOnEntityManagerFactoryConfiguration.class) + .run((context) -> { + assertThat(context).hasNotFailed(); + assertThat(context).hasSingleBean(EntityManagerFactory.class); + }); + } + @Test void customJpaProperties() { this.contextRunner @@ -1474,4 +1483,17 @@ SimpleAsyncTaskExecutor applicationTaskExecutor() { } + @Configuration(proxyBeanMethods = false) + static class AsyncTaskExecutorDependingOnEntityManagerFactoryConfiguration { + + AsyncTaskExecutorDependingOnEntityManagerFactoryConfiguration(EntityManagerFactory entityManagerFactory) { + } + + @Bean + SimpleAsyncTaskExecutor exampleTaskExecutor() { + return new SimpleAsyncTaskExecutor(); + } + + } + } diff --git a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java index 65a56b7e13fa..46b8f87e00b4 100644 --- a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java +++ b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/EntityManagerFactoryBuilder.java @@ -68,7 +68,7 @@ public class EntityManagerFactoryBuilder { private final @Nullable URL persistenceUnitRootLocation; - private final @Nullable AsyncTaskExecutor fallbackBootstrapExecutor; + private final Supplier fallbackBootstrapExecutor; private @Nullable AsyncTaskExecutor bootstrapExecutor; @@ -105,7 +105,7 @@ public EntityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter, public EntityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter, Function> jpaPropertiesFactory, @Nullable PersistenceUnitManager persistenceUnitManager, @Nullable URL persistenceUnitRootLocation) { - this(jpaVendorAdapter, jpaPropertiesFactory, persistenceUnitManager, persistenceUnitRootLocation, null); + this(jpaVendorAdapter, jpaPropertiesFactory, persistenceUnitManager, persistenceUnitRootLocation, () -> null); } /** @@ -126,6 +126,29 @@ public EntityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter, Function> jpaPropertiesFactory, @Nullable PersistenceUnitManager persistenceUnitManager, @Nullable URL persistenceUnitRootLocation, @Nullable AsyncTaskExecutor fallbackBootstrapExecutor) { + this(jpaVendorAdapter, jpaPropertiesFactory, persistenceUnitManager, persistenceUnitRootLocation, + () -> fallbackBootstrapExecutor); + } + + /** + * Create a new instance passing in the common pieces that will be shared if multiple + * EntityManagerFactory instances are created. + * @param jpaVendorAdapter a vendor adapter + * @param jpaPropertiesFactory the JPA properties to be passed to the persistence + * provider, based on the {@linkplain #dataSource(DataSource) configured data source} + * @param persistenceUnitManager optional source of persistence unit information (can + * be null) + * @param persistenceUnitRootLocation the persistence unit root location to use as a + * fallback or {@code null} + * @param fallbackBootstrapExecutor a supplier of the fallback executor to use when + * background bootstrapping is required but no explicit executor has been set. The + * supplier is only invoked when background bootstrapping is actually required. + * @since 4.1.1 + */ + public EntityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter, + Function> jpaPropertiesFactory, + @Nullable PersistenceUnitManager persistenceUnitManager, @Nullable URL persistenceUnitRootLocation, + Supplier fallbackBootstrapExecutor) { this.jpaVendorAdapter = jpaVendorAdapter; this.persistenceUnitManager = persistenceUnitManager; this.jpaPropertiesFactory = jpaPropertiesFactory; @@ -343,8 +366,9 @@ private Map jpaPropertyMap() { return EntityManagerFactoryBuilder.this.bootstrapExecutor; } if (EntityManagerFactoryBuilder.this.requireBootstrapExecutorExceptionSupplier != null) { - if (EntityManagerFactoryBuilder.this.fallbackBootstrapExecutor != null) { - return EntityManagerFactoryBuilder.this.fallbackBootstrapExecutor; + @Nullable AsyncTaskExecutor fallback = EntityManagerFactoryBuilder.this.fallbackBootstrapExecutor.get(); + if (fallback != null) { + return fallback; } RuntimeException ex = EntityManagerFactoryBuilder.this.requireBootstrapExecutorExceptionSupplier.get(); throw (ex != null) ? ex : new IllegalStateException("A bootstrap executor is required"); 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 726c06f9ae99..81bd01579362 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 @@ -28,6 +28,7 @@ import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfigurationPackages; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -123,11 +124,10 @@ public JpaVendorAdapter jpaVendorAdapter() { @ConditionalOnMissingBean public EntityManagerFactoryBuilder entityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter, ObjectProvider persistenceUnitManager, - ObjectProvider customizers, - Map taskExecutors) { - @Nullable AsyncTaskExecutor bootstrapExecutor = determineBootstrapExecutor(taskExecutors); + ObjectProvider customizers, ListableBeanFactory beanFactory) { EntityManagerFactoryBuilder builder = new EntityManagerFactoryBuilder(jpaVendorAdapter, - this::buildJpaProperties, persistenceUnitManager.getIfAvailable(), null, bootstrapExecutor); + this::buildJpaProperties, persistenceUnitManager.getIfAvailable(), null, + () -> determineBootstrapExecutor(beanFactory)); if (this.properties.getBootstrap() == Bootstrap.ASYNC) { builder.requireBootstrapExecutor( () -> BootstrapExecutorRequiredException.ofProperty("spring.jpa.bootstrap", "async")); @@ -136,7 +136,8 @@ public EntityManagerFactoryBuilder entityManagerFactoryBuilder(JpaVendorAdapter return builder; } - private @Nullable AsyncTaskExecutor determineBootstrapExecutor(Map taskExecutors) { + private @Nullable AsyncTaskExecutor determineBootstrapExecutor(ListableBeanFactory beanFactory) { + Map taskExecutors = beanFactory.getBeansOfType(AsyncTaskExecutor.class); return (taskExecutors.size() == 1) ? taskExecutors.values().iterator().next() : taskExecutors.get(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME); } diff --git a/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/EntityManagerFactoryBuilderTests.java b/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/EntityManagerFactoryBuilderTests.java index 58b584f27951..0ac3a1616afa 100644 --- a/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/EntityManagerFactoryBuilderTests.java +++ b/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/EntityManagerFactoryBuilderTests.java @@ -18,7 +18,9 @@ import java.util.Collections; import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; +import java.util.function.Supplier; import javax.sql.DataSource; @@ -125,6 +127,26 @@ void requireBootstrapExecutorWhenSupplierReturnsNullExecutorAndNoFallbackExecuto .withMessage("A bootstrap executor is required"); } + @Test + void requireBootstrapExecutorWhenFallbackExecutorSupplierProvidesExecutorDoesNotThrow() { + EntityManagerFactoryBuilder builder = createEmptyBuilderWithFallbackSupplier(SimpleAsyncTaskExecutor::new); + builder.requireBootstrapExecutor(() -> new IllegalStateException("BAD")); + DataSource dataSource = mock(); + assertThatNoException().isThrownBy(builder.dataSource(dataSource)::build); + } + + @Test + void fallbackExecutorSupplierIsNotInvokedWhenBootstrapExecutorNotRequired() { + AtomicBoolean invoked = new AtomicBoolean(); + EntityManagerFactoryBuilder builder = createEmptyBuilderWithFallbackSupplier(() -> { + invoked.set(true); + return new SimpleAsyncTaskExecutor(); + }); + DataSource dataSource = mock(); + builder.dataSource(dataSource).build(); + assertThat(invoked).isFalse(); + } + private EntityManagerFactoryBuilder createEmptyBuilder() { return createEmptyBuilder(null); } @@ -135,6 +157,13 @@ private EntityManagerFactoryBuilder createEmptyBuilder(@Nullable AsyncTaskExecut fallbackBootstrapExecutor); } + private EntityManagerFactoryBuilder createEmptyBuilderWithFallbackSupplier( + Supplier fallbackBootstrapExecutorSupplier) { + Function> jpaPropertiesFactory = (dataSource) -> Collections.emptyMap(); + return new EntityManagerFactoryBuilder(new TestJpaVendorAdapter(), jpaPropertiesFactory, null, null, + fallbackBootstrapExecutorSupplier); + } + static class TestJpaVendorAdapter extends AbstractJpaVendorAdapter { @Override