写点什么

5 种高大上的 yml 文件读取方式,你知道吗?

作者:码农参上
  • 2022 年 4 月 24 日
  • 本文字数:5883 字

    阅读完需:约 19 分钟

5种高大上的yml文件读取方式,你知道吗?

在上一篇文章中,我们从源码角度分析了 SpringBoot 解析 yml 配置文件的全流程,那么我们今天就来点实战,总结一下除了烂大街的@Value@ConfigurationProperties外,还能够通过哪些方式,来读取 yml 配置文件的内容。

1、Environment

在 Spring 中有一个类Environment,它可以被认为是当前应用程序正在运行的环境,它继承了PropertyResolver接口,因此可以作为一个属性解析器使用。先创建一个 yml 文件,属性如下:


person:  name: hydra  gender: male  age: 18
复制代码


使用起来也非常简单,直接使用@Autowired就可以注入到要使用的类中,然后调用它的getProperty()方法就可以根据属性名称取出对应的值了。


@RestControllerpublic class EnvironmentController {    @Autowired    private Environment environment;
@GetMapping("envTest") private void getEnv(){ System.out.println(environment.getProperty("person.name")); System.out.println(environment.getProperty("person.gender"));
Integer autoClose = environment .getProperty("person.age", Integer.class); System.out.println(autoClose); String defaultValue = environment .getProperty("person.other", String.class, "defaultValue"); System.out.println(defaultValue); }}
复制代码


在上面的例子中可以看到,除了简单的获取外,Environment提供的方法还可以对取出的属性值进行类型转换、以及默认值的设置,调用一下上面的接口,打印结果如下:


hydramale18defaultValue
复制代码


除了获取属性外,还可以用来判断激活的配置文件,我们先在application.yml中激活pro文件:


spring:  profiles:    active: pro
复制代码


可以通过acceptsProfiles方法来检测某一个配置文件是否被激活加载,或者通过getActiveProfiles方法拿到所有被激活的配置文件。测试接口:


@GetMapping("getActiveEnv")private void getActiveEnv(){    System.out.println(environment.acceptsProfiles("pro"));    System.out.println(environment.acceptsProfiles("dev"));
String[] activeProfiles = environment.getActiveProfiles(); for (String activeProfile : activeProfiles) { System.out.println(activeProfile); }}
复制代码


打印结果:


truefalsepro
复制代码

2、YamlPropertiesFactoryBean

在 Spring 中还可以使用YamlPropertiesFactoryBean来读取自定义配置的 yml 文件,而不用再被拘束于application.yml及其激活的其他配置文件。


在使用过程中,只需要通过setResources()方法设置自定义 yml 配置文件的存储路径,再通过getObject()方法获取Properties对象,后续就可以通过它获取具体的属性,下面看一个例子:


@GetMapping("fcTest")public void ymlProFctest(){    YamlPropertiesFactoryBean yamlProFb = new YamlPropertiesFactoryBean();    yamlProFb.setResources(new ClassPathResource("application2.yml"));    Properties properties = yamlProFb.getObject();    System.out.println(properties.get("person2.name"));    System.out.println(properties.get("person2.gender"));    System.out.println(properties.toString());}
复制代码


查看运行结果,可以读取指定的application2.yml的内容:


susanfemale{person2.age=18, person2.gender=female, person2.name=susan}
复制代码


但是这样的使用中有一个问题,那就是只有在这个接口的请求中能够取到这个属性的值,如果再写一个接口,不使用YamlPropertiesFactoryBean读取配置文件,即使之前的方法已经读取过这个 yml 文件一次了,第二个接口取到的仍然还是空值。来对这个过程进行一下测试:


@Value("${person2.name:null}")private String name;@Value("${person2.gender:null}")private String gender;
@GetMapping("fcTest2")public void ymlProFctest2(){ System.out.println(name); System.out.println(gender);}
复制代码


先调用一次fcTest接口,再调用fcTest2接口时会打印null值:


nullnull
复制代码


想要解决这个问题也很简单,可以配合PropertySourcesPlaceholderConfigurer使用,它实现了BeanFactoryPostProcessor接口,也就是一个 bean 工厂后置处理器的实现,可以将配置文件的属性值加载到一个Properties文件中。使用方法如下:


@Configurationpublic class PropertyConfig {    @Bean    public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() {        PropertySourcesPlaceholderConfigurer configurer             = new PropertySourcesPlaceholderConfigurer();        YamlPropertiesFactoryBean yamlProFb             = new YamlPropertiesFactoryBean();        yamlProFb.setResources(new ClassPathResource("application2.yml"));        configurer.setProperties(yamlProFb.getObject());        return configurer;    }}
复制代码


再次调用之前的接口,结果如下,可以正常的取到application2.yml中的属性:


susanfemale
复制代码


除了使用YamlPropertiesFactoryBean将 yml 解析成Properties外,其实我们还可以使用YamlMapFactoryBean解析 yml 成为Map,使用方法非常类似:


@GetMapping("fcMapTest")public void ymlMapFctest(){    YamlMapFactoryBean yamlMapFb = new YamlMapFactoryBean();    yamlMapFb.setResources(new ClassPathResource("application2.yml"));    Map<String, Object> map = yamlMapFb.getObject();    System.out.println(map);}
复制代码


打印结果:


{person2={name=susan, gender=female, age=18}}
复制代码

3、监听事件

在上篇介绍原理的文章中,我们知道 SpringBoot 是通过监听事件的方式来加载和解析的 yml 文件,那么我们也可以仿照这个模式,来加载自定义的配置文件。


首先,定义一个类实现ApplicationListener接口,监听的事件类型为ApplicationEnvironmentPreparedEvent,并在构造方法中传入要解析的 yml 文件名:


public class YmlListener implements     ApplicationListener<ApplicationEnvironmentPreparedEvent> {    private String ymlFilePath;    public YmlListener(String ymlFilePath){        this.ymlFilePath = ymlFilePath;    }    //...}
复制代码


自定义的监听器中需要实现接口的onApplicationEvent()方法,当监听到ApplicationEnvironmentPreparedEvent事件时会被触发:


@Overridepublic void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {    ConfigurableEnvironment environment = event.getEnvironment();    ResourceLoader loader = new DefaultResourceLoader();    YamlPropertySourceLoader ymlLoader = new YamlPropertySourceLoader();    try {        List<PropertySource<?>> sourceList = ymlLoader            .load(ymlFilePath, loader.getResource(ymlFilePath));        for (PropertySource<?> propertySource : sourceList) {            environment.getPropertySources().addLast(propertySource);        }    } catch (IOException e) {        e.printStackTrace();    }}
复制代码


上面的代码中,主要实现了:


  • 获取当前环境Environment,当ApplicationEnvironmentPreparedEvent事件被触发时,已经完成了Environment的装载,并且能够通过event事件获取

  • 通过YamlPropertySourceLoader加载、解析配置文件

  • 将解析完成后的OriginTrackedMapPropertySource添加到Environment


修改启动类,在启动类中加入这个监听器:


public static void main(String[] args) {    SpringApplication application = new SpringApplication(MyApplication.class);    application.addListeners(new YmlListener("classpath:/application2.yml"));    application.run(args);}
复制代码


在向environment中添加propertySource前加一个断点,查看环境的变化:



执行完成后,可以看到配置文件源已经被添加到了环境中:



启动完成后再调用一下接口,查看结果:


susanfemale
复制代码


能够正确的取到配置文件中的值,说明自定义的监听器已经生效。

4、SnakeYml

前面介绍的几种方式,在 Spring 环境下无需引入其他依赖就可以完成的,接下来要介绍的SnakeYml在使用前需要引入依赖,但是同时也可以脱离 Spring 环境单独使用。先引入依赖坐标:


<dependency>    <groupId>org.yaml</groupId>    <artifactId>snakeyaml</artifactId>    <version>1.23</version></dependency>
复制代码


准备一个 yml 配置文件:


person1:  name: hydra  gender: maleperson2:  name: susan  gender: female
复制代码


在使用SnakeYml解析 yml 时,最常使用的就是loadloadlAllloadAs方法,这三个方法可以加载 yml 文件或字符串,最后返回解析后的对象。我们先从基础的load方法开始演示:


public void test1(){    Yaml yaml=new Yaml();    Map<String, Object> map =            yaml.load(getClass().getClassLoader()                    .getResourceAsStream("snake1.yml"));    System.out.println(map);}
复制代码


运行上面的代码,打印 Map 中的内容:


{person1={name=hydra, gender=male}, person2={name=susan, gender=female}}
复制代码


接下来看一下loadAll方法,它可以用来加载 yml 中使用---连接符连接的多个文档,将上面的 yml 文件进行修改:


person1:  name: hydra  gender: male---person2:  name: susan  gender: female
复制代码


在添加了连接符后,尝试再使用load方法进行解析,报错如下显示发现了另一段 yml 文档从而无法正常解析:



这时候修改上面的代码,使用loadAll方法:


public void test2(){    Yaml yaml=new Yaml();    Iterable<Object> objects =         yaml.loadAll(getClass().getClassLoader()            .getResourceAsStream("snake2.yml"));    for (Object object : objects) {        System.out.println(object);    }}
复制代码


执行结果如下:


{person1={name=hydra, gender=male}}{person2={name=susan, gender=female}}
复制代码


可以看到,loadAll方法返回的是一个对象的迭代,里面的每个对象对应 yml 中的一段文档,修改后的 yml 文件就被解析成了两个独立的 Map。


接下来再来看一下loadAs方法,它可以在 yml 解析过程中指定类型,直接封装成一个对象。我们直接复用上面的snake1.yml,在解析前先创建两个实体类对象用于接收:


@Datapublic class Person {    SinglePerson person1;    SinglePerson person2;}
@Datapublic class SinglePerson { String name; String gender;}
复制代码


下面使用loadAs方法加载 yml,注意方法的第二个参数,就是用于封装 yml 的实体类型。


public void test3(){    Yaml yaml=new Yaml();    Person person =         yaml.loadAs(getClass().getClassLoader().            getResourceAsStream("snake1.yml"), Person.class);    System.out.println(person.toString());}
复制代码


查看执行结果:


Person(person1=SinglePerson(name=hydra, gender=male), person2=SinglePerson(name=susan, gender=female))
复制代码


实际上,如果想要将 yml 封装成实体对象,也可以使用另一种方法。在创建Yaml对象的时候,传入一个指定实体类的构造器对象,然后直接调用load方法就可以实现:


public void test4(){    Yaml yaml=new Yaml(new Constructor(Person.class));    Person person = yaml.load(getClass().getClassLoader().            getResourceAsStream("snake1.yml"));    System.out.println(person.toString());}
复制代码


执行结果与上面相同:


Person(person1=SinglePerson(name=hydra, gender=male), person2=SinglePerson(name=susan, gender=female))
复制代码


SnakeYml 其实实现了非常多的功能,这里就不一一列举了,有兴趣的小伙伴可以自己查看一下文档。如果你看了上一篇的文章后跟着翻阅了一下源码,那么你会发现,其实在 SpringBoot 的底层,也是借助了 SnakeYml 来进行的 yml 的解析操作。

5、jackson-dataformat-yaml

相比大家平常用 jackson 比较多的场景是用它来处理 json,其实它也可以用来处理 yml,使用前需要引入依赖:


<dependency>    <groupId>com.fasterxml.jackson.dataformat</groupId>    <artifactId>jackson-dataformat-yaml</artifactId>    <version>2.12.3</version></dependency>
复制代码


使用 jackson 读取 yml 也非常简单,这里用到了常用的ObjectMapper,在创建ObjectMapper对象时指定使用YAML工厂,之后就可以简单的将 yml 映射到实体:


public void read() throws IOException {    ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory());    InputStream input =        new FileInputStream("F:\\Work\\yml\\src\\main\\resources\\snake1.yml");    Person person = objectMapper.readValue(input, Person.class);    System.out.println(person.toString());}
复制代码


运行结果:


Person(person1=SinglePerson(name=hydra, gender=male), person2=SinglePerson(name=susan, gender=female))
复制代码


如果想要生成 yml 文件的话,可以调用ObjectMapperwriteValue方法实现:


public void write() throws IOException {    Map<String,Object> map=new HashMap<>();    SinglePerson person1 = new SinglePerson("Trunks", "male");    SinglePerson person2 = new SinglePerson("Goten", "male");    Person person=new Person(person1,person2);    map.put("person",person);
ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory()); objectMapper .writeValue(new File("F:\\Work\\yml\\src\\main\\resources\\jackson-gen.yml"),map);}
复制代码


查看生成的 yml 文件,可以看到 jackson 对字符串类型严格的添加了引号,还在文档的开头添加了 yml 的链接符。至于其他 jackson 读写 yml 的复杂功能,大家可以在工作中自己去探索使用。


总结

本文介绍了 5 种读取 yml 配置文件的方式,前 3 种依赖于 Spring 环境,而SnakeYmlJackson则可以脱离环境独立使用,可以说它们是对@Value@ConfigurationProperties注解使用的补充。这几种方法的使用场景不同,也各有各的有优点,各自具备一些特殊的用法,而我们在工作中更多情况下,要根据具体的用途进行一种方案的选取或多种的搭配使用。


好了,希望这篇实战能够帮助到大家,我是 Hydra,我们下篇再见。


最后


如果觉得对您有所帮助,小伙伴们可以点个赞啊,非常感谢~

公众号码农参上,一个热爱分享的公众号,有趣、深入、直接,与你聊聊技术。欢迎来加 Hydra 好友,做个点赞之交。

发布于: 刚刚阅读数: 2
用户头像

码农参上

关注

公众号:码农参上 2021.03.30 加入

公众号【码农参上】,有趣、深入、与你聊聊技术。

评论

发布
暂无评论
5种高大上的yml文件读取方式,你知道吗?_springboot_码农参上_InfoQ写作社区