关于SpringBoot的外部化配置使用记录

声明: 若有任何纰漏、错误请不吝指出!


更新: 工作中突然想起来,关于Yaml的使用,并不属于Spring的范畴,是org.yaml.snakeyaml处理的。所以yaml的使用应该参考官方,不过貌似打不开。。。

Spring利用snakeyaml将配置解析成PropertySource,然后写入到Environment,就能使用了

记录下使用SpringBoot配置时遇到的一些麻烦,虽然这种麻烦是因为知识匮乏导致的。

记录下避免一段时间后自己又给忘记了,以防万一。

如果放到博客里能帮助到遇到同样问题的同志,自是极好!

SpringBoot的外部化配置,主要就是指平时用到的一些配置文件,这些配置由于不是硬编码,放在了配置文件中,所以相对来说是一个外部化的配置Externalized Configuration

SpringBoot官方外部化配置的在线文档Externalized Configuration

初级用法

SpringBoot对配置提供了极大的便利,仅仅需要编写一个Yaml文件或者Properties文件,按照其规定的格式,书写好我们的配置信息,然后编写一个相应的Java类,使用注解@ConfigurationProperties@Configuration配合使用,或者@Configuration@Value配合使用,即可将配置的值,映射到我们配置类或者JavaBean中。

有如下Java配置类

@Configuration
@ConfigurationProperties(prefix="spring.server")
public class AppConfig{
private String name;
private String port;
public void setName(String name){
this.name = name;
}
public void setPort(String port){
this.port = port;
}
}

如下配置文件,

Yaml格式配置文件

# application.yml
spring:
server:
name: spring-app
port: 9527

Properties格式配置文件

# application.properties
spring.server.name=spring-app
spring.server.port=9237

使用@ConfigurationProperties,必须要有Setter方法,绑定时是通过Setter方法绑定的

参见org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor#postProcessBeforeInitialization,这是一个BeanPostProcessor

这样在SpringBoot中,我们就可以将AppConfig这个Bean注入到别的Bean中使用我们的配置了。

以上这些在开发中基本上也就满足需要了,大部分我们的配置都很简单,通常都是数值型的和字符串。

但是,凡事不能绝对。

高级用法

以下配置参考这位

Array/List

假如有如下需求,应用仅对几个有限的IP开放访问,然后我们想把这几个获得许可的IP地址写在配置文件中。

这个时候如果配置解析仅仅支持字符串和数值型的话,就需要我们自己获取到配置值以后,再去进行一些后续的处理,比如转换成数组或者列表。

好在,优秀的框架,总能满足大部分的需求,SpringBoot是直接配置直接到数组或者列表的映射到

使用方式

Java配置类

@Configuration
@ConfigurationProperties(prefix="allows")
public class AllowedAccessConfig{
private String[] ipList; // 字段类型可以是 List<String>
public void setPort(String[] port){
this.ipList = ipList;
}
}

配置文件

# application.yml
allows:
ipList:
- 192.168.1.1
- 192.168.1.2
- 192.168.1.3
- 192.168.1.4
# or
allows:
ipList: 192.168.1.1, 192.168.1.2, 192.168.1.3, 192.168.1.4
# application.properties
allows.ipList[0]=192.168.1.1
allows.ipList[1]=192.168.1.2
allows.ipList[2]=192.168.1.3
allows.ipList[3]=192.168.1.4
# or
allows.ipList= 192.168.1.1, 192.168.1.2, 192.168.1.3, 192.168.1.4

Map

如果数组或者列表不满足需求,需要key-vlaue型的,没问题,SpringBoot也是支持的。

假设一个对接方不同的业务,使用了不同的AES密钥,那么在配置的时候,就要根据业务类型作为key,对应的密钥作为value

Java配置类

@Configuration
@ConfigurationProperties(prefix="aes.keys")
public class AesKeyConfig{
private Map<String,String> keys;
public void setKeys(Map<String,String> keys){
this.keys = keys;
}
}

配置文件

# application.yml
aes:
keys:
order: 28jsaS2asf2fSA2
pay: @ra@3safdsR5&sDa
# or
aes:
keys[order]: 28jsaS2asf2fSA2
keys[pay]: @ra@3safdsR5&sDa
# application.properties
aes.keys.order=28jsaS2asf2fSA2
aes.keys.pay=@ra@3safdsR5&sDa
# or
aes.keys[order]=28jsaS2asf2fSA2
aes.keys[pay]=@ra@3safdsR5&sDa

Enum

枚举?那必须支持

不过实际意义不怎么大,如果配置的值要可以转换成枚举值的话,配置的值必须和枚举值的name一致,大小写都不能差,因为SpringBoot实现的配置到枚举的转换,使用的是

/* java.lang.Enum#valueOf */
public static <T extends Enum<T>> T valueOf(Class<T> enumType,
String name) {
// 这里的name就是枚举值的字符表示,一般都是大写的
T result = enumType.enumConstantDirectory().get(name);
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum constant " + enumType.getCanonicalName() + "." + name);
}

关于这段代码的理解,可以参考另外一片文章深入理解Java枚举

如果枚举还有其他字段的话,就没办法了

JavaBean

什么? 还是不能满足?想要直接把配置绑定到一个JavaBean

干就完事了!

JavaBean

@Configuration
@ConfigurationProperties(prefix="upload")
public class UploadConfig{
private String rootPath;
private String fileType;
private int fileSize;
private boolean rename;
// 省略 Setter方法
}

配置文件

# application.yml
upload:
root-path: /xx/xx/xx
file-type: zip
fileSize: 1M
rename: false
# application.properties
upload.rootPath=/xx/xx/xx
upload.fileType=zip
upload.fileSize=1M
upload.rename=false

以上几种用法,可以组合使用,非常的灵活

不过如果是JavaBean的数组或者List,或者作为Mapvalue,会发现绑定不上去。

原因在于,绑定默认是基于Setter方法,进行单个字段的绑定,赋值,而这里要的是一个JavaBean,需要创建一个JavaBean对象,再去做属性绑定赋值。

如果按照这两步走,也可以做到成功绑定到一个作为元素的JavaBean对象。

只是SpringBoot并没有那么做,而是提供了一个@ConstructorBinding注解,让我们使用构造器绑定数据。

复杂配置

JavBean

@Configuration
@ConfigurationProperties(prefix="app")
public class AppConfig{ private Map<String, DataSourceMetadata> multiDataSourceMap; public void setMultiDataSourceMap(Map<String, DataSourceMetadata> multiDataSourceMap){
this.multiDataSourceMap = multiDataSourceMap;
}
} public class DataSourceMetadata{
private String url;
private String driverClass;
private String username;
private String passowrd;
// 省略Setter和Getter
}

配置文件

app:
multiDataSourceMap:
ds1:
url: jdbc://
driver-class: com.mysql.cj.Driver
username: xxx
password: xxx
ds2:
url: jdbc://
driver-class: com.mysql.cj.Driver
username: 12sds
password: adfwqw
# or
app:
multiDataSourceMap:
ds1: {
url: jdbc://
driver-class: com.mysql.cj.Driver
username: xxx
password: xxx
} ds2: {
url: jdbc://
driver-class: com.mysql.cj.Driver
username: 12sds
password: adfwqw
}

然后启动,走起,立马会发现熟悉又可气的NPE

原因很简单,SpringBoot没能从配置文件读取相应的配置数据并且实例化一个Map,因为

它现在面对的情况比以前复杂了,现在的JavaBean是一个Mapvalue

解决方法就是使用构造器绑定的方式,并且需要在构造器使用此注解@ConstructorBinding

public class DataSourceMetadata{
private String url;
private String driverClass;
private String username;
private String passowrd;
@ConstructorBinding
public DataSourceMetadata(String url, String driverClass, String username, String passowrd){
this.url = url;
this.driverClass = driverClass;
this.username = username;
this.password = password;
}
// 省略Setter和Getter
}

只要这么一搞就正常了,不会有烦人的NPE

我并不知道是否有别的方式也可以做到,比如继续使用Setter方法来进行数据绑定

瘦身计划

上面的这些配置,如果都有的话,全部写到application.yml或者application.properties文件中,会导致配置文件内容太多,而且各种配置混合在一起,不便于管理和维护。

如果需要改动某个配置,就要改动整个文件,存在一定的风险导致其他配置被误改。

所以应该一类配置,放到一起去管理。

同样的,一类配置通常对应一个功能,如果其中一项配置的改动,那么相应的测试,也能保证同一个配置文件的修改不会引发其他问题。

所以有必要将application.yml拆分了。

花了一番力气,拆分了一个出来upload.yml,然后使用如下方式引入配置文件

配置文件默认是放在 resources目录下(maven/gradle),配置文件在编译打包后,会位于classes的根目录下,也就是我们所谓的classpath

@Configuration
@PropertySource("classpath:upload.yml")
@ConfigurationProperties(prefix="upload")
public class UploadConfig{
private String rootPath;
private String fileType;
private int fileSize;
private boolean rename;
// 省略 Setter方法
}

问题来了,死活没法将数据绑定到JavaBean的属性上。

Debug看源码,陷进去出不来。试着使用profile来解决,虽然可以解决,但是毕竟不是同一个使用场景,并不合适。

最后找人求救,告知@PropertySource不支持yaml文件,仅支持properties,于是试了下,果然是的

SpringBoot版本是2.2.6,有个群友说他1.5的还是支持的,不过SpringBoot官方明确写到不支持的

2.7.4. YAML Shortcomings

YAML files cannot be loaded by using the @PropertySource annotation. So, in the case that you need to load values that way, you need to use a properties file.

上面看到,其实yaml配置更有优势一些,所以如果想继续使用yaml的话,也不是不可以

@PropertySource支持自定义文件格式

// 这里继承了DefaultPropertySourceFactory,也可以直接实现PropertySourceFactory
public class YamlPropertySourceFactory extends DefaultPropertySourceFactory { public YamlPropertySourceFactory () {
super();
} @Override
public PropertySource<?> createPropertySource (String name, EncodedResource resource)
throws IOException {
// 这个判断是有必要的,因为直接使用name是null,没深究原因
String nameToUse = name != null ? name : resource.getResource().getFilename();
// yml文件,使用YamlPropertiesFactoryBean来从yaml文件Resource中构建一个Properties对象
// 然后使用PropertiesPropertySource封装成PropertySource对象,就能加入到Environment
if (nameToUse.endsWith(".yml")) {
YamlPropertiesFactoryBean factoryBean = new YamlPropertiesFactoryBean();
factoryBean.setResources(resource.getResource());
factoryBean.afterPropertiesSet();
return new PropertiesPropertySource(nameToUse, factoryBean.getObject());
}
// 如果不是yml配置文件,使用默认实现
return super.createPropertySource(name, resource);
}
}

使用时,@PropertySource(factory=YamlPropertySourceFactory.class)即可。

使用@Value

@ValueSpring Framework的注解,不属于SpringBoot,其典型使用场景就是注入外部化配置属性,官方文档@Values介绍

@Value使用Spring内建的转化器SimpleTypeConverter,这个支持IntegerString,和逗号分割的字符串数组。

如果觉得支持不够,还是可以自定义转换支持,自定义一个Converter,然后加入到ConverterService这个Bean中,因为后面的BeanPostProcessor依赖的就是ConverterService来处理转换的

所以如果有一些复杂的配置,最好还是使用SpringBoot的方式。

@Value的优势在于,它支持SpEL,而且可以使用在任意一个Bean的方法参数或者字段上

所以这是两种不同的使用场景,看情况自己选择。

不过总体个人倾向于前面一种,因为如果在其他的Bean中直接使用@Value,万一我们要改配置的名字了,结果因为使用了@Value,遍布的到处都是,改起来很麻烦,所以从管理维护的角度来说,@Value太野了。

顺便说一下对@Value的处理位置org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#populateBean,当然这里也是处理@Inject @Autowired @Resource的地方

后记

从配置文件到程序中使用到配置的值,一共经历两大步

  1. 读取配置内容到Environment中,无论任何形式的配置,最后都是一个Property Source
  2. 通过BeanPostProcessor来进行配置值绑定注入

如果不满足properties或者yaml格式的配置,可以自定义PropertySourceLoader,可以参考

org.springframework.boot.env.YamlPropertySourceLoaderorg.springframework.boot.env.PropertiesPropertySourceLoader


补充更新

在使用@ConfigurationProperties时,可以不用有对应的字段定义,如果需要对注入的配置值,在Setter方法中转换成其他类型时。

因为这种绑定方式直接通过Setter方法来做的(其实@Value也可以注解在方法上),并不会检查是否存在这个字段的定义。

JavaConfig

@Configuration
@ConfigurationProperties("config")
public class Config{
Map<BizType, Metadata> bizMetaMap = new ConcurrentHashMap<>(5);
public void setMetadatas(List<Metadata> metas){
for(Metadata meta: metas){
bizMetaMap.put(BizType.forCode(),meta);
}
}
}

yaml配置

config:
metadatas:
-
name: xxx
age: xxx

这样就可以了,不需要实际有对应的字段存在与之对应,能找到对应的Setter方法即可!

关于SpringBoot的外部化配置使用记录的更多相关文章

  1. SpringBoot的外部化配置最全解析!

    目录 SpringBoot中的配置解析[Externalized Configuration] 本篇要点 一.SpringBoot官方文档对于外部化配置的介绍及作用顺序 二.各种外部化配置举例 1.随 ...

  2. SpringBoot外部化配置使用Plus版

    本文如有任何纰漏.错误,请不吝指正! PS: 之前写过一篇关于SpringBoo中使用配置文件的一些姿势,不过嘛,有句话(我)说的好:曾见小桥流水,未睹观音坐莲!所以再写一篇增强版,以便记录. 序言 ...

  3. SpringBoot 正式环境必不可少的外部化配置

    前言 <[源码解析]凭什么?spring boot 一个 jar 就能开发 web 项目> 中有读者反应: 部署后运维很不方便,比较修改一个 IP 配置,需要重新打包. 这一点我是深有体会 ...

  4. SpringBoot官方文档学习(二)Externalized Configuration(外部化配置)

    Spring Boot允许您将配置外部化,以便可以在不同的环境中使用相同的应用程序代码.您可以使用属性文件.YAML文件.环境变量和命令行参数来具体化配置.属性值可以通过使用@Value注释直接注入b ...

  5. Dubbo 新编程模型之外部化配置

    外部化配置(External Configuration) 在Dubbo 注解驱动例子中,无论是服务提供方,还是服务消费方,均需要转配相关配置Bean: @Bean public Applicatio ...

  6. Spring Boot外部化配置实战解析

    一.流程分析 1.1 入口程序 在 SpringApplication#run(String... args) 方法中,外部化配置关键流程分为以下四步 public ConfigurableAppli ...

  7. Spring Boot 外部化配置(一)- Environment、ConfigFileApplicationListener

    目录 前言 1.起源 2.外部化配置的资源类型 3.外部化配置的核心 3.1 Environment 3.1.1.ConfigFileApplicationListener 3.1.2.关联 Spri ...

  8. 玩转Spring Boot 自定义配置、导入XML配置与外部化配置

    玩转Spring Boot 自定义配置.导入XML配置与外部化配置       在这里我会全面介绍在Spring Boot里面如何自定义配置,更改Spring Boot默认的配置,以及介绍各配置的优先 ...

  9. 曹工谈Spring Boot:Spring boot中怎么进行外部化配置,一不留神摔一跤;一路debug,原来是我太年轻了

    spring boot中怎么进行外部化配置,一不留神摔一跤:一路debug,原来是我太年轻了 背景 我们公司这边,目前都是spring boot项目,没有引入spring cloud config,也 ...

随机推荐

  1. 什么是LVM

    LVM是逻辑盘卷管理(Logical Volume Manager)的简称,它是Linux环境下对磁盘分区进行管理的一种机制,LVM是建立在硬盘和分区之上的一个逻辑层,来提高磁盘分区管理的灵活性.前面 ...

  2. ES6让字符串String增加了哪些好玩的特性呢?

    确实因为现在天气变热了,所以一天天的这么写我也很累.所以如果阅读的时候有什么错误还请大家指出来,不好意思.学习永无止境. OK,今天继续讲解ES6系列知识 学过上一节的解构赋值就知道,ES6确实给我们 ...

  3. 可以用 Python 编程语言做哪些神奇好玩的事情?除了生孩子不能,其他全都行!

    坦克大战 源自于一个用Python写各种小游戏的github合集,star数1k.除了坦克大战外,还包含滑雪者.皮卡丘GOGO.贪吃蛇.推箱子.拼图等游戏. 图片转铅笔画 帮助你快速生成属于自己的铅笔 ...

  4. C1FlexGrid双grid滚动条联动

    利用AfterScroll事件,来实现双grid联动. private void Grid_AfterScroll(object sender, RangeEventArgs e) { try { i ...

  5. JS静态变量和静态函数

    本文链接:https://blog.csdn.net/u012790503/article/details/46278521 function A(){this.id = "我是AA&quo ...

  6. thinkphp5.0 配置文件加载路径说明

    在thinphp5.0框架里,js,css等配置文件都是加载在/public/static的目录下,所以要引用这些文件,路径必须是要写好的,代码如图: return [ // 默认模块名 'defau ...

  7. python学习01python入门一

    在正式开启python代码的学习之旅之前,先来了解一下有关python语言的一些内容吧. python的命名,起源等等,大家自行百度了解,此处不再赘述. python是一种解释型语言,具有强调代码可读 ...

  8. linux抓包的实现

    工具: wireshark tcpdump 在这里仅仅介绍后者: 在网络问题的调试中,tcpdump应该说是一个必不可少的工具,和大部分linux下优秀工具一样,它的特点就是简单而强大. 默认情况下, ...

  9. [Qt]执行cmd命令

    要加 /c 参数 QProcess p; p.start("cmd", QStringList()<<"/c"<<"ping ...

  10. Linux监听磁盘使用情况

    前阵子服务器磁盘写满了,导致项目出了很多奇怪的问题,比如文件上传不了(这个很好理解),还有登录时验证码无法加载(现在依旧不知道原因,项目的验证码图片是只在内存中生成的BufferedImage对象,不 ...