From d28cec064e9be099eb08b50b50a1971004ed57ef Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 25 Mar 2026 19:19:48 -0700 Subject: [PATCH] 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 --- .../DataJpaRepositoriesAutoConfiguration.java | 6 +- .../HibernateJpaAutoConfigurationTests.java | 6 +- .../BootstrapExecutorRequiredException.java | 69 +++++++++++++++++++ ...tstrapExecutorRequiredFailureAnalyzer.java | 50 ++++++++++++++ .../autoconfigure/JpaBaseConfiguration.java | 2 +- ...yBasedRequiredBackgroundBootstrapping.java | 60 ---------------- .../main/resources/META-INF/spring.factories | 4 ++ ...pExecutorRequiredFailureAnalyzerTests.java | 66 ++++++++++++++++++ ...dRequiredBackgroundBootstrappingTests.java | 40 ----------- 9 files changed, 196 insertions(+), 107 deletions(-) create mode 100644 module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/BootstrapExecutorRequiredException.java create mode 100644 module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/BootstrapExecutorRequiredFailureAnalyzer.java delete mode 100644 module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/PropertyBasedRequiredBackgroundBootstrapping.java create mode 100644 module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/autoconfigure/BootstrapExecutorRequiredFailureAnalyzerTests.java delete mode 100644 module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/autoconfigure/PropertyBasedRequiredBackgroundBootstrappingTests.java 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 a66405b5ac9..5bcaed3c0fc 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 @@ -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 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 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 5e0ab071723..5bbb2992cf1 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 @@ -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'")); } diff --git a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/BootstrapExecutorRequiredException.java b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/BootstrapExecutorRequiredException.java new file mode 100644 index 00000000000..f2f2dc5da55 --- /dev/null +++ b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/BootstrapExecutorRequiredException.java @@ -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); + } + +} diff --git a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/BootstrapExecutorRequiredFailureAnalyzer.java b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/BootstrapExecutorRequiredFailureAnalyzer.java new file mode 100644 index 00000000000..c0fd5557876 --- /dev/null +++ b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/BootstrapExecutorRequiredFailureAnalyzer.java @@ -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 { + + @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"; + } + +} 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 c4c14902b25..726c06f9ae9 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 @@ -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; diff --git a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/PropertyBasedRequiredBackgroundBootstrapping.java b/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/PropertyBasedRequiredBackgroundBootstrapping.java deleted file mode 100644 index 1f139a2a8c6..00000000000 --- a/module/spring-boot-jpa/src/main/java/org/springframework/boot/jpa/autoconfigure/PropertyBasedRequiredBackgroundBootstrapping.java +++ /dev/null @@ -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 { - - 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()); - } - -} diff --git a/module/spring-boot-jpa/src/main/resources/META-INF/spring.factories b/module/spring-boot-jpa/src/main/resources/META-INF/spring.factories index 6437bb94c7a..8b972a2e354 100644 --- a/module/spring-boot-jpa/src/main/resources/META-INF/spring.factories +++ b/module/spring-boot-jpa/src/main/resources/META-INF/spring.factories @@ -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 diff --git a/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/autoconfigure/BootstrapExecutorRequiredFailureAnalyzerTests.java b/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/autoconfigure/BootstrapExecutorRequiredFailureAnalyzerTests.java new file mode 100644 index 00000000000..b172256f40c --- /dev/null +++ b/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/autoconfigure/BootstrapExecutorRequiredFailureAnalyzerTests.java @@ -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. + """); + } + +} diff --git a/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/autoconfigure/PropertyBasedRequiredBackgroundBootstrappingTests.java b/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/autoconfigure/PropertyBasedRequiredBackgroundBootstrappingTests.java deleted file mode 100644 index 765b0f1fa63..00000000000 --- a/module/spring-boot-jpa/src/test/java/org/springframework/boot/jpa/autoconfigure/PropertyBasedRequiredBackgroundBootstrappingTests.java +++ /dev/null @@ -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'"); - } - -}