这篇文章主要讲解了关于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,或者作为Map的value,会发现绑定不上去。
原因在于,绑定默认是基于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是一个Map的value值
解决方法就是使用构造器绑定的方式,并且需要在构造器使用此注解@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#
@Value是Spring Framework的注解,不属于SpringBoot,其典型使用场景就是注入外部化配置属性,官方文档@Values介绍
@Value使用Spring内建的转化器SimpleTypeConverter,这个支持Integer,String,和逗号分割的字符串数组。
如果觉得支持不够,还是可以自定义转换支持,自定义一个Converter,然后加入到ConverterService这个Bean中,因为后面的BeanPostProcessor依赖的就是ConverterService来处理转换的
所以如果有一些复杂的配置,最好还是使用SpringBoot的方式。
@Value的优势在于,它支持SpEL,而且可以使用在任意一个Bean的方法参数或者字段上
所以这是两种不同的使用场景,看情况自己选择。
不过总体个人倾向于前面一种,因为如果在其他的Bean中直接使用@Value,万一我们要改配置的名字了,结果因为使用了@Value,遍布的到处都是,改起来很麻烦,所以从管理维护的角度来说,@Value太野了。
顺便说一下对@Value的处理位置org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#populateBean,当然这里也是处理@Inject @Autowired @Resource的地方
后记#
从配置文件到程序中使用到配置的值,一共经历两大步
如果不满足properties或者yaml格式的配置,可以自定义PropertySourceLoader,可以参考
org.springframework.boot.env.YamlPropertySourceLoader 和org.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
看完上述内容,是不是对关于SpringBoot的外部化配置使用记录的详细解析有进一步的了解,如果还想学习更多内容,欢迎关注亿速云行业资讯频道。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。