Lazily resolve JPA fallback bootstrap executor#50801
Conversation
The `entityManagerFactoryBuilder` bean method injected a `Map<String, AsyncTaskExecutor>` to determine the fallback executor used for background JPA bootstrapping. A `Map` parameter is resolved eagerly, so this forced early initialization of every `AsyncTaskExecutor` bean whenever the builder was created, even when background bootstrapping was not in use. When an `AsyncTaskExecutor` directly or transitively depended on the `EntityManagerFactory`, this resulted in a `BeanCurrentlyInCreationException`. The fallback executor is now resolved lazily. `EntityManagerFactoryBuilder` holds a `Supplier` that is only invoked when background bootstrapping is actually required, and the executor is then looked up from the `BeanFactory` rather than eagerly injected. A new `Supplier`-based constructor is added for this purpose; the existing constructor that accepts an `AsyncTaskExecutor` is retained and delegates to it. The same eager `Map<String, AsyncTaskExecutor>` injection, along with an unused private method, is also removed from `DataJpaRepositoriesAutoConfiguration`. Closes spring-projectsgh-50797 Signed-off-by: Ns <397827222@qq.com>
prashantpiyush1111
left a comment
There was a problem hiding this comment.
Reviewed the lazy resolution approach for the JPA bootstrap executor. Left a few clarifying questions inline about backward compatibility and bean lazy-init behavior. Overall the fix looks solid for the reported regression.
| @Nullable PersistenceUnitManager persistenceUnitManager, @Nullable URL persistenceUnitRootLocation, | ||
| @Nullable AsyncTaskExecutor fallbackBootstrapExecutor) { | ||
| this(jpaVendorAdapter, jpaPropertiesFactory, persistenceUnitManager, persistenceUnitRootLocation, | ||
| () -> fallbackBootstrapExecutor); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
That makes sense now—thanks for explaining it clearly.
|
|
||
| private @Nullable AsyncTaskExecutor determineBootstrapExecutor(Map<String, AsyncTaskExecutor> taskExecutors) { | ||
| private @Nullable AsyncTaskExecutor determineBootstrapExecutor(ListableBeanFactory beanFactory) { | ||
| Map<String, AsyncTaskExecutor> taskExecutors = beanFactory.getBeansOfType(AsyncTaskExecutor.class); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
That's a clear distinction — thanks! Makes sense that deferring when it's called is the actual fix, not changing what getBeansOfType does internally.
| } | ||
|
|
||
| @Test | ||
| void fallbackExecutorSupplierIsNotInvokedWhenBootstrapExecutorNotRequired() { |
There was a problem hiding this comment.
This is exactly the right test to have — verifies the core behavior change (lazy invocation). Good coverage.
|
I didn't repro the issue, but I think use @Bean
@ConditionalOnMissingBean
public EntityManagerFactoryBuilder entityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter,
ObjectProvider<PersistenceUnitManager> persistenceUnitManager,
ObjectProvider<EntityManagerFactoryBuilderCustomizer> customizers,
ObjectProvider<AsyncTaskExecutor> taskExecutors,
@Qualifier(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME) Optional<AsyncTaskExecutor> applicationTaskExecutor) {
@Nullable AsyncTaskExecutor bootstrapExecutor = determineBootstrapExecutor(taskExecutors,
applicationTaskExecutor.orElse(null));
EntityManagerFactoryBuilder builder = new EntityManagerFactoryBuilder(jpaVendorAdapter,
this::buildJpaProperties, persistenceUnitManager.getIfAvailable(), null, bootstrapExecutor);
if (this.properties.getBootstrap() == Bootstrap.ASYNC) {
builder.requireBootstrapExecutor(
() -> BootstrapExecutorRequiredException.ofProperty("spring.jpa.bootstrap", "async"));
}
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
return builder;
}
private @Nullable AsyncTaskExecutor determineBootstrapExecutor(ObjectProvider<AsyncTaskExecutor> taskExecutors,
@Nullable AsyncTaskExecutor applicationTaskExecutor) {
@Nullable AsyncTaskExecutor asyncTaskExecutor = taskExecutors.getIfUnique();
return (asyncTaskExecutor != null) ? asyncTaskExecutor : applicationTaskExecutor;
} |
|
Thanks for taking a look, @quaff! You're right that The subtle part is that the regression isn't really about the injected type — it's about when the executors get resolved. In the snippet, I gave your version a try against the reproduction test in this PR ( That's why the fix defers resolution into a That said, your |
@ns3154 You are right, GH-50813 improved but not fix if there is only one |
This addresses #50797.
Broken behavior
JpaBaseConfiguration#entityManagerFactoryBuilderinjects aMap<String, AsyncTaskExecutor>parameter to pick the fallback executor used for background JPA bootstrapping. AMapmethod parameter is resolved eagerly by the container, so creating theEntityManagerFactoryBuilderforces everyAsyncTaskExecutorbean to be initialized early — even when background bootstrapping is not enabled (spring.jpa.bootstrapdefaults todefault).When an application defines an
AsyncTaskExecutorthat depends, directly or transitively, on theEntityManagerFactory, this premature initialization produces aBeanCurrentlyInCreationExceptionand the context fails to start. This is a regression in 4.1.0.Fix
The fallback executor is now resolved lazily:
EntityManagerFactoryBuilderstores the fallback as aSupplier<? extends @Nullable AsyncTaskExecutor>and only invokes it when background bootstrapping is actually required (atbuild()time). A newSupplier-based constructor is added (@since 4.1.1); the existingAsyncTaskExecutorconstructor is retained for backwards compatibility and delegates to it.JpaBaseConfiguration#entityManagerFactoryBuilderno longer injects aMap. It now takes aListableBeanFactoryand resolves the executor viagetBeansOfType(...)inside the supplier, so the lookup runs only when needed and no longer forces eager initialization.DataJpaRepositoriesAutoConfigurationcarried the same eagerMap<String, AsyncTaskExecutor>injection plus an unused private method left over from the same change; both are removed.The executor selection logic is unchanged: a single
AsyncTaskExecutoris used if exactly one is present, otherwise the one namedapplicationTaskExecutoris used.Tests
HibernateJpaAutoConfigurationTestsreproduces the dependency loop (anAsyncTaskExecutordepending on theEntityManagerFactory) and verifies the context now starts.EntityManagerFactoryBuilderTestsadds coverage for the newSupplier-based fallback, including verifying that the supplier is not invoked when background bootstrapping is not required.The default (synchronous),
spring.jpa.bootstrap=async, andspring.data.jpa.repositories.bootstrap-mode=deferredpaths were all exercised.