From a86aa4330338484ad5a0aff14e16d4b890cedbf5 Mon Sep 17 00:00:00 2001 From: Hyunwoo Jung Date: Fri, 25 Sep 2026 02:08:26 +0900 Subject: [PATCH] Allow properties migrator to fail application startup Add a new configuration property to prevent the application from starting when configuration keys that need to be migrated are found. With `on-error`, the application fails when keys that are no longer supported are found. With `on-warning`, it also fails when keys that have been renamed are found. The default, `never`, keeps the current behavior of only logging the report. See gh-51855 Signed-off-by: Hyunwoo Jung --- .../DocumentConfigurationProperties.java | 1 + .../build.gradle | 1 + .../migrator/PropertiesMigrationListener.java | 54 +++++++++++++++++-- .../spring-configuration-metadata.json | 11 ++++ .../PropertiesMigrationListenerTests.java | 39 ++++++++++++++ .../antora/modules/ROOT/pages/upgrading.adoc | 8 +++ 6 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 core/spring-boot-properties-migrator/src/main/resources/META-INF/spring-configuration-metadata.json diff --git a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java index eee7ebd81af..a07ea58611b 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java @@ -106,6 +106,7 @@ public abstract class DocumentConfigurationProperties extends DefaultTask { config.accept("spring.ssl"); config.accept("spring.task"); config.accept("spring.threads"); + config.accept("spring.tools.properties-migrator"); config.accept("spring.validation"); config.accept("spring.mandatory-file-encoding"); config.accept("info"); diff --git a/core/spring-boot-properties-migrator/build.gradle b/core/spring-boot-properties-migrator/build.gradle index b30463c15de..a658dae6a6a 100644 --- a/core/spring-boot-properties-migrator/build.gradle +++ b/core/spring-boot-properties-migrator/build.gradle @@ -16,6 +16,7 @@ plugins { id "java" + id "org.springframework.boot.configuration-metadata" id "org.springframework.boot.deployed" } diff --git a/core/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationListener.java b/core/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationListener.java index 1522b25debd..a1b8803a36a 100644 --- a/core/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationListener.java +++ b/core/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationListener.java @@ -18,6 +18,7 @@ package org.springframework.boot.context.properties.migrator; import java.io.IOException; import java.io.InputStream; +import java.util.Locale; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -29,6 +30,7 @@ import org.springframework.boot.context.event.ApplicationFailedEvent; import org.springframework.boot.context.event.ApplicationPreparedEvent; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.boot.context.event.SpringApplicationEvent; +import org.springframework.boot.context.properties.bind.Binder; import org.springframework.context.ApplicationListener; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.io.Resource; @@ -37,14 +39,18 @@ import org.springframework.core.io.support.PathMatchingResourcePatternResolver; /** * An {@link ApplicationListener} that inspects the {@link ConfigurableEnvironment * environment} for configuration keys that need to be migrated. Automatically renames the - * keys that have a matching replacement and logs a report of what was discovered. + * keys that have a matching replacement and logs a report of what was discovered. Can + * optionally fail application startup when such keys are found. * * @author Stephane Nicoll + * @author Hyunwoo Jung */ class PropertiesMigrationListener implements ApplicationListener { private static final Log logger = LogFactory.getLog(PropertiesMigrationListener.class); + private static final String FAIL_PROPERTY = "spring.tools.properties-migrator.fail"; + private @Nullable PropertiesMigrationReport report; private boolean reported; @@ -60,10 +66,11 @@ class PropertiesMigrationListener implements ApplicationListener false; + case ON_ERROR -> report.getErrorReport() != null; + case ON_WARNING -> report.getErrorReport() != null || report.getWarningReport() != null; + }; + } + + /** + * Conditions under which property migration should cause application startup to fail. + */ + enum Fail { + + /** + * Do not fail application startup due to migration warnings or errors. + */ + NEVER, + + /** + * Fail application startup if the migration report contains errors. + */ + ON_ERROR, + + /** + * Fail application startup if the migration report contains warnings or errors. + */ + ON_WARNING + + } + } diff --git a/core/spring-boot-properties-migrator/src/main/resources/META-INF/spring-configuration-metadata.json b/core/spring-boot-properties-migrator/src/main/resources/META-INF/spring-configuration-metadata.json new file mode 100644 index 00000000000..d48f7b2461b --- /dev/null +++ b/core/spring-boot-properties-migrator/src/main/resources/META-INF/spring-configuration-metadata.json @@ -0,0 +1,11 @@ +{ + "properties": [ + { + "name": "spring.tools.properties-migrator.fail", + "type": "org.springframework.boot.context.properties.migrator.PropertiesMigrationListener$Fail", + "description": "When the application should fail to start because of configuration keys that need to be migrated.", + "sourceType": "org.springframework.boot.context.properties.migrator.PropertiesMigrationListener", + "defaultValue": "never" + } + ] +} diff --git a/core/spring-boot-properties-migrator/src/test/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationListenerTests.java b/core/spring-boot-properties-migrator/src/test/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationListenerTests.java index fb4d91466d6..364d201a539 100644 --- a/core/spring-boot-properties-migrator/src/test/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationListenerTests.java +++ b/core/spring-boot-properties-migrator/src/test/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationListenerTests.java @@ -28,11 +28,14 @@ import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Configuration; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatNoException; /** * Tests for {@link PropertiesMigrationListener}. * * @author Stephane Nicoll + * @author Hyunwoo Jung */ @ExtendWith(OutputCaptureExtension.class) class PropertiesMigrationListenerTests { @@ -55,6 +58,42 @@ class PropertiesMigrationListenerTests { .doesNotContain("Please refer to the release notes"); } + @Test + void failNeverWithRenamedKeyStarts() { + assertThatNoException().isThrownBy(() -> this.context = createSampleApplication() + .run("--spring.tools.properties-migrator.fail=never", "--logging.file=test.log")); + } + + @Test + void failNeverWithUnsupportedKeyStarts() { + assertThatNoException().isThrownBy(() -> this.context = createSampleApplication() + .run("--spring.tools.properties-migrator.fail=never", "--spring.banner.image.width=10")); + } + + @Test + void failOnErrorWithRenamedKeyStarts() { + assertThatNoException().isThrownBy(() -> this.context = createSampleApplication() + .run("--spring.tools.properties-migrator.fail=on-error", "--logging.file=test.log")); + } + + @Test + void failOnErrorWithUnsupportedKeyFails() { + assertThatIllegalStateException().isThrownBy(() -> createSampleApplication() + .run("--spring.tools.properties-migrator.fail=on-error", "--spring.banner.image.width=10")); + } + + @Test + void failOnWarningWithRenamedKeyFails() { + assertThatIllegalStateException().isThrownBy(() -> createSampleApplication() + .run("--spring.tools.properties-migrator.fail=on-warning", "--logging.file=test.log")); + } + + @Test + void failOnWarningWithUnsupportedKeyFails() { + assertThatIllegalStateException().isThrownBy(() -> createSampleApplication() + .run("--spring.tools.properties-migrator.fail=on-warning", "--spring.banner.image.width=10")); + } + private SpringApplication createSampleApplication() { return new SpringApplication(TestApplication.class); } diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/upgrading.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/upgrading.adoc index 52bdcdaa8c7..a1724a03b20 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/upgrading.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/upgrading.adoc @@ -49,6 +49,14 @@ To enable that feature, add the following dependency to your project: ---- +The configprop:spring.tools.properties-migrator.fail[] property controls whether migration warnings or errors cause the application to fail to start: + +* `never`: do not fail application startup because of migration warnings or errors (default). +* `on-error`: fail when properties that are no longer supported are found. +* `on-warning`: fail when properties that have been renamed or are no longer supported are found. + +Migration diagnostics are logged regardless of this setting. + WARNING: Properties that are added late to the environment, such as when using javadoc:org.springframework.context.annotation.PropertySource[format=annotation], will not be taken into account. NOTE: Once you finish the migration, please make sure to remove this module from your project's dependencies.