實戰《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;
    }
}

總結

可以通過該組件來校驗配置是否過時,如果過時了,及時修改配置文件。在正式發佈時,記得移除該組件,以免影響啓動效率和佔用內存。

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