Created
November 8, 2020 22:45
-
-
Save ttddyy/ec22d32a20ed8385a478f2657e8aee4a to your computer and use it in GitHub Desktop.
Check the usage of deprecated properties at start up
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.example.analysis; | |
import java.io.IOException; | |
import java.io.InputStream; | |
import java.util.Collections; | |
import java.util.Comparator; | |
import java.util.HashSet; | |
import java.util.LinkedHashMap; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.Set; | |
import java.util.stream.Collectors; | |
import lombok.AllArgsConstructor; | |
import lombok.Getter; | |
import lombok.Setter; | |
import lombok.extern.slf4j.Slf4j; | |
import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty; | |
import org.springframework.boot.configurationmetadata.ConfigurationMetadataRepository; | |
import org.springframework.boot.configurationmetadata.ConfigurationMetadataRepositoryJsonBuilder; | |
import org.springframework.boot.configurationmetadata.Deprecation; | |
import org.springframework.boot.context.event.ApplicationPreparedEvent; | |
import org.springframework.boot.context.properties.source.ConfigurationProperty; | |
import org.springframework.boot.context.properties.source.ConfigurationPropertyName; | |
import org.springframework.boot.context.properties.source.ConfigurationPropertySource; | |
import org.springframework.boot.context.properties.source.ConfigurationPropertySources; | |
import org.springframework.boot.origin.Origin; | |
import org.springframework.boot.origin.TextResourceOrigin; | |
import org.springframework.context.ApplicationListener; | |
import org.springframework.core.env.ConfigurableEnvironment; | |
import org.springframework.core.env.Environment; | |
import org.springframework.core.env.PropertySource; | |
import org.springframework.core.io.Resource; | |
import org.springframework.core.io.support.PathMatchingResourcePatternResolver; | |
import org.springframework.lang.Nullable; | |
import org.springframework.util.LinkedMultiValueMap; | |
import org.springframework.util.MultiValueMap; | |
import org.springframework.util.StringUtils; | |
/** | |
* Check the usage of deprecated properties based on configuration metadata | |
* (META-INF/spring-configuration-metadata.json). | |
* <p> | |
* When usage of deprecated properties are found, this listener writes error logs and stop | |
* starting the application by default. User can modify the behavior to write warning logs | |
* only, continue the check by adding properties to allow-list, or completely disable | |
* deprecated properties check via properties. | |
* <p> | |
* It is recommended to always use non deprecated properties. | |
* <p> | |
* Implementation is based on "spring-boot-properties-migrator". See the background: | |
* https://github.com/spring-projects/spring-boot/issues/23973 | |
* <p> | |
* Dependency to "spring-boot-configuration-metadata" is required. | |
* <p> | |
* Add this listener to "spring.factories" under "org.springframework.context.ApplicationListener". | |
* | |
* @author Tadaya Tsuyukubo | |
*/ | |
@Slf4j | |
public class PropertyValidationListener implements ApplicationListener<ApplicationPreparedEvent> { | |
private static final String ENABLED_KEY = "analysis.property-validation.enabled"; | |
private static final String WARNING_ONLY_KEY = "analysis.property-validation.warning-only"; | |
private static final String ALLOW_LIST_KEY = "analysis.property-validation.allow-list"; | |
@Override | |
public void onApplicationEvent(ApplicationPreparedEvent event) { | |
ConfigurableEnvironment environment = event.getApplicationContext().getEnvironment(); | |
boolean enabled = environment.getProperty(ENABLED_KEY, Boolean.class, Boolean.TRUE); | |
if (!enabled) { | |
return; | |
} | |
// retrieve configuration metadata | |
ConfigurationMetadataRepository repository = loadRepository(); | |
Map<String, ConfigurationMetadataProperty> allProperties = Collections | |
.unmodifiableMap(repository.getAllProperties()); | |
// from | |
// org.springframework.boot.context.properties.migrator.PropertiesMigrationReporter#getReport | |
MatchedProperties matched = getMatchingProperties(environment, allProperties); | |
// found usage of deprecated properties but in allow list | |
if (!matched.allowed.isEmpty()) { | |
log.info(getAllowedReport(matched.allowed)); | |
} | |
if (matched.deprecated.isEmpty()) { | |
return; | |
} | |
String report = getDeprecatedReport(matched.deprecated); | |
boolean warningOnly = environment.getProperty(WARNING_ONLY_KEY, Boolean.class, Boolean.FALSE); | |
if (warningOnly) { | |
log.warn(report); | |
return; | |
} | |
log.error(report); | |
throw new IllegalStateException("Found deprecated properties"); | |
} | |
// Similar format to | |
// org.springframework.boot.context.properties.migrator.PropertiesMigrationReport#getWarningReport | |
private String getAllowedReport(MultiValueMap<String, DeprecatedProperty> allowed) { | |
StringBuilder report = new StringBuilder(); | |
report.append(String | |
.format("%nThe use of configuration keys that have been deprecated was found in the environment:%n%n")); | |
append(report, allowed); | |
report.append(String.format("%n")); | |
report.append("Each configuration are specified in allow list."); | |
report.append(String.format("%n")); | |
return report.toString(); | |
} | |
// Similar format to | |
// org.springframework.boot.context.properties.migrator.PropertiesMigrationReport#getWarningReport | |
private String getDeprecatedReport(MultiValueMap<String, DeprecatedProperty> deprecated) { | |
StringBuilder report = new StringBuilder(); | |
report.append(String | |
.format("%nThe use of configuration keys that have been deprecated was found in the environment:%n%n")); | |
append(report, deprecated); | |
report.append(String.format("%n")); | |
report.append("To silence this warning, please update your configuration to use the new keys.\n"); | |
report.append("Or set property to do:\n"); | |
report.append(String.format("- Make warning only with: \"%s=true\"\n", WARNING_ONLY_KEY)); | |
report.append(String.format("- Add keys to allow-list: \"%s=key.a,key.b\"\n", ALLOW_LIST_KEY)); | |
report.append(String.format("- Disable this check: \"%s=false\"\n", ENABLED_KEY)); | |
report.append(String.format("%n")); | |
return report.toString(); | |
} | |
private void append(StringBuilder report, MultiValueMap<String, DeprecatedProperty> content) { | |
content.forEach((name, properties) -> { | |
report.append(String.format("Property source '%s':%n", name)); | |
properties.sort(DeprecatedProperty.COMPARATOR); | |
properties.forEach((property) -> { | |
ConfigurationMetadataProperty metadata = property.getMetadata(); | |
report.append(String.format("\tKey: %s%n", metadata.getId())); | |
if (property.getLineNumber() != null) { | |
report.append(String.format("\t\tLine: %d%n", property.getLineNumber())); | |
} | |
report.append(String.format("\t\t%s%n", property.getDetermineReason())); | |
ConfigurationMetadataProperty replacement = property.getReplacementMetadata(); | |
if (replacement != null) { | |
report.append(String.format("\t\tReplacement: %s%n", replacement.getId())); | |
} | |
}); | |
report.append(String.format("%n")); | |
}); | |
} | |
// Based on | |
// org.springframework.boot.context.properties.migrator.PropertiesMigrationReporter#getMatchingProperties | |
private MatchedProperties getMatchingProperties(Environment environment, | |
Map<String, ConfigurationMetadataProperty> allProperties) { | |
MatchedProperties result = new MatchedProperties(); | |
List<ConfigurationMetadataProperty> deprecated = allProperties.values().stream() | |
.filter(ConfigurationMetadataProperty::isDeprecated).collect(Collectors.toList()); | |
if (deprecated.isEmpty()) { | |
return result; | |
} | |
@SuppressWarnings("unchecked") | |
Set<String> allowedIds = environment.getProperty(ALLOW_LIST_KEY, Set.class, new HashSet<>()); | |
getPropertySourcesAsMap(environment).forEach((name, source) -> deprecated.forEach((metadata) -> { | |
ConfigurationProperty configurationProperty = source | |
.getConfigurationProperty(ConfigurationPropertyName.of(metadata.getId())); | |
if (configurationProperty != null) { | |
Integer lineNumber = determineLineNumber(configurationProperty); | |
ConfigurationMetadataProperty replacementMetadata = determineReplacementMetadata(metadata, | |
allProperties); | |
String reason = determineReason(metadata, replacementMetadata); | |
DeprecatedProperty property = new DeprecatedProperty(configurationProperty, lineNumber, metadata, | |
replacementMetadata, reason); | |
if (allowedIds.contains(metadata.getId())) { | |
result.allowed.add(name, property); | |
} | |
else { | |
result.deprecated.add(name, property); | |
} | |
} | |
})); | |
return result; | |
} | |
// org.springframework.boot.context.properties.migrator.PropertyMigration#determineReason | |
private String determineReason(ConfigurationMetadataProperty metadata, | |
@Nullable ConfigurationMetadataProperty replacementMetadata) { | |
Deprecation deprecation = metadata.getDeprecation(); | |
if (StringUtils.hasText(deprecation.getShortReason())) { | |
return "Reason: " + deprecation.getShortReason(); | |
} | |
if (StringUtils.hasText(deprecation.getReplacement())) { | |
if (replacementMetadata != null) { | |
return String.format("Reason: Replacement key '%s' uses an incompatible target type", | |
deprecation.getReplacement()); | |
} | |
else { | |
return String.format("Reason: No metadata found for replacement key '%s'", | |
deprecation.getReplacement()); | |
} | |
} | |
return "Reason: none"; | |
} | |
// org.springframework.boot.context.properties.migrator.PropertiesMigrationReporter#determineReplacementMetadata | |
@Nullable | |
private ConfigurationMetadataProperty determineReplacementMetadata(ConfigurationMetadataProperty metadata, | |
Map<String, ConfigurationMetadataProperty> allProperties) { | |
String replacementId = metadata.getDeprecation().getReplacement(); | |
if (StringUtils.hasText(replacementId)) { | |
ConfigurationMetadataProperty replacement = allProperties.get(replacementId); | |
if (replacement != null) { | |
return replacement; | |
} | |
return detectMapValueReplacement(replacementId, allProperties); | |
} | |
return null; | |
} | |
// org.springframework.boot.context.properties.migrator.PropertiesMigrationReporter#detectMapValueReplacement | |
@Nullable | |
private ConfigurationMetadataProperty detectMapValueReplacement(String fullId, | |
Map<String, ConfigurationMetadataProperty> allProperties) { | |
int lastDot = fullId.lastIndexOf('.'); | |
if (lastDot != -1) { | |
return allProperties.get(fullId.substring(0, lastDot)); | |
} | |
return null; | |
} | |
// org.springframework.boot.context.properties.migrator.PropertyMigration#determineLineNumber | |
@Nullable | |
private static Integer determineLineNumber(ConfigurationProperty property) { | |
Origin origin = property.getOrigin(); | |
if (origin instanceof TextResourceOrigin) { | |
TextResourceOrigin textOrigin = (TextResourceOrigin) origin; | |
if (textOrigin.getLocation() != null) { | |
return textOrigin.getLocation().getLine() + 1; | |
} | |
} | |
return null; | |
} | |
// org.springframework.boot.context.properties.migrator.PropertiesMigrationListener#loadRepository() | |
private ConfigurationMetadataRepository loadRepository() { | |
try { | |
return loadRepository(ConfigurationMetadataRepositoryJsonBuilder.create()); | |
} | |
catch (IOException ex) { | |
throw new IllegalStateException("Failed to load metadata", ex); | |
} | |
} | |
private ConfigurationMetadataRepository loadRepository(ConfigurationMetadataRepositoryJsonBuilder builder) | |
throws IOException { | |
Resource[] resources = new PathMatchingResourcePatternResolver() | |
.getResources("classpath*:/META-INF/spring-configuration-metadata.json"); | |
for (Resource resource : resources) { | |
try (InputStream inputStream = resource.getInputStream()) { | |
builder.withJsonResource(inputStream); | |
} | |
} | |
return builder.build(); | |
} | |
// org.springframework.boot.context.properties.migrator.PropertiesMigrationReporter#getPropertySourcesAsMap | |
private Map<String, ConfigurationPropertySource> getPropertySourcesAsMap(Environment environment) { | |
Map<String, ConfigurationPropertySource> map = new LinkedHashMap<>(); | |
for (ConfigurationPropertySource source : ConfigurationPropertySources.get(environment)) { | |
map.put(determinePropertySourceName(source), source); | |
} | |
return map; | |
} | |
private String determinePropertySourceName(ConfigurationPropertySource source) { | |
if (source.getUnderlyingSource() instanceof PropertySource) { | |
return ((PropertySource<?>) source.getUnderlyingSource()).getName(); | |
} | |
return source.getUnderlyingSource().toString(); | |
} | |
static class MatchedProperties { | |
// Deprecated properties in allow list | |
MultiValueMap<String, DeprecatedProperty> allowed = new LinkedMultiValueMap<>(); | |
// Deprecated properties NOT in allow list | |
MultiValueMap<String, DeprecatedProperty> deprecated = new LinkedMultiValueMap<>(); | |
} | |
@Getter | |
@Setter | |
@AllArgsConstructor | |
static class DeprecatedProperty { | |
static final Comparator<DeprecatedProperty> COMPARATOR = Comparator | |
.comparing((property) -> property.getMetadata().getId()); | |
private ConfigurationProperty property; | |
private final Integer lineNumber; | |
private ConfigurationMetadataProperty metadata; | |
private ConfigurationMetadataProperty replacementMetadata; | |
private String determineReason; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment