Replace PropertyBasedRequiredBackgroundBootstrapping with exception

Replace `PropertyBasedRequiredBackgroundBootstrapping` with a simpler
`BootstrapExecutorRequiredException` and use a regular failure analyzer
to provide help.

See gh-49733
See gh-49688
This commit is contained in:
Phillip Webb
2026-03-25 19:19:48 -07:00
parent 5b6d346c60
commit d28cec064e
9 changed files with 196 additions and 107 deletions
@@ -33,8 +33,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration;
import org.springframework.boot.data.jpa.autoconfigure.DataJpaRepositoriesAutoConfiguration.JpaRepositoriesImportSelector;
import org.springframework.boot.hibernate.autoconfigure.HibernateJpaAutoConfiguration;
import org.springframework.boot.jpa.autoconfigure.BootstrapExecutorRequiredException;
import org.springframework.boot.jpa.autoconfigure.EntityManagerFactoryBuilderCustomizer;
import org.springframework.boot.jpa.autoconfigure.PropertyBasedRequiredBackgroundBootstrapping;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportSelector;
@@ -85,8 +85,8 @@ public final class DataJpaRepositoriesAutoConfiguration {
@ConditionalOnProperty(name = "spring.data.jpa.repositories.bootstrap-mode", havingValue = "deferred")
EntityManagerFactoryBuilderCustomizer entityManagerFactoryBootstrapExecutorCustomizer(
Map<String, AsyncTaskExecutor> taskExecutors) {
return (builder) -> builder.requireBootstrapExecutor(new PropertyBasedRequiredBackgroundBootstrapping(
"spring.data.jpa.repositories.bootstrap-mode", "deferred"));
return (builder) -> builder.requireBootstrapExecutor(() -> BootstrapExecutorRequiredException
.ofProperty("spring.data.jpa.repositories.bootstrap-mode", "deferred"));
}
@Bean
@@ -68,7 +68,6 @@ 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;
@@ -82,6 +81,7 @@ import org.springframework.boot.jdbc.autoconfigure.DataSourceInitializationAutoC
import org.springframework.boot.jdbc.autoconfigure.DataSourceTransactionManagerAutoConfiguration;
import org.springframework.boot.jdbc.autoconfigure.XADataSourceAutoConfiguration;
import org.springframework.boot.jpa.EntityManagerFactoryBuilder;
import org.springframework.boot.jpa.autoconfigure.BootstrapExecutorRequiredException;
import org.springframework.boot.jpa.autoconfigure.EntityManagerFactoryBuilderCustomizer;
import org.springframework.boot.jpa.autoconfigure.JpaBaseConfiguration;
import org.springframework.boot.jpa.autoconfigure.JpaProperties;
@@ -274,7 +274,7 @@ class HibernateJpaAutoConfigurationTests {
this.contextRunner.withPropertyValues("spring.jpa.bootstrap=async")
.run((context) -> assertThat(context).getFailure()
.rootCause()
.isInstanceOf(FailureAnalyzedException.class)
.isInstanceOf(BootstrapExecutorRequiredException.class)
.message()
.contains("bootstrap executor is required when 'spring.jpa.bootstrap' is set to 'async'"));
}
@@ -285,7 +285,7 @@ class HibernateJpaAutoConfigurationTests {
.withUserConfiguration(MultipleAsyncTaskExecutorsConfiguration.class)
.run((context) -> assertThat(context).getFailure()
.rootCause()
.isInstanceOf(FailureAnalyzedException.class)
.isInstanceOf(BootstrapExecutorRequiredException.class)
.message()
.contains("bootstrap executor is required when 'spring.jpa.bootstrap' is set to 'async'"));
}
@@ -0,0 +1,69 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.jpa.autoconfigure;
import org.jspecify.annotations.Nullable;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.util.Assert;
/**
* Exception thrown then the auto-configured
* {@link LocalContainerEntityManagerFactoryBean} is missing a required bootstrap
* executor.
*
* @author Phillip Webb
* @since 4.1.0
*/
public class BootstrapExecutorRequiredException extends IllegalStateException {
private final @Nullable String propertyName;
private final @Nullable String propertyValue;
public BootstrapExecutorRequiredException(String message) {
this(message, null);
}
public BootstrapExecutorRequiredException(String message, @Nullable Throwable cause) {
this(message, null, null, cause);
}
private BootstrapExecutorRequiredException(String message, @Nullable String propertyName,
@Nullable String propertyValue, @Nullable Throwable cause) {
super(message, cause);
this.propertyName = propertyName;
this.propertyValue = propertyValue;
}
@Nullable String getPropertyName() {
return this.propertyName;
}
@Nullable String getPropertyValue() {
return this.propertyValue;
}
public static BootstrapExecutorRequiredException ofProperty(String name, String value) {
Assert.hasText(name, "'name' must not be empty");
Assert.hasText(value, "'value' must not be empty");
String message = "An EntityManagerFactoryBean bootstrap executor is required when '%s' is set to '%s'"
.formatted(name, value);
return new BootstrapExecutorRequiredException(message, name, value, null);
}
}
@@ -0,0 +1,50 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.jpa.autoconfigure;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;
import org.springframework.boot.diagnostics.FailureAnalyzer;
/**
* {@link FailureAnalyzer} for {@link BootstrapExecutorRequiredException}.
*
* @author Phillip Webb
*/
class BootstrapExecutorRequiredFailureAnalyzer extends AbstractFailureAnalyzer<BootstrapExecutorRequiredException> {
@Override
protected @Nullable FailureAnalysis analyze(Throwable rootFailure, BootstrapExecutorRequiredException cause) {
StringBuilder action = new StringBuilder();
action.append(actionPreamble(cause.getPropertyName()));
action.append("\t- With an auto-configured task executor "
+ "(you may need to set 'spring.task.execution.mode' to 'force').\n");
action.append("\t- With an AsyncTaskExecutor bean named 'applicationTaskExecutor'.\n");
action.append("\t- Using a EntityManagerFactoryBuilderCustomizer.\n");
return new FailureAnalysis(cause.getMessage(), action.toString(), cause);
}
private String actionPreamble(@Nullable String propertyName) {
return (propertyName != null)
? "Use a different '%s' value or provide a bootstrap executor using one of the following methods:\n"
.formatted(propertyName)
: "Provide a bootstrap executor using one of the following methods:\n";
}
}
@@ -130,7 +130,7 @@ public abstract class JpaBaseConfiguration {
this::buildJpaProperties, persistenceUnitManager.getIfAvailable(), null, bootstrapExecutor);
if (this.properties.getBootstrap() == Bootstrap.ASYNC) {
builder.requireBootstrapExecutor(
new PropertyBasedRequiredBackgroundBootstrapping("spring.jpa.bootstrap", "async"));
() -> BootstrapExecutorRequiredException.ofProperty("spring.jpa.bootstrap", "async"));
}
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
return builder;
@@ -1,60 +0,0 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.jpa.autoconfigure;
import java.util.function.Supplier;
import org.springframework.boot.diagnostics.FailureAnalyzedException;
import org.springframework.boot.jpa.EntityManagerFactoryBuilder;
import org.springframework.util.Assert;
/**
* {@link Supplier} to use with
* {@link EntityManagerFactoryBuilder#requireBootstrapExecutor} when a property indicates
* background bootstrapping is required.
*
* @author Phillip Webb
* @since 4.1.0
*/
public class PropertyBasedRequiredBackgroundBootstrapping implements Supplier<RuntimeException> {
private final String propertyName;
private final String propertyValue;
public PropertyBasedRequiredBackgroundBootstrapping(String propertyName, String propertyValue) {
Assert.notNull(propertyName, "'propertyName' must not be null");
Assert.notNull(propertyValue, "'propertyValue' must not be null");
this.propertyName = propertyName;
this.propertyValue = propertyValue;
}
@Override
public RuntimeException get() {
String description = "A LocalContainerEntityManagerFactoryBean bootstrap executor is required when '%s' is set to '%s'"
.formatted(this.propertyName, this.propertyValue);
StringBuilder action = new StringBuilder();
action.append("Use a different '%s' or provide a bootstrap executor using one of the following methods:\n"
.formatted(this.propertyName));
action.append("\tWith an auto-configured task executor "
+ "(you may need to set 'spring.task.execution.mode' to 'force').");
action.append("\tWith an AsyncTaskExecutor bean named 'applicationTaskExecutor.'");
action.append("\tUsing a EntityManagerFactoryBuilderCustomizer.");
return new FailureAnalyzedException(description, action.toString());
}
}
@@ -1,3 +1,7 @@
# Failure Analyzers
org.springframework.boot.diagnostics.FailureAnalyzer=\
org.springframework.boot.jpa.autoconfigure.BootstrapExecutorRequiredFailureAnalyzer
# Database Initializer Detectors
org.springframework.boot.sql.init.dependency.DatabaseInitializerDetector=\
org.springframework.boot.jpa.JpaDatabaseInitializerDetector
@@ -0,0 +1,66 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.jpa.autoconfigure;
import org.junit.jupiter.api.Test;
import org.springframework.boot.diagnostics.FailureAnalysis;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link BootstrapExecutorRequiredFailureAnalyzer}.
*
* @author Phillip Webb
*/
class BootstrapExecutorRequiredFailureAnalyzerTests {
private final BootstrapExecutorRequiredFailureAnalyzer analyzer = new BootstrapExecutorRequiredFailureAnalyzer();
@Test
void analyzeWhenBootstrapExecutorRequiredExceptionWithProperties() {
BootstrapExecutorRequiredException exception = BootstrapExecutorRequiredException.ofProperty("testname",
"testvalue");
FailureAnalysis result = this.analyzer.analyze(exception);
assertThat(result).isNotNull();
assertThat(result.getDescription()).isEqualTo(
"An EntityManagerFactoryBean bootstrap executor is required when 'testname' is set to 'testvalue'");
assertThat(result.getAction()).isEqualTo(
"""
Use a different 'testname' value or provide a bootstrap executor using one of the following methods:
- With an auto-configured task executor (you may need to set 'spring.task.execution.mode' to 'force').
- With an AsyncTaskExecutor bean named 'applicationTaskExecutor'.
- Using a EntityManagerFactoryBuilderCustomizer.
""");
}
@Test
void analyzeWhenBootstrapExecutorRequiredExceptionWithMessage() {
BootstrapExecutorRequiredException exception = new BootstrapExecutorRequiredException("A custom message");
FailureAnalysis result = this.analyzer.analyze(exception);
assertThat(result).isNotNull();
assertThat(result.getDescription()).isEqualTo("A custom message");
assertThat(result.getAction()).isEqualTo(
"""
Provide a bootstrap executor using one of the following methods:
- With an auto-configured task executor (you may need to set 'spring.task.execution.mode' to 'force').
- With an AsyncTaskExecutor bean named 'applicationTaskExecutor'.
- Using a EntityManagerFactoryBuilderCustomizer.
""");
}
}
@@ -1,40 +0,0 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.jpa.autoconfigure;
import org.junit.jupiter.api.Test;
import org.springframework.boot.diagnostics.FailureAnalyzedException;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link PropertyBasedRequiredBackgroundBootstrapping}.
*
* @author Phillip Webb
*/
class PropertyBasedRequiredBackgroundBootstrappingTests {
@Test
void getReturnsFailureAnalyzableException() {
RuntimeException exception = new PropertyBasedRequiredBackgroundBootstrapping("test.bootstrap", "true").get();
assertThat(exception).isInstanceOf(FailureAnalyzedException.class)
.hasMessage("A LocalContainerEntityManagerFactoryBean bootstrap executor is required "
+ "when 'test.bootstrap' is set to 'true'");
}
}