Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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"));
}
Expand All @@ -94,13 +88,6 @@ static LazyInitializationExcludeFilter eagerJpaMetamodelCacheCleanup() {
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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1474,4 +1483,17 @@ SimpleAsyncTaskExecutor applicationTaskExecutor() {

}

@Configuration(proxyBeanMethods = false)
static class AsyncTaskExecutorDependingOnEntityManagerFactoryConfiguration {

AsyncTaskExecutorDependingOnEntityManagerFactoryConfiguration(EntityManagerFactory entityManagerFactory) {
}

@Bean
SimpleAsyncTaskExecutor exampleTaskExecutor() {
return new SimpleAsyncTaskExecutor();
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -105,7 +105,7 @@ public EntityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter,
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);
}

/**
Expand All @@ -126,6 +126,29 @@ public EntityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter,
Function<DataSource, Map<String, ?>> jpaPropertiesFactory,
@Nullable PersistenceUnitManager persistenceUnitManager, @Nullable URL persistenceUnitRootLocation,
@Nullable AsyncTaskExecutor fallbackBootstrapExecutor) {
this(jpaVendorAdapter, jpaPropertiesFactory, persistenceUnitManager, persistenceUnitRootLocation,
() -> fallbackBootstrapExecutor);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice approach wrapping the old AsyncTaskExecutor param into a Supplier for backward compatibility. Just confirming — since this old constructor still accepts a concrete AsyncTaskExecutor (not a Supplier), doesn't that mean any caller using this old constructor still eagerly resolves the executor before passing it in? The laziness benefit only applies to callers using the new Supplier-based constructor, right?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right — a caller using the AsyncTaskExecutor constructor still resolves the executor itself before passing it in, so the laziness only benefits callers of the new Supplier-based constructor. That's intentional: it's the released @since 4.1.0 public API, and a caller of it already holds a concrete executor instance, so there is nothing to defer there.

The regression wasn't caused by this constructor, but by JpaBaseConfiguration injecting a Map<String, AsyncTaskExecutor> method parameter, which forced eager creation of every executor bean while the entityManagerFactoryBuilder bean was being created. The actual fix is that the auto-configuration now uses the new Supplier-based constructor and resolves the executor lazily; the old constructor is retained purely for backward compatibility.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense now—thanks for explaining it clearly.

}

/**
* 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<DataSource, Map<String, ?>> jpaPropertiesFactory,
@Nullable PersistenceUnitManager persistenceUnitManager, @Nullable URL persistenceUnitRootLocation,
Supplier<? extends @Nullable AsyncTaskExecutor> fallbackBootstrapExecutor) {
this.jpaVendorAdapter = jpaVendorAdapter;
this.persistenceUnitManager = persistenceUnitManager;
this.jpaPropertiesFactory = jpaPropertiesFactory;
Expand Down Expand Up @@ -343,8 +366,9 @@ private Map<String, Object> 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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -123,11 +124,10 @@ public JpaVendorAdapter jpaVendorAdapter() {
@ConditionalOnMissingBean
public EntityManagerFactoryBuilder entityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter,
ObjectProvider<PersistenceUnitManager> persistenceUnitManager,
ObjectProvider<EntityManagerFactoryBuilderCustomizer> customizers,
Map<String, AsyncTaskExecutor> taskExecutors) {
@Nullable 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"));
Expand All @@ -136,7 +136,8 @@ public EntityManagerFactoryBuilder entityManagerFactoryBuilder(JpaVendorAdapter
return builder;
}

private @Nullable AsyncTaskExecutor determineBootstrapExecutor(Map<String, AsyncTaskExecutor> taskExecutors) {
private @Nullable AsyncTaskExecutor determineBootstrapExecutor(ListableBeanFactory beanFactory) {
Map<String, AsyncTaskExecutor> taskExecutors = beanFactory.getBeansOfType(AsyncTaskExecutor.class);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good fix moving this behind a Supplier. One minor question: getBeansOfType(AsyncTaskExecutor.class) — does this also trigger eager initialization of any AsyncTaskExecutor beans that are themselves lazy-init? Or does getBeansOfType respect @lazy on individual beans?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. getBeansOfType(AsyncTaskExecutor.class) does use allowEagerInit=true by default, so it will instantiate the matching beans when it runs — that part is unchanged. What changed is when it runs: the lookup is now wrapped in the Supplier, which is only invoked when background bootstrapping is actually required (at build() time, and only when requireBootstrapExecutor was set — i.e. spring.jpa.bootstrap=async or spring.data.jpa.repositories.bootstrap-mode=deferred).

The original regression came from resolving the executors eagerly as a Map method parameter while the entityManagerFactoryBuilder bean itself was still being created (during JPA infrastructure setup), which is what produced the dependency cycle. Deferring the lookup to bootstrap time means a @Lazy applicationTaskExecutor is created only when it is genuinely needed, which is consistent with @Lazy's intent and avoids the early cycle.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a clear distinction — thanks! Makes sense that deferring when it's called is the actual fix, not changing what getBeansOfType does internally.

return (taskExecutors.size() == 1) ? taskExecutors.values().iterator().next()
: taskExecutors.get(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is exactly the right test to have — verifies the core behavior change (lazy invocation). Good coverage.

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);
}
Expand All @@ -135,6 +157,13 @@ private EntityManagerFactoryBuilder createEmptyBuilder(@Nullable AsyncTaskExecut
fallbackBootstrapExecutor);
}

private EntityManagerFactoryBuilder createEmptyBuilderWithFallbackSupplier(
Supplier<? extends @Nullable AsyncTaskExecutor> fallbackBootstrapExecutorSupplier) {
Function<DataSource, Map<String, ?>> jpaPropertiesFactory = (dataSource) -> Collections.emptyMap();
return new EntityManagerFactoryBuilder(new TestJpaVendorAdapter(), jpaPropertiesFactory, null, null,
fallbackBootstrapExecutorSupplier);
}

static class TestJpaVendorAdapter extends AbstractJpaVendorAdapter {

@Override
Expand Down