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 <hyunwoojung@kakao.com>
This commit is contained in:
Hyunwoo Jung
2026-09-25 20:23:53 -07:00
committed by Phillip Webb
parent 789e06631b
commit a86aa43303
6 changed files with 109 additions and 5 deletions
@@ -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");
@@ -16,6 +16,7 @@
plugins {
id "java"
id "org.springframework.boot.configuration-metadata"
id "org.springframework.boot.deployed"
}
@@ -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<SpringApplicationEvent> {
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<SpringApplicati
}
private void onApplicationPreparedEvent(ApplicationPreparedEvent event) {
ConfigurationMetadataRepository repository = loadRepository();
PropertiesMigrationReporter reporter = new PropertiesMigrationReporter(repository,
event.getApplicationContext().getEnvironment());
this.report = reporter.getReport();
ConfigurableEnvironment environment = event.getApplicationContext().getEnvironment();
PropertiesMigrationReport report = new PropertiesMigrationReporter(loadRepository(), environment).getReport();
this.report = report;
Fail fail = Binder.get(environment).bind(FAIL_PROPERTY, Fail.class).orElse(Fail.NEVER);
failIfNecessary(report, fail);
}
private ConfigurationMetadataRepository loadRepository() {
@@ -102,4 +109,41 @@ class PropertiesMigrationListener implements ApplicationListener<SpringApplicati
this.reported = true;
}
private void failIfNecessary(PropertiesMigrationReport report, Fail fail) {
if (shouldFail(report, fail)) {
throw new IllegalStateException("Found configuration keys that need to be migrated (%s=%s)"
.formatted(FAIL_PROPERTY, fail.name().toLowerCase(Locale.ROOT).replace('_', '-')));
}
}
private static boolean shouldFail(PropertiesMigrationReport report, Fail fail) {
return switch (fail) {
case NEVER -> 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
}
}
@@ -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"
}
]
}
@@ -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);
}
@@ -49,6 +49,14 @@ To enable that feature, add the following dependency to your project:
</dependency>
----
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.