Merge pull request #50801 from ns3154

* gh-50797-lazy-jpa-bootstrap-executor:
  Polish "Lazily resolve JPA fallback bootstrap executor"
  Lazily resolve JPA fallback bootstrap executor

Closes gh-50801
This commit is contained in:
Stéphane Nicoll
2026-07-14 17:54:33 +02:00
5 changed files with 88 additions and 28 deletions
@@ -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.boot.jpa.autoconfigure.EntityManagerFactoryBuilderCus
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<String, AsyncTaskExecutor> taskExecutors) {
EntityManagerFactoryBuilderCustomizer entityManagerFactoryBootstrapExecutorCustomizer() {
return (builder) -> builder.requireBootstrapExecutor(() -> BootstrapExecutorRequiredException
.ofProperty("spring.data.jpa.repositories.bootstrap-mode", "deferred"));
}
@@ -94,13 +88,6 @@ public final class DataJpaRepositoriesAutoConfiguration {
return (name, definition, type) -> "org.springframework.data.jpa.util.JpaMetamodelCacheCleanup".equals(name);
}
private @Nullable AsyncTaskExecutor determineBootstrapExecutor(Map<String, AsyncTaskExecutor> 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(
@@ -104,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.AsyncTaskExecutor;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.SQLExceptionTranslator;
@@ -303,6 +304,15 @@ class HibernateJpaAutoConfigurationTests {
return (builder) -> builder.setBootstrapExecutor(new SimpleAsyncTaskExecutor());
}
@Test
void whenAsyncTaskExecutorIsDefinedInJpaDependentConfigurationDoesNotFail() {
this.contextRunner.withUserConfiguration(TaskExecutorDependingOnEntityManagerFactoryConfiguration.class)
.run((context) -> {
assertThat(context).hasNotFailed();
assertThat(context).hasSingleBean(EntityManagerFactory.class).hasSingleBean(AsyncTaskExecutor.class);
});
}
@Test
void customJpaProperties() {
this.contextRunner
@@ -1474,4 +1484,17 @@ class HibernateJpaAutoConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
static class TaskExecutorDependingOnEntityManagerFactoryConfiguration {
TaskExecutorDependingOnEntityManagerFactoryConfiguration(EntityManagerFactory entityManagerFactory) {
}
@Bean
SimpleAsyncTaskExecutor exampleTaskExecutor() {
return new SimpleAsyncTaskExecutor();
}
}
}
@@ -68,7 +68,7 @@ public class EntityManagerFactoryBuilder {
private final @Nullable URL persistenceUnitRootLocation;
private final @Nullable AsyncTaskExecutor fallbackBootstrapExecutor;
private final Supplier<? extends @Nullable AsyncTaskExecutor> fallbackBootstrapExecutor;
private @Nullable AsyncTaskExecutor bootstrapExecutor;
@@ -105,7 +105,7 @@ public class EntityManagerFactoryBuilder {
public EntityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter,
Function<DataSource, Map<String, ?>> jpaPropertiesFactory,
@Nullable PersistenceUnitManager persistenceUnitManager, @Nullable URL persistenceUnitRootLocation) {
this(jpaVendorAdapter, jpaPropertiesFactory, persistenceUnitManager, persistenceUnitRootLocation, null);
this(jpaVendorAdapter, jpaPropertiesFactory, persistenceUnitManager, persistenceUnitRootLocation, () -> null);
}
/**
@@ -121,11 +121,36 @@ public class EntityManagerFactoryBuilder {
* @param fallbackBootstrapExecutor the fallback executor to use when background
* bootstrapping is required but no explicit executor has been set
* @since 4.1.0
* @deprecated since 4.1.1 for removal in 4.3.0 in favor of
* {@link #EntityManagerFactoryBuilder(JpaVendorAdapter, Function, PersistenceUnitManager, URL, Supplier)}
*/
@Deprecated(since = "4.1.1", forRemoval = true)
public EntityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter,
Function<DataSource, Map<String, ?>> 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.
* @since 4.1.1
*/
public EntityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter,
Function<DataSource, Map<String, ?>> jpaPropertiesFactory,
@Nullable PersistenceUnitManager persistenceUnitManager, @Nullable URL persistenceUnitRootLocation,
Supplier<? extends @Nullable AsyncTaskExecutor> fallbackBootstrapExecutor) {
this.jpaVendorAdapter = jpaVendorAdapter;
this.persistenceUnitManager = persistenceUnitManager;
this.jpaPropertiesFactory = jpaPropertiesFactory;
@@ -343,8 +368,9 @@ public class EntityManagerFactoryBuilder {
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");
@@ -28,6 +28,7 @@ import org.apache.commons.logging.LogFactory;
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 abstract class JpaBaseConfiguration {
@ConditionalOnMissingBean
public EntityManagerFactoryBuilder entityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter,
ObjectProvider<PersistenceUnitManager> persistenceUnitManager,
ObjectProvider<EntityManagerFactoryBuilderCustomizer> customizers,
Map<String, AsyncTaskExecutor> taskExecutors) {
AsyncTaskExecutor bootstrapExecutor = determineBootstrapExecutor(taskExecutors);
ObjectProvider<EntityManagerFactoryBuilderCustomizer> 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 abstract class JpaBaseConfiguration {
return builder;
}
private @Nullable AsyncTaskExecutor determineBootstrapExecutor(Map<String, AsyncTaskExecutor> taskExecutors) {
private @Nullable AsyncTaskExecutor determineBootstrapExecutor(ListableBeanFactory beanFactory) {
Map<String, AsyncTaskExecutor> taskExecutors = beanFactory.getBeansOfType(AsyncTaskExecutor.class);
return (taskExecutors.size() == 1) ? taskExecutors.values().iterator().next()
: taskExecutors.get(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME);
}
@@ -18,7 +18,9 @@ package org.springframework.boot.jpa;
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;
@@ -102,7 +104,7 @@ class EntityManagerFactoryBuilderTests {
@Test
void requireBootstrapExecutorWhenFallbackExecutorProvidesExecutorDoesNotThrow() {
EntityManagerFactoryBuilder builder = createEmptyBuilder(new SimpleAsyncTaskExecutor());
EntityManagerFactoryBuilder builder = createEmptyBuilder(SimpleAsyncTaskExecutor::new);
builder.requireBootstrapExecutor(() -> new IllegalStateException("BAD"));
DataSource dataSource = mock();
assertThatNoException().isThrownBy(builder.dataSource(dataSource)::build);
@@ -125,14 +127,35 @@ class EntityManagerFactoryBuilderTests {
.withMessage("A bootstrap executor is required");
}
private EntityManagerFactoryBuilder createEmptyBuilder() {
return createEmptyBuilder(null);
@Test
void requireBootstrapExecutorWhenFallbackExecutorSupplierProvidesExecutorDoesNotThrow() {
EntityManagerFactoryBuilder builder = createEmptyBuilder(SimpleAsyncTaskExecutor::new);
builder.requireBootstrapExecutor(() -> new IllegalStateException("BAD"));
DataSource dataSource = mock();
assertThatNoException().isThrownBy(builder.dataSource(dataSource)::build);
}
private EntityManagerFactoryBuilder createEmptyBuilder(@Nullable AsyncTaskExecutor fallbackBootstrapExecutor) {
@Test
void fallbackExecutorSupplierIsNotInvokedWhenBootstrapExecutorNotRequired() {
AtomicBoolean invoked = new AtomicBoolean();
EntityManagerFactoryBuilder builder = createEmptyBuilder(() -> {
invoked.set(true);
return new SimpleAsyncTaskExecutor();
});
DataSource dataSource = mock();
builder.dataSource(dataSource).build();
assertThat(invoked).isFalse();
}
private EntityManagerFactoryBuilder createEmptyBuilder() {
return createEmptyBuilder(() -> null);
}
private EntityManagerFactoryBuilder createEmptyBuilder(
Supplier<? extends @Nullable AsyncTaskExecutor> fallbackBootstrapExecutorSupplier) {
Function<DataSource, Map<String, ?>> jpaPropertiesFactory = (dataSource) -> Collections.emptyMap();
return new EntityManagerFactoryBuilder(new TestJpaVendorAdapter(), jpaPropertiesFactory, null, null,
fallbackBootstrapExecutor);
fallbackBootstrapExecutorSupplier);
}
static class TestJpaVendorAdapter extends AbstractJpaVendorAdapter {