springboot自定义yml配置文件及其外部部署过程
作者:一个假的程序媛
1、序
背景:有个小项目需要后台,俺顶着Java菜逼的头衔接下来了,被男票疯狂安利spring boot,于是(被逼无奈)开始了边学边开发的躺坑之路……真香,资料超多,超好用!!!
电厂的项目,用了公司自己开发的实时数据库,后台这边就涉及到很多测点的信息需要存储到配置文件(为什么不是关系数据库真的不要问我),并且希望在部署的时候方便修改,考虑到内容颇多,放在application-pro.yml中实在不合适,就加了个point.yml。倒不是因为现场测点信息会变才需要更改,更多的是突然一拍脑袋,发现手抖写错了?
首先,因为一不小心变成了xxx.yml玩家,好好用哦,没能回去xxx.properties,传说中官方不支持像加载xxx.properties配置文件那样使用注解@PropertySource("classpath:xxx.properties")的方式加载yml配置文件,这里要说的就是加载自定义yml文件的方法。
官方说明看一下
加载自定义xxx.properties文件的方法参考这篇文章:
注意:之前在找多数据源配置的资料时,就因为资料对应的spring boot版本差异搞得很郁闷,请务必注意俺用的版本是:
spring boot 2.13
2、加载自定义yml文件
spring boot的资料非常多,多到非常容易不用动脑就解决了问题呢~项目做完之后冷静下来,觉得还是应该验证一下,毕竟打脸是为了以后有头有脸。
2.1、使用@PropertiesSource注解读取yml配置文件-简单版
按照上面给出的官宣,这条路是不行的。因为没看到文档对应的版本号,还是试一下:
# 配置文件 point.yml id: 2233 name: Ellie
(呃,这种信息为啥要叫point啊啊啊!
// 配置对应的config类 @Data @Configuration @PropertySource(value = {"classpath:point.yml"}) @ConfigurationProperties() public class TestPoint { private int id; private String name; @Override public String toString() { return "TestPoint{" + "id=" + id + ", name='" + name + '\'' + '}'; } }
随手糊了个controller来测试
@RestController public class TestConfigController { @Resource TestPoint testPoint; @ApiOperation("测试 配置文件") @RequestMapping(value = "/config") public ResultBean<String> testConfig() { return ResultBeanUtil.makeOkResp(testPoint.toString()); } }
postman搞起来
都挺好!
所以如果只是要读取这样简单的信息的话,直接使用注解@PropertiesSource是可以的,官方说的不确定的影响我也不知道是啥哦。
2.2、使用@PropertiesSource注解读取yml配置文件-不简单版?
加个list<基础类型>看看。
# point.yml id: 2233 name: Ellie cards: - XD02101263 - ZY8965 - GX0009 // 配置类 @Data @Configuration @PropertySource(value = {"classpath:point.yml"}) @ConfigurationProperties() public class TestPoint { private int id; private String name; private List<String> cards; @Override public String toString() { return "TestPoint{" + "id=" + id + ", name='" + name + '\'' + ", cards=" + cards + '}'; } }
postman
装逼失败,不行了哦。使用@Value("id")注解也是不行的呢,因为这个注解是用来匹配变量名称和配置文件不一致的情况。
按照其他博客里讲的(才糊代码一个月根本没有深入看原理的我只好是:大佬说啥就是啥),是因为使用@PropertySource注解只能加载yml配置文件,但不能将其配置信息暴露给spring environment,需要手动暴露。方法就是在让application启动的时候把下面的bean加载。
@Bean public static PropertySourcesPlaceholderConfigurer properties() { PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer(); YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean(); yaml.setResources(new ClassPathResource("point.yml")); configurer.setProperties(yaml.getObject()); return configurer; }
偷懒的我直接丢到了main函数所在的.java文件。运行:
真的不是我截错图哦。
2.3、加前缀可行版
毕竟我这么机智(无脑分析!),悄咪咪加了个前缀,前缀的名字随意取哈,与配置类中对应即可,我只是偷懒叫做prefix。
# point.yml prefix: id: 2233 name: Ellie cards: - XD02101263 - ZY8965 - GX0009 // config类 @Data @Configuration @PropertySource(value = {"classpath:point.yml"}) @ConfigurationProperties(prefix = "prefix") public class TestPoint { private int id; private String name; private List<String> cards; @Override public String toString() { return "TestPoint{" + "id=" + id + ", name='" + name + '\'' + ", cards=" + cards + '}'; } }
都挺好大结局?
求助:
这样为什么可行,俺是一点都不晓得的,如果有大佬路过,请帮忙解答!!!跪谢orz
顺便说一句,出于好奇,试了下某些博文里说的前缀加yml分隔符---配合的方式,感觉上是一本正经胡说八道,实际上也没读出来。读取List<类>也是同样可行的。
# point.yml prefix: id: 2233 name: Ellie cards: - name: XD code: XD02101263 - name: ZY code: ZY8965 - name: GX code: GX0009 // config 类 @Data @Configuration @PropertySource(value = {"classpath:point.yml"}) @ConfigurationProperties(prefix = "prefix") public class TestPoint { private int id; private String name; private List<Card> cards; @Override public String toString() { return "TestPoint{" + "id=" + id + ", name='" + name + '\'' + ", cards=" + cards + '}'; } } // 并不需要有什么与众不同的card类 @Data public class Card { private String name; private String code; @Override public String toString() { return "Card{" + "name='" + name + '\'' + ", code='" + code + '\'' + '}'; } }
请求
查找资料的过程中还看到了一种别致的写法,是为了解决多层嵌套的yml的读写,未验证,因为有选择的话,我不愿意这样写,不过写法确实很别致,哈哈哈!https://www.jb51.net/article/242026.htm
3、外部部署
其实就是把配置文件部署在jar包外部,方便修改而不必重新打包。
3.1、spring boot核心配置文件外部加载
希望外部加载自定义配置文件,需要先了解spring默认的文件加载方式。
spring程序会按优先级从下面这些路径来加载application.properties配置文件:
- 当前目录下的/config目录
- 当前目录
- classpath里的/config目录
- classpath 根目录
idea中,在源码下的classpath对应src/main/resources很明确,打包后的classpath在哪里俺是不知道的,然后就把打包后的jar包解压看了下,在BOOT-INF\classes下看到了application.yml和point.yml。所以要想覆盖配置文件,我再jar包同级目录下建了config文件夹,修改配置文件内容看覆盖是否生效。
具体操作:
- 打包的时候默认将application.yml和point.yml打包到jar中(classpath)
- 部署时,jar包同级目录下建立config文件夹,修改application.yml中端口号和point.yml内容,看修改是否生效。
修改后的point.yml文件如下:
prefix: id: 2233 name: FakeEllie cards: - name: NONE code: 00000001
测试结果:端口号修改生效(application.yml修改生效),修改后的point.yml并未生效。
毕竟自定义配置文件,一厢情愿希望spring boot按照核心文件加载方式加载point.yml,没有生效也在意料之中,不过路并没有堵死。
3.2、在@PropertySource中添加路径
查资料的时候注意到还有这种写法:
@Data @Configuration @PropertySource(value = {"file:config/point.yml"}) @ConfigurationProperties(prefix = "prefix") public class TestPoint { private int id; private String name; private List<Card> cards; @Override public String toString() { return "TestPoint{" + "id=" + id + ", name='" + name + '\'' + ", cards=" + cards + '}'; } }
就是通过file来指定文件路径,之前是classpath来指定资源相对路径,说来神奇,这种方式没有报错,但读取的内容却是classpath下的point.yml,而不是config下的point.yml。
想来是通过@ConfigurationProperties(prefix = "prefix")指定的前缀去classpath下匹配到的。跟@PropertySource(value = {"file:config/point.yml"})大概是没有关系了,忘崽牛奶真好喝。
3.3、通过YamlPropertiesFactoryBean添加路径
回想上面的描述,YamlPropertiesFactoryBean是将配置文件暴露给spring环境的,可以考虑使用它来指定文件路径。
修改bean,添加new FileSystemResource("config/point.yml")来指定config文件夹下的配置。
@Bean public static PropertySourcesPlaceholderConfigurer properties() { PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer(); YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean(); yaml.setResources(new ClassPathResource("point.yml"), new FileSystemResource("config/point.yml")); configurer.setProperties(yaml.getObject()); return configurer; }
此时配置类上使用@PropertySource(value = {"file:config/point.yml"})这种写法,返回的是
成功了?但是好像搞笑了。不过也说明了配置文件读取的顺序。config文件夹下的有最终决定权。
为了直观些,俺顺手修改jar包同级目录下config文件夹中point.yml配置文件,保证list元素个数相同:
prefix: id: 2233 name: FakeEllie cards: - name: NONE code: 00000001 - name: NONE code: 00000002 - name: NONE code: 00000003
不搞笑了。
但是,改成此时配置类上使用@PropertySource(value = {"classpath:point.yml"})后,返回并没有变化。所以YamlPropertiesFactoryBean将配置文件暴露给spring环境,说的应该就是将文件添加到spring的classpath下了,先读默认的,再读新添加这样子的。
然鹅这样就没有办法在不进行外部配置的时候使用默认的classpath下的配置文件了。
此外,通过YamlPropertiesFactoryBean添加配置文件的方式,就需要保证config/point.yml一定要存在,要想达到不进行外部配置的时候读取默认classpath下point.yml,在进行外部配置的时候读取config/point.yml。那就只好耍流氓了。
@Data @Configuration @PropertySource(value = {"file:config/point.yml", "classpath:point.yml"}, ignoreResourceNotFound = true) @ConfigurationProperties(prefix = "prefix") public class TestPoint { private int id; private String name; private List<Card> cards; @Override public String toString() { return "TestPoint{" + "id=" + id + ", name='" + name + '\'' + ", cards=" + cards + '}'; } }
重点:然后!在不进行外部配置的时候,config/point.yml内容为空,或者干脆跟classpath下的point.yml内容保持一致。
小孩子才做选择题,我全都想要
虽然看上去像个意外,但是好在意啊啊啊啊,遏制不住的好奇心啊!就是刚刚那个拼起来的返回值。
想看看是不是application.yml覆盖list也会这样,俺把配置类对应的内容举家搬迁到了application.yml中。如下:
// 配置类 @Data @Configuration @ConfigurationProperties(prefix = "prefix") public class TestPoint { private int id; private String name; private List<Card> cards; @Override public String toString() { return "TestPoint{" + "id=" + id + ", name='" + name + '\'' + ", cards=" + cards + '}'; } }
配置类默认读取application.yml。
# classpath:application.yml prefix: id: 2233 name: Ellie cards: - name: XD code: XD02101263 - name: ZY code: ZY8965 - name: GX code: GX0009 #config/application.yml prefix: id: 2233 name: FakeEllie cards: - name: NONE code: 00000001
测试结果:
并没有进行拼接啊喂!!!
在各种调换顺序看影响的时候,修改了YamlPropertiesFactoryBean添加source的顺序,返回结果发生了变化。
@Bean public static PropertySourcesPlaceholderConfigurer properties() { PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer(); YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean(); yaml.setResources(new FileSystemResource("config/point.yml"), new ClassPathResource("point.yml")); configurer.setProperties(yaml.getObject()); configurer.setIgnoreResourceNotFound(true); return configurer; }
返回结果
有一种简单的在运行时通过命令参数指定配置文件的方式效果与此类似。
java -jar demo.jar --Dspring.config.location=point.yml
俺的原则时,代码能解决的,就不要交给人来解决。
虽然没有解决任何问题,但是顺便知道了读取的先后顺序就是setResources的先后顺序。卒
所以目前的结论是,对于有list的配置,并且个数发生变化的时候,这种方式并不适用。
3.4、自定义yaml文件资源加载类
在注解@PropertySource中,有个属性factory主要用来声明解析配置文件的类,这个类必须是PropertySourceFactory接口的实现。从这里入手。
参考资料:
默认调用的是PropertySourceFactory的实现DefaultPropertySourceFactory,因此可以自定义factory实现PropertySourceFactory接口,也可以扩展DefaultPropertySourceFactory类。两种写法的效果是一样的,列出来。
直接实现PropertySourceFactory接口
public class YamlPropertyLoaderFactory implements PropertySourceFactory { @Override public PropertySource<?> createPropertySource(String name, EncodedResource encodedResource) throws IOException { List<PropertySource<?>> sources = name != null ? new YamlPropertySourceLoader().load(name, encodedResource.getResource()) : new YamlPropertySourceLoader().load( getNameForResource(encodedResource.getResource()), encodedResource.getResource()); if (sources.size() == 0) { return null; } return sources.get(0); } private static String getNameForResource(Resource resource) { String name = resource.getDescription(); if (!StringUtils.hasText(name)) { name = resource.getClass().getSimpleName() + "@" + System.identityHashCode(resource); } return name; } }
扩展DefaultPropertySourceFactory
public class YamlPropertyLoaderFactory extends DefaultPropertySourceFactory { @Override public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException { if (resource == null) { return super.createPropertySource(name, resource); } List<PropertySource<?>> sources = new YamlPropertySourceLoader().load(resource.getResource().getFilename(), resource.getResource()); if (sources.size() == 0) { return super.createPropertySource(name, resource); } return sources.get(0); } }
建议用第二种方式。
用factory的方式来实现的话,前面莫名其妙加个prefix就可以正常读取的诡异操作也不需要了哦。
使用方式如下:
@Data @Configuration @PropertySource(value = {"classpath:point.yml", "file:config/point.yml"}, factory = YamlPropertyLoaderFactory.class, ignoreResourceNotFound = true) @ConfigurationProperties public class TestPoint{ private int id; private String name; private List<Card> cards; @Override public String toString() { return "TestPoint{" + "id=" + id + ", name='" + name + '\'' + ", cards=" + cards + '}'; } } # config/point.yml id: 2233 name: FakeEllie cards: - name: NONE code: 00000001
测试结果:
自定义factory的方式,读取多种路径的配置文件时,也是有先后顺序的,就是@PropertySource中value属性指定的顺序,与使用YamlPropertiesFactoryBean将资源暴露给spring环境不同,这个不会有前面出现的“拼接”效果出现,棒呆~
以解决问题为目标和以写清楚文章为目标去看同样的问题,真的是不一样的探索路径呢,凑字数和为了flag不倒的文写的远远超出自己最初的预期,真好,超喜欢!
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。