掘金 后端 ( ) • 2024-05-05 16:23

theme: cyanosis

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

背景

近期发布项目时经常出现一个问题,大体报错如下:

The use of configuration keys that are no longer supported was found in the environment:

Property source 'systemEnvironment':
	Key: spring.cloud.nacos.config.shared-dataids
		Reason: replaced to NacosConfigProperties#sharedConfigs and not use it at the same time.

Property source 'bootstrapProperties-login-service.yaml,login-service':
	Key: spring.mvc.favicon.enabled
		Reason: none

Property source 'bootstrapProperties-application.yaml,DEFAULT_GROUP':
	Key: server.connection-timeout
		Reason: Each server behaves differently.


Please refer to the release notes or reference guide for potential alternatives.

从日志报错有一个直观的判断,就是这些被抛出来的 key 是不被支持的(过期或者已经移除的)。这里先给出项目的基本信息:

  • 基于 spring boot 2.4.2 +
  • 配置中心使用的是 nacos ( cloud alibaba nacos)

此外,这个报错不会导致我项目启动直接阻断,而且存在一定的随机性,并不是每次都会出现。针对这个报错,有两个问题:

  • 1、这个报错产生的具体原因是什么?
  • 2、为什么会有随机性,不是必现

下面是分析及排查的过程。

分析及排查过程

首先明确一点,任何报错日志,都会有其抛出堆栈或者日志的地方;找到它就可以现确定它是那个组件或者框架产生的,对于一些简单的异常,通过阅读源码是可以直接发现问题原因的,对于复杂的异常情况,可以通过简化项目结构进行场景复现,通过 debug 找到问题原因

报错日志产生原因

针对上面的error 日志,现进行过滤:

  • source 'xxxx' :可以忽略,这里指的是下面 key 的 source。
  • key:配置的 key ,可以缩小我们的排查范围
  • Reason:产生原因

此外如上所述,找到异常产生的源码 org.springframework.boot.context.properties.migrator.PropertiesMigrationReport#getErrorReport

/**
	 * Return a report for all the properties that are no longer supported. If no such
	 * properties were found, return {@code null}.
	 * @return a report with the configurations keys that are no longer supported
	 */
String getErrorReport() {
    Map<String, List<PropertyMigration>> content = getContent(LegacyProperties::getUnsupported);
    if (content.isEmpty()) {
        return null;
    }
    StringBuilder report = new StringBuilder();
    report.append(String.format(
        "%nThe use of configuration keys that are no longer supported was found in the environment:%n%n"));
    append(report, content);
    report.append(String.format("%n"));
    report.append("Please refer to the release notes or reference guide for potential alternatives.");
    report.append(String.format("%n"));
    return report.toString();
}

通过分析执行堆栈发现,这里被执行到,在 springboot 中并不是通过 ApplicationFailsEvent 事件发布出去,而是通过 ApplicationReadyEvent 事件,所以这里就解释了为什么这个报错不会阻断程序运行的原因。

这里先给出一个小结论:报错的 key 在相关的 ConfigProperties 中都是被标记 @Deprecated 或者 @Deprecated@DeprecatedConfigurationProperty 注解的。

这里衍生一个新问题,被打了 @Deprecated / @DeprecatedConfigurationProperty 注解的这些配置属性和被 report 之间的关系是逻辑是什么?

// filter 从上层过来条件是 ConfigurationMetadataProperty::isDeprecated ,即过期的
private Map<String, List<PropertyMigration>> getMatchingProperties(
			Predicate<ConfigurationMetadataProperty> filter) {
    MultiValueMap<String, PropertyMigration> result = new LinkedMultiValueMap<>();
    // 从 allProperties 中筛选出所有过期的配置
    List<ConfigurationMetadataProperty> candidates = this.allProperties.values().stream().filter(filter)
        .collect(Collectors.toList());
    // 从当前项目配置中去匹配,如果有匹配到的 key ,则放到 result 中
    getPropertySourcesAsMap().forEach((name, source) -> candidates.forEach((metadata) -> {
        ConfigurationProperty configurationProperty = source
            .getConfigurationProperty(ConfigurationPropertyName.of(metadata.getId()));
        if (configurationProperty != null) {
            result.add(name,
                       new PropertyMigration(configurationProperty, metadata, determineReplacementMetadata(metadata)));
        }
    }));
    return result;
}

上面这段代码的核心就是,先将当前 class path 下所有的 properties 项枚举出来,然后和当前项目中的配置进行比对,大体策略如下:

  • 假设工程所有可提供的 ConfigProperties 的配置项是 :a.b.c a.b.b a.b.d a.b.e a.b.f ,启动 a.b.d a.b.e a.b.f 是过期的,所以第一遍拿到的 candidates 中就包括 a.b.d a.b.e a.b.f 三项。

  • 此时项目中配置了a.b.d a.b.e 两项,所以在 result 中就是 a.b.d a.b.e 两个 item,这两个 item 会在 error 日志中体现出来。

关于 ConfigurationMetadataProperty

对于 springboot 工程的 properties 文件,在使用时会有自动补全的机制,这主要依赖于 spring-boot-configuration-processor , spring-boot-configuration-processor 还有一个作用是,如果你的项目中引入了它,在 build 之后,会产生一个 json 文件:

以为例 spring.mvc.favicon.enabled 为例:

{
      "name": "spring.mvc.favicon.enabled",
      "type": "java.lang.Boolean",
      "description": "Whether to enable resolution of favicon.ico.",
      "deprecated": true,
      "deprecation": {
        "level": "error"
      }
    },

原始代码如下:

@DeprecatedConfigurationProperty(reason = "Use of path extensions for request mapping and for content negotiation is discouraged.")
@Deprecated
public boolean isFavorPathExtension() {
    return this.favorPathExtension;
}

偶现的原因

这里其实在一开始大概就能猜到和依赖版本有关,这里主要取决于 maven 编译构建机制,maven 在构建 fatjar 时,依赖的引入和 jar 版本生效机制是个复杂的问题,这里没有直接去关注 maven 相关的东西,而是通过选择降低版本、高低版本同时存在两个场景从测试验证了猜想。

PS:依赖管理对于任何公司、任何工程、任何项目来说,都是头疼的问题。

解决方案

当明确问题产生的具体原因之后,解决它就是非常容易的事情了。这里有两个考虑点,第一这个报错不会影响项目的启动,是否有必要关注它?如果你不确定,那么你尽量不要动。第二在你非常清楚这个配置项并且确定项目中不使用它或者你知道它的替代方案时,那么请直接将这个配置项干掉或者替换它。

在我的工程中,是选择直接删除掉的,属于历史遗留问题

关于依赖问题,请不要随意指定任何依赖的版本,项目中已有 >> dependencyManager 管控版本 >> dependency 指定版本。