实战《Spring Boot 2.1.5》-属性迁移工具Migrator

migrator介绍

以下内容为Spring Boot Reference Guide v2.1.5的内容

When upgrading to a new feature release, some properties may have been renamed or removed. Spring Boot provides a way to analyze your application’s environment and print diagnostics at startup, but also temporarily migrate properties at runtime for you. To enable that feature, add the following dependency to your project:

当升级到新功能版本时,一些配置可能会重命名或者被移除。SpringBoot提供一种方式去分析你应用的环境和在启动时打印诊断内容,还可以在运行时为你临时迁移属性。要启用该特性,添加下方的依赖到你的工程中:

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-properties-migrator</artifactId>
 <scope>runtime</scope>
</dependency>

Properties that are added late to the environment, such as when using @PropertySource, will not be taken into account.

晚添加到环境中的属性,例如使用@PropertySource时,将不被考虑。

Once you’re done with the migration, please make sure to remove this module from your project’s dependencies.

一旦你完成迁移,请确保在你的项目依赖中移除该模块。

功能介绍

配置文件:application.properties

server.port=8080

# 该属性已经被重命名了
# 参考spring-boot-autoconfigure-2.1.5.RELEASE.jar下META-INF的additional-spring-configuration-metadata.json文件

# {
#  "name": "security.user.name",
#  "type": "java.lang.String",
#  "description": "Default user name.",
#  "defaultValue": "user",
#  "deprecation": {
#    "replacement": "spring.security.user.name",
#    "level": "error"
#  }
# }
    
security.user.name=test

# 该属性已经被重命名了
# 参考spring-boot-2.1.5.RELEASE.jar下META-INF的spring-configuration-metadata.json文件

# {
#   "name": "banner.image.height",
#   "type": "java.lang.Integer",
#   "description": "Banner image height (in chars).",
#   "deprecated": true,
#   "deprecation": {
#     "level": "error",
#     "replacement": "spring.banner.image.height"
#   }
# }
banner.image.height=1

提示内容

当你的配置文件中存在被识别的已经移除的属性时,日志打印以下内容:

2020-02-12 22:06:01.321  WARN 22900 --- [           main] o.s.b.c.p.m.PropertiesMigrationListener  : 
The use of configuration keys that have been renamed was found in the environment:

Property source 'applicationConfig: [classpath:/application.properties]':
	Key: banner.image.height
		Line: 3
		Replacement: spring.banner.image.height
	Key: security.user.name
		Line: 2
		Replacement: spring.security.user.name


Each configuration key has been temporarily mapped to its replacement for your convenience. To silence this warning, please update your configuration to use the new keys.

注意:并不是配置中所有的属性重命名之后都会被提示,必须是被识别的,并且是已经移除属性。参考实现原理内容。

实现原理

类PropertiesMigrationListener实现ApplicationListener。对三类事件进行操作,分别是ApplicationPreparedEvent,ApplicationReadyEvent,ApplicationFailedEvent。优先执行事件优先执行

  • ApplicationPreparedEvent

核心代码如下:

private void onApplicationPreparedEvent(ApplicationPreparedEvent event) {
    
    // 步骤1:加载数据到内存
    ConfigurationMetadataRepository repository = this.loadRepository();
    PropertiesMigrationReporter reporter = new PropertiesMigrationReporter(repository, event.getApplicationContext().getEnvironment());
    
    // 下面那个方法
    this.report = reporter.getReport();
}


// this.report = reporter.getReport();的方法
public PropertiesMigrationReport getReport() {
    PropertiesMigrationReport report = new PropertiesMigrationReport();
    
    // 步骤2和步骤3,匹配数据
    Map<String, List<PropertyMigration>> properties = this.getMatchingProperties(this.deprecatedFilter());
    if (properties.isEmpty()) {
        return report;
    } else {
        properties.forEach((name, candidates) -> {
            
            // 步骤4
            PropertySource<?> propertySource = this.mapPropertiesWithReplacement(report, name, candidates);
            if (propertySource != null) {
                
                // 步骤5让其属性生效,这也是为什么“晚添加到环境中的属性,例如使用@PropertySource时,将不被考虑。”的原因
                this.environment.getPropertySources().addBefore(name, propertySource);
            }

        });
        return report;
    }
}
  1. 扫描指定路径下的文件:classpath*:/META-INF/spring-configuration-metadata.json,将数据加载到内存,以下为spring-boot-2.1.5.RELEASE.jar下spring-configuration-metadata.json的部分内容
// spring-configuration-metadata.json的数据格式
{
  "groups": [
    {
      "name": "logging",
      "type": "org.springframework.boot.context.logging.LoggingApplicationListener"
    },
    ......
  ],
  "properties": [
     {
      "name": "server.address",
      "type": "java.net.InetAddress",
      "description": "Network address to which the server should bind.",
      "sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties"
     },
     ......
     {
      "name": "banner.image.height",
      "type": "java.lang.Integer",
      "description": "Banner image height (in chars).",
      "deprecated": true,
      "deprecation": {
        "level": "error",
        "replacement": "spring.banner.image.height"
      }
     },
     ......
  ],
  "hints": [
    {
      "name": "logging.group.values",
      "providers": [
        {
          "name": "logger-name",
          "parameters": {
            "group": false
          }
        }
      ]
    },
    ......
  ]
}
  1. 过滤出步骤1加载到内存的异常数据(针对properties数据),代码如下:
private Predicate<ConfigurationMetadataProperty> deprecatedFilter() {
    return (property) -> {
        return property.getDeprecation() != null && property.getDeprecation().getLevel() == Level.ERROR;
    };
}
  1. 匹配出错误配置,代码如下:
private Map<String, List<PropertyMigration>> getMatchingProperties(Predicate<ConfigurationMetadataProperty> filter) {
    MultiValueMap<String, PropertyMigration> result = new LinkedMultiValueMap();
    
    // filter为步骤2的FunctionalInterface
    List<ConfigurationMetadataProperty> candidates= (List)this.allProperties.values().stream().filter(filter).collect(Collectors.toList());
    
    // 加载当前环境的配置文件,并循环遍历
    this.getPropertySourcesAsMap().forEach((name, source) -> {
        
        // 遍历异常数据
        candidates.forEach((metadata) -> {
        
            // 如果当前环境的配置在异常数据种存在,则加入到map
            ConfigurationProperty configurationProperty = source.getConfigurationProperty(ConfigurationPropertyName.of(metadata.getId()));
            if (configurationProperty != null) {
                result.add(name, new PropertyMigration(configurationProperty, metadata, this.determineReplacementMetadata(metadata)));
            }

        });
    });
    return result;
}
  1. 将过时的属性名称修改最新的属性名称,代码如下:
private PropertySource<?> mapPropertiesWithReplacement(PropertiesMigrationReport report, String name, List<PropertyMigration> properties) {
    
    // 为下一个事件,打印日志做准备
    report.add(name, properties);
    
    // 允许重命名的属性列表
    List<PropertyMigration> renamed = (List)properties.stream().filter(PropertyMigration::isCompatibleType).collect(Collectors.toList());
    if (renamed.isEmpty()) {
        return null;
    } else {
        String target = "migrate-" + name;
        Map<String, OriginTrackedValue> content = new LinkedHashMap();
        Iterator var7 = renamed.iterator();

        while(var7.hasNext()) {
        
            // 修改属性名称
            PropertyMigration candidate = (PropertyMigration)var7.next();
            OriginTrackedValue value = OriginTrackedValue.of(candidate.getProperty().getValue(), candidate.getProperty().getOrigin());
            content.put(candidate.getMetadata().getDeprecation().getReplacement(), value);
        }

        return new OriginTrackedMapPropertySource(target, content);
    }
}
  • ApplicationReadyEvent或ApplicationFailedEvent

根据第一步步骤4添加的数据,打印日志,日志分为warn和error两种,根据属性是否可以重命名来区分,代码如下:

// 判断属性是否可以重命名
private static boolean determineCompatibleType(ConfigurationMetadataProperty metadata, ConfigurationMetadataProperty replacementMetadata) {
    String currentType = metadata.getType();
    String replacementType = determineReplacementType(replacementMetadata);
    if (replacementType != null && currentType != null) {
        if (replacementType.equals(currentType)) {
            return true;
        } else {
            return replacementType.equals(Duration.class.getName()) && (currentType.equals(Long.class.getName()) || currentType.equals(Integer.class.getName()));
        }
    } else {
        return false;
    }
}

// 打印日志代码
private void logLegacyPropertiesReport() {
    if (this.report != null && !this.reported) {
        String warningReport = this.report.getWarningReport();
        if (warningReport != null) {
            logger.warn(warningReport);
        }

        String errorReport = this.report.getErrorReport();
        if (errorReport != null) {
            logger.error(errorReport);
        }

        this.reported = true;
    }
}

总结

可以通过该组件来校验配置是否过时,如果过时了,及时修改配置文件。在正式发布时,记得移除该组件,以免影响启动效率和占用内存。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章