Skip to content

Instantly share code, notes, and snippets.

@ttddyy
Created November 8, 2020 22:45
Show Gist options
  • Save ttddyy/ec22d32a20ed8385a478f2657e8aee4a to your computer and use it in GitHub Desktop.
Save ttddyy/ec22d32a20ed8385a478f2657e8aee4a to your computer and use it in GitHub Desktop.
Check the usage of deprecated properties at start up
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