7.PropertySource加载Yaml配置文件实例演示

一灰灰blogSpringBoot基础系列配置PropertySource约 2114 字大约 7 分钟

在之前有介绍过借助注解@PropertySource来引入自定义的配置文件,在当时遇到抛出了一个问题,通过这个注解可以正确获取到.properties文件的配置信息,但是yaml文件却读取不到,最近又碰到这个问题,正好把之前挖的坑填上;本文将主要定位一下,为啥yml文件读取不了,又可以如何处理

如对之前博文有兴趣的小伙伴,可以查看: 180921-SpringBoot基础篇配置信息之自定义配置指定与配置内引用open in new window

I. 项目环境

1. 基本配置

本文后续的源码定位以及实例演示都是基于SpringBoot 2.2.1.RELEASE进行,如需复现本文中的case,请确保环境一致

  • IDEA
  • MAVEN
  • SpringBoot 2.2.1.RELEASE
  • JDK1.8

2. 实例项目

创建一个SpringBoot项目,用于后续的演示,首先创建一个配置文件biz.properties

biz.token=mytoken
biz.appKey=asdf
biz.appVersion=1
biz.source=xxx.yyy

biz.uuid=${biz.token}#${biz.appKey}

接下来定义对应的配置类

@Data
@Configuration
@PropertySource({"classpath:biz.properties"})
@ConfigurationProperties(prefix = "biz")
public class OtherProperBean {
    private String token;
    private String appKey;
    private Integer appVersion;
    private String source;
    private String uuid;
}

最后补上SpringBoot项目不可获取的启动类

/**
 * Created by @author yihui in 14:08 18/9/19.
 */
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }
}

II. PropertySource原理分析

想要定位为啥@PropertySource注解只会获取到properties文件的配置,而不能获取yaml文件配置信息,最直接的办法当然是直接撸源码(实际上最简单的办法直接借助搜索引擎,看一下有没有哪位大佬有过相关分享,如果不是为了写本文,我可是完全没想开撸,毕竟从提出这个问题到现在回复,也过了两年多了😭...)

1. 源码定位

那么这个源码可以怎么定位分析呢,先直接进入这个注解瞅一下

public @interface PropertySource {
  // ... 省略无关的属性

	Class<? extends PropertySourceFactory> factory() default PropertySourceFactory.class;
}

请注意上面的特意留出来的PropertySourceFactory, 从命名上来看,大致就能感觉这个工厂类与属性有关了,主要就是为了创建PropertySource对象

它就比较有意思了,如果没有猜错的话,配置文件加载到Spring容器之后,多半就会与PropertySource关联起来了(所以说好的命名可以省很多注释说明)

接下来看一下这个工厂类的默认实现DefaultPropertySourceFactory,源码很简单

public class DefaultPropertySourceFactory implements PropertySourceFactory {

	@Override
	public PropertySource<?> createPropertySource(@Nullable String name, EncodedResource resource) throws IOException {
		return (name != null ? new ResourcePropertySource(name, resource) : new ResourcePropertySource(resource));
	}

}

在这里我们打个断点,确认一下会发生什么神器的事情

从上面的截图可以看到,这个EncodedResource包含了我们指定的配置文件,直接单步进去,可以看到执行的时候下面这个

// org.springframework.core.io.support.ResourcePropertySource#ResourcePropertySource(org.springframework.core.io.support.EncodedResource)
public ResourcePropertySource(EncodedResource resource) throws IOException {
		super(getNameForResource(resource.getResource()), PropertiesLoaderUtils.loadProperties(resource));
		this.resourceName = null;
}

请注意,核心代码不是super()这个构造方法,而是传参的PropertiesLoaderUtils.loadProperties(resource)

上面这一行调用,就是实现具体的从配置文件中获取配置信息

下面是具体的实现(摘抄有用的部分逻辑)

// org.springframework.core.io.support.PropertiesLoaderUtils
public static Properties loadProperties(EncodedResource resource) throws IOException {
	Properties props = new Properties();
	fillProperties(props, resource);
	return props;
}

public static void fillProperties(Properties props, EncodedResource resource)
		throws IOException {
  // 属性填充,注意DefaultPropertiesPersister
	fillProperties(props, resource, new DefaultPropertiesPersister());
}

static void fillProperties(Properties props, EncodedResource resource, PropertiesPersister persister)
		throws IOException {
  ...
	try {
		String filename = resource.getResource().getFilename();
		if (filename != null && filename.endsWith(XML_FILE_EXTENSION)) {
			stream = resource.getInputStream();
			// 这个是关键
			persister.loadFromXml(props, stream);
		}
		else if (resource.requiresReader()) {
			reader = resource.getReader();
			// 关键调用
			persister.load(props, reader);
		}
		else {
			stream = resource.getInputStream();
			// 关键调用
			persister.load(props, stream);
		}
	}
	...
}

配置信息的读取,最终依靠的就是org.springframework.util.DefaultPropertiesPersister#load(),到这里我们基本上就找到了从配置文件中读取配置的“幕后黑手”,直接看一下它的实现逻辑就能知道为啥不支持yaml了

public class DefaultPropertiesPersister implements PropertiesPersister {

	@Override
	public void load(Properties props, InputStream is) throws IOException {
		props.load(is);
	}

	@Override
	public void load(Properties props, Reader reader) throws IOException {
		props.load(reader);
	}
}

直接进入看到源码,非常简单直观的实现方式了,直接使用jdk的java.util.Properties#load(java.io.InputStream)来读取配置文件,所以真相已经大白了(原来都是jdk的锅😂)

2. yaml文件支持

经过上面的一番操作,我们知道@ConfigurationProperties加载配置文件,主要是借助jdk的Properties#load方法来读取配置文件到容器内,那么若我们希望加载yaml配置文件,可以怎么搞呢?

因为SpringBoot是支持yaml配置文件的读取的,所以我们完全可以扩展一下,借助SpringBoot的工具类来实现配置文件加载,所以可以实现自定义的PropertySourceFactory

public class YamlSourceFactory extends DefaultPropertySourceFactory {

    @Override
    public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
        if (resource == null) {
            return super.createPropertySource(name, resource);
        }

        // 这里使用Yaml配置加载类来读取yml文件信息
        List<PropertySource<?>> sources = new YamlPropertySourceLoader().load(resource.getResource().getFilename(), resource.getResource());
        return sources.get(0);
    }
}

然后再我们希望使用的地方,利用自定义的工厂类替换默认的即可

@Data
@Configuration
@PropertySource(value = {"classpath:biz2.yml"}, factory = YamlSourceFactory.class)
@ConfigurationProperties(prefix = "biz2.yml")
public class YmlProperties {

    private Integer type;

    private String name;

    private List<Map<String, String>> ary;
}

对应的配置文件如下

biz2:
  yml:
    type: 1
    name: biz.yml.name
    ary:
      - a: hello
      - b: world

最后实例验证一下

@SpringBootApplication
public class Application {

    public Application(YmlProperties ymlProperties) {
        System.out.println(ymlProperties);
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }
}

3. 小结

当我们希望加载自定义的配置文件时,@PropertySource注解是一个非常好的选择(当然也可以借助多环境配置方案,指定spring.profiles.active的值,实现加载前缀为application-的配置文件,有兴趣的小伙伴可以查看我之前的博文)

请注意@PropertySource引入的配置文件不支持yaml文件,如需支持,可以参考本文中的实现方式,自定义一个yaml文件的PropertySourceFactory

最后提一句,遇到问题千万不要放过,尽量迅速解决,不要留待以后,不然拖延症发作的话,这个时间可能就一直悬着了...

III. 其他

0. 项目

项目源码

系列博文

Loading...