Merge pull request #51855 from noojung

Closes gh-51855

* pr/51855:
  Polish 'Allow properties migrator to fail application startup'
  Allow properties migrator to fail application startup
This commit is contained in:
Phillip Webb
2026-09-25 20:27:02 -07:00
6 changed files with 106 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.properties");
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.function.Predicate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -29,22 +30,28 @@ 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;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.util.Assert;
/**
* 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.properties.migrator.fail";
private @Nullable PropertiesMigrationReport report;
private boolean reported;
@@ -60,10 +67,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();
Fail fail = Binder.get(environment).bind(FAIL_PROPERTY, Fail.class).orElse(Fail.NEVER);
PropertiesMigrationReport report = new PropertiesMigrationReporter(loadRepository(), environment).getReport();
this.report = report;
fail.check(report);
}
private ConfigurationMetadataRepository loadRepository() {
@@ -102,4 +110,37 @@ class PropertiesMigrationListener implements ApplicationListener<SpringApplicati
this.reported = true;
}
/**
* Conditions under which property migration should cause application startup to fail.
*/
public enum Fail {
/**
* Do not fail application startup due to migration warnings or errors.
*/
NEVER((report) -> false),
/**
* Fail application startup if the migration report contains errors.
*/
ON_ERROR((report) -> report.getErrorReport() != null),
/**
* Fail application startup if the migration report contains warnings or errors.
*/
ON_WARNING((report) -> report.getErrorReport() != null || report.getWarningReport() != null);
private final Predicate<PropertiesMigrationReport> predicate;
Fail(Predicate<PropertiesMigrationReport> predicate) {
this.predicate = predicate;
}
void check(PropertiesMigrationReport report) {
boolean failed = this.predicate.test(report);
Assert.state(!failed, "Found configuration keys that need to be migrated");
}
}
}
@@ -0,0 +1,11 @@
{
"properties": [
{
"name": "spring.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.properties.migrator.fail=never", "--logging.file=test.log"));
}
@Test
void failNeverWithUnsupportedKeyStarts() {
assertThatNoException().isThrownBy(() -> this.context = createSampleApplication()
.run("--spring.properties.migrator.fail=never", "--spring.banner.image.width=10"));
}
@Test
void failOnErrorWithRenamedKeyStarts() {
assertThatNoException().isThrownBy(() -> this.context = createSampleApplication()
.run("--spring.properties.migrator.fail=on-error", "--logging.file=test.log"));
}
@Test
void failOnErrorWithUnsupportedKeyFails() {
assertThatIllegalStateException().isThrownBy(() -> createSampleApplication()
.run("--spring.properties.migrator.fail=on-error", "--spring.banner.image.width=10"));
}
@Test
void failOnWarningWithRenamedKeyFails() {
assertThatIllegalStateException().isThrownBy(() -> createSampleApplication()
.run("--spring.properties.migrator.fail=on-warning", "--logging.file=test.log"));
}
@Test
void failOnWarningWithUnsupportedKeyFails() {
assertThatIllegalStateException().isThrownBy(() -> createSampleApplication()
.run("--spring.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.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.