扩展SpringBoot配置文件: application.json
在 SpringBoot 中,从 application.properties 或 application.yaml 文件中读取配置文件是非常常用的功能。
本文介绍的是从 JSON 文件中读取配置,而不是从 .properties 文件或 .yaml 文件中读取配置。
如果你有用 .json 文件代替 .properties 或 .yaml 文件读取配置的需求,或者你想了解一下如何扩展 SpringBoot 默认的配置读取方式,那可以看看本文。
为什么要从.json文件读取配置?
总有那么一些奇葩的需求可能会用到,比如:
- .properties文件内容冗余太多
- .yaml文件在配置层级嵌套太多的情况下容易出现缩进错误
- …………
.json文件能代替.properties或.yaml文件吗?
.json文件当然能代替.yaml文件。
- yaml 能代替 properties 这个已经毋庸置疑了吧(SpringBoot已经证明了: 可以用 application.yaml 能代替 application.properties)
- yaml 就是 JSON 的超集(子集当然可以表示超集的内容了)
超集的意思就是: .yaml 能表达的语义 .json 一定能表达,但是反过来不一定。
总之就是 .json 文件一定能表达 .yaml 的语义。
看看下面三个配置文件:
- .yaml 文件内容
xxx:
config:
data:
datasource-list:
- datasource-name: : ds-01
url: jdbc:ds-01-xxxx
password: pwd-for-ds-01
username: username-for-ds-01
driver-class-name: com.mysql.cj.jdbc.Driver
- datasource-name: : ds-02
url: jdbc:ds-02-xxxx
password: pwd-for-ds-02
username: username-for-ds-02
driver-class-name: com.mysql.cj.jdbc.NonRegisteringDriver
- .properties 文件内容
xxx.config.data.datasource-list[0].datasource-name=ds-01
xxx.config.data.datasource-list[0].url=jdbc:ds-01-xxxx
xxx.config.data.datasource-list[0].password=pwd-for-ds-01
xxx.config.data.datasource-list[0].username=username-for-ds-01
xxx.config.data.datasource-list[0].driver-class-name=com.mysql.cj.jdbc.Driver
xxx.config.data.datasource-list[1].datasource-name=ds-02
xxx.config.data.datasource-list[1].url=jdbc:ds-02-xxxx
xxx.config.data.datasource-list[1].password=pwd-for-ds-02
xxx.config.data.datasource-list[1].username=username-for-ds-02
xxx.config.data.datasource-list[1].driver-class-name=com.mysql.cj.jdbc.NonRegisteringDriver
- .json 文件内容
{
"xxx": {
"config": {
"data": {
"datasource-list": [
{
"datasource-name": "ds-01",
"url": "jdbc:ds-01-xxxx",
"password": "pwd-for-ds-01",
"username": "username-for-ds-01",
"driver-class-name": "com.mysql.cj.jdbc.Driver"
},
{
"datasource-name": "ds-02",
"url": "jdbc:ds-02-xxxx",
"password": "pwd-for-ds-02",
"username": "username-for-ds-02",
"driver-class-name": "com.mysql.cj.jdbc.NonRegisteringDriver"
}
]
}
}
}
}
不难发现,上面三种格式的配置文件,其实表达的配置语义是等价的。
- .properties 文件,冗余而繁杂,并且配置层级不够明显(至少前缀都是一致的)
- .json 文件,结构非常清晰,层级分明,但是稍显复杂
- .yaml 文件,有 .json 文件的优点,同时省略了不必要的花括号,而改为使用缩进来表示层级
yaml 文件的缩进在 少量配置 的情况下看着非常清晰,但是当配置非常多、嵌套非常深的情况下就显得有点乱了。
所以本文介绍一种可以使用 application.json 文件代替 application.yaml 的方式。
如何扩展SpringBoot以支持application.json ?
默认情况下 Spring 会将 application.properties 或 application.yaml 封装为 PropertySource,最终以 分层的方式加入到 org.springframework.core.env.Environment(以下简称 Environment) 中。
如上如所示:在 SpringBoot 中能读取的所有配置都是从 Environment 中读取的。
在 spring 源码的 org.springframework.core.env.PropertySourcesPropertyResolver#getProperty(java.lang.String, java.lang.Class<T>, boolean) 方法中可以看到如下内容:
public class PropertySourcesPropertyResolver extends AbstractPropertyResolver {
// ...
@Nullable
protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
if (this.propertySources != null) {
// 从上到下 逐个遍历 PropertySource
// 哪个 PropertySource 能获取到 key 就返回
for (PropertySource<?> propertySource : this.propertySources) {
if (logger.isTraceEnabled()) {
logger.trace("Searching for key '" + key + "' in PropertySource '" +
propertySource.getName() + "'");
}
Object value = propertySource.getProperty(key);
if (value != null) {
if (resolveNestedPlaceholders && value instanceof String) {
value = resolveNestedPlaceholders((String) value);
}
logKeyFound(key, propertySource, value);
return convertValueIfNecessary(value, targetValueType);
}
}
}
if (logger.isTraceEnabled()) {
logger.trace("Could not find key '" + key + "' in any property source");
}
return null;
}
// ...
}
spring 获取配置的方式就是:
- 从上到下逐个遍历 PropertySource
- 有哪个 PropertySource 能解析对应的 key 就直接返回(上层覆盖下层)
也就是类似于 Docker 分层镜像的思想,上层覆盖下层。多个相同的 key ,靠前的优先。
那我们扩展 .json 文件就可以从这里入手了:
- 给 Environment 的 PropertySource 里加一个 .json 文件的内容不就行了?
- Environment 有个子接口 ConfigurableEnvironment 可以实现对 PropertySource 的 CRUD。
经过上面的分析,通过 ConfigurableEnvironment 结合 org.springframework.boot.env.EnvironmentPostProcessor 就可以实现 .json 文件的支持了。
但是,本文不打算直接修改 ConfigurableEnvironment,因为有一个更简单的方式就是 扩展 org.springframework.boot.env.PropertySourceLoader。
顾名思义 org.springframework.boot.env.PropertySourceLoader 中的 PropertySourceLoader 就是来加载 PropertySource的 ,扩展 .json 文件的需求,我们直接实现 PropertySourceLoader 接口就行了,而没必要通过 ConfigurableEnvironment 来修改 PropertySource,直接修改 PropertySource 的话还需要考虑 多环境支持的问题,而通过 PropertySourceLoader 的话就无需考虑多环境支持了(SpringBoot会替我们搞定)。
支持 application.json 配置文件
其实核心就下面几行代码,自定义一个能加载 JSON 文件的 JsonPropertySourceLoader:
public class JsonPropertySourceLoader implements PropertySourceLoader {
@Override
public String[] getFileExtensions() {
return new String[]{"json", "json5"};
}
@Override
public List<PropertySource<?>> load(String name, Resource resource) throws IOException {
try (final InputStream inputStream = resource.getInputStream()) {
// application.json 的内容读取到字符串 jsonString 中
final String jsonString = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
// 将 jsonString 字符串转为 Map
final Map<String, Object> map = JsonFlattener.flattenAsMap(jsonString);
// 从 Map 构建一个 PropertySource
final MapPropertySource propertySource = new MapPropertySource(name, map);
return List.of(propertySource);
}
}
}
但是别忘了将我们自定义的 JsonPropertySourceLoader 加入到 spring 的 SPI 文件 spring.factories 中:
验证
- 一个简单的绑定配置的类
@Data
@ConfigurationProperties(prefix = "xxx.config.data")
public class MyDataConfig {
private List<DatasourceConfig> datasourceList = new ArrayList<>();
@Data
public static class DatasourceConfig {
private String datasourceName;
private String url;
private String username;
private String password;
private Class<? extends Driver> driverClassName;
}
}
- 配置文件如下
- 随便建个类去使用自定义的配置类来映射JSON配置
@RestController
@EnableConfigurationProperties({MyDataConfig.class})
public class MyDataConfigController {
private final MyDataConfig myDataConfig;
public MyDataConfigController(MyDataConfig myDataConfig) {
this.myDataConfig = myDataConfig;
}
@GetMapping("/my-data-config")
public MyDataConfig myDataConfig() {
return this.myDataConfig;
}
}
- postman 请求接口看看
好了,现在已经可以支持 application.json 文件了。其实关键类就是 扩展了 PropertySourceLoader 而已。
顺便引出了 SpringBoot 分层配置的思想供读者自己去探索。
如果有必要的话,我可以在再写一篇专门讲解 spring 的 Environment 的文章。