天啦!竟然从来没有人讲过 SpringBoot 支持配置如此平滑的迁移
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 配置提示功能
通过阅读代码,获得以下信息:
- 监听
ApplicationPreparedEvent
事件(即:环境已准备事件),执行以下操作并收集信息 - 从
classpath*:/META-INF/spring-configuration-metadata.json
中载入所有配置 - 从上下文的
environment
中过滤出提示的配置(满足条件:1. deprecation 不为 null,且提示level
为 error) - 判断是否兼容(兼容条件见下一节),提取出兼容的属性
- 将 value 对应到
replacement
的 key,并将其属性源命名为:migrate-原名 - 将配置迁移的
新属性源
添加到 environment 中,且添加
到原属性源之前
(优先级高)。 - 监听事件:ApplicationReadyEvent(应用上下文已准备) 或 ApplicationFailedEvent(应用启动失败),打印以上步骤收集的遗留配置信息。以 warn 级别打印兼容的配置,以 error 级别打印不兼容的配置
配置兼容条件
根据元数据中定义的 type
判断
- 如果旧类型、新类型其中之一为 null(元数据中未指定),则不兼容
- 如果两个类型一样,兼容
- 如果新类型是 Duration,而旧类型是 Long 或 Integer,则兼容
- 其他情况视为不兼容
-
从
environment
中取配置信息,理论上支持 SpringBoot 所有的配置方式。
效果
兼容效果:弃用
属性(如果还存在)与替换
后的属性都会使用配置文件中的弃用的属性名所对应的的值。
总结
使用配置迁移功能,需要以下步骤:
- 引入依赖:
spring-boot-properties-migrator
(支持配置迁移)、spring-boot-configuration-processor
(生成元数据文件,如果已经有完整的,不需要此依赖) - 元数据文件
spring-configuration-metadata.json
中弃用属性名对应的 properties 中必须有deprecation
(在additional-spring-configuration-metadata.json
中添加,相关文章: SpringBoot 配置提示功能) -
deprecation
中需指定level
为 error -
deprecation
中需指定replacement
-
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 的属性才有。
但是其满足配置兼容的条件,为什么实际上使用却好像不兼容呢?
其实是因为没有引入依赖,当引入依赖,就会发现此方式配置可以起作用。
示例之两种属性都存在
代码示例见 https://gitee.com/lw888/spring-boot-source-example/tree/master/properties-migrator
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.name
Line: 4
Replacement: my.app.name
Key: server.context-path
Line: 2
Replacement: 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 领域知识的深入学习,从源码到原理,系统有序的学习)