天啦!竟然从来没有人讲过 SpringBoot 支持配置如此平滑的迁移

2,661 阅读3分钟

候鸟迁徙

SpringBoot 是原生支持配置迁移的,但是官方文档没有看到这方面描述,在源码中才看到此模块,spring-boot-properties-migrator,幸亏我没有跳过。看到这篇文章的各位,可算是捡到宝了,相信你继续往下看下去,定会忍不住点赞、收藏、关注。

效果

先放个效果吸引你 :)

从 SpringBoot 2.0.0 版本开始,配置服务上下文,不支持 server.context-path,而需要server.servlet.context-path配置。但是只要加上以下一个官方依赖,就可以支持使用 server.context-path

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

server.context-path 所对应的属性 ServerProperties#contextPath 在 Java 代码中已不存在,server.servlet.context-path 所对应的的属性在内部类 Servlet 中才有,为何加了此依赖就能实现如此神奇的效果呢。

原理

SpringBoot 对外部化配置原生支持迁移功能,所谓迁移,具体是指对应配置的属性名变动,仍可以使用原来的属性名配置。在 spring-configuration-metadata.json 的信息可以辅助 IDE 进行配置的提示,也可以用来完成配置的迁移。非常的简单。

相关文章: SpringBoot 配置提示功能

通过阅读代码,获得以下信息:

  1. 监听 ApplicationPreparedEvent 事件(即:环境已准备事件),执行以下操作并收集信息
  2. classpath*:/META-INF/spring-configuration-metadata.json 中载入所有配置
  3. 从上下文的 environment 中过滤出提示的配置(满足条件:1. deprecation 不为 null,且提示 level 为 error)
  4. 判断是否兼容(兼容条件见下一节),提取出兼容的属性
  5. 将 value 对应到 replacement 的 key,并将其属性源命名为:migrate-原名
  6. 将配置迁移的新属性源添加到 environment 中,且添加到原属性源之前(优先级高)。
  7. 监听事件:ApplicationReadyEvent(应用上下文已准备) 或 ApplicationFailedEvent(应用启动失败),打印以上步骤收集的遗留配置信息。以 warn 级别打印兼容的配置,以 error 级别打印不兼容的配置

配置兼容条件

根据元数据中定义的 type 判断

  1. 如果旧类型、新类型其中之一为 null(元数据中未指定),则不兼容
  2. 如果两个类型一样,兼容
  3. 如果新类型是 Duration,而旧类型是 Long 或 Integer,则兼容
  4. 其他情况视为不兼容
  5. environment 中取配置信息,理论上支持 SpringBoot 所有的配置方式

效果

兼容效果:弃用属性(如果还存在)与替换后的属性都会使用配置文件中的弃用的属性名所对应的的值。

总结

使用配置迁移功能,需要以下步骤:

  1. 引入依赖:spring-boot-properties-migrator(支持配置迁移)、spring-boot-configuration-processor(生成元数据文件,如果已经有完整的,不需要此依赖)
  2. 元数据文件spring-configuration-metadata.json 中弃用属性名对应的 properties 中必须有 deprecation(在additional-spring-configuration-metadata.json 中添加,相关文章: SpringBoot 配置提示功能
  3. deprecation 中需指定 levelerror
  4. deprecation 中需指定 replacement
  5. replacement 对应的属性配置在元数据文件中存在,与弃用属性兼容

经典示例之配置上下文

再说回一开始展示的配置上下文示例。

# 配置 servlet 服务上下文
server:
  context-path: test

从 SpringBoot 2.0.0 版本开始,以上配置不支持,点到配置元数据文件中(spring-configuration-metadata.json),发现如下信息:

{
  "properties": [
    {
      "name": "server.context-path",
      "type": "java.lang.String",
      "description": "Context path of the application.",
      "deprecated": true,
      "deprecation": {
        "level": "error",
        "replacement": "server.servlet.context-path"
      }
    },
    {
      "name": "server.servlet.context-path",
      "type": "java.lang.String",
      "description": "Context path of the application.",
      "sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties$Servlet"
    }

替换属性名为:server.servlet.context-path,此属性在org.springframework.boot.autoconfigure.web.ServerProperties 中,且在类中可以发现,server.context-path 所对应的属性 ServerProperties#contextPath 在代码中已不存在,而是在内部类 Servlet 中有,也就是对应 server.servlet.context-path 的属性才有。

但是其满足配置兼容的条件,为什么实际上使用却好像不兼容呢?其实是因为没有引入依赖,当引入依赖,就会发现此方式配置可以起作用。

示例之两种属性都存在

代码示例见 gitee.com/lw888/sprin…

1、引入依赖

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

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-configuration-processor</artifactId>
  <optional>true</optional>
</dependency>

2、Java 配置此处故意保留弃用属性

@Data
@Configuration
@ConfigurationProperties(prefix = "my")
public class MyProperties {
  /** the project name */
  private String name;

  private App app;

  @Data
  public static class App {
    private String name;
  }
}

3、元数据配置,spring-configuration-metadata.json 由程序生成,自定义配置放在 additional-spring-configuration-metadata.json

{
  "properties": [
    {
      "name": "my.name",
      "type": "java.lang.String",
      "description": "the project name.",
      "deprecation": {
        "reason": "test the properties-migrator feature.",
        "replacement": "my.app.name",
        "level": "error"
      }
    },
    {
      "name": "my.app.name",
      "type": "java.lang.String",
      "sourceType": "com.lw.properties.migrator.config.MyProperties$App",
      "description": "the project name."
    }
  ]
}

4、在 properties 或 yml 文件中配置

my:
  name: lw
  app:
    name: app

5、打印配置信息

@Slf4j
@SpringBootApplication
public class PropertiesMigratorApplication {

  public static void main(String[] args) {
    ConfigurableApplicationContext context =
        SpringApplication.run(PropertiesMigratorApplication.class, args);
    MyProperties myProperties = context.getBean(MyProperties.class);
    log.info("myProperties.name:{}", myProperties.getName());
    log.info(
        "myProperties$app.name:{}",
        Optional.ofNullable(myProperties.getApp()).orElse(new App()).getName());
  }
}

6、打印信息如下:

2019-11-23 21:42:09.580 WARN 109408 --- [ 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.yml]':Key: my.nameLine: 4Replacement: my.app.nameKey: server.context-pathLine: 2Replacement: server.servlet.context-path

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.
......... myProperties.name:lw
......... myProperties$app.name:lw
......... serverProperties$servlet.contextPath:/app

7、效果解析在 yml 中弃用属性名优先级更高,弃用属性与新属性都使用此弃用属性名对应的值。

参考资料

SpringBoot 2.2.1.RELEASE 源码公众号:逸飞兮(专注于 Java 领域知识的深入学习,从源码到原理,系统有序的学习)

逸飞兮