Spring Boot 2.X 装载 yaml 配置文件的键值对

Posted 福州-司马懿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Boot 2.X 装载 yaml 配置文件的键值对相关的知识,希望对你有一定的参考价值。

加载 yaml 配置文件

目前主要有3种方案

  1. @Value
  • 适用于简单类型的注入(不支持复杂类型封装注解)
  • 只能一个个指定
  • 支持 SpringEl 语法
  1. @ConfigurationProperties(prefix = “A.B.C”)
  • 可以根据变量名批量注入
  • 且只需要指定一个前缀,就能绑定有这个前缀的所有属性值
  • 不支持 SpringEl 语法
  • 支持JSR303进行配置文件值及校验
  1. Environment
  • 仅支持 boolean、int、string 和 list 的行内写法,功能比 @Value 还弱

指定配置文件路径

PropertySource 指定路径,对于当前 Spring Boot 2.X 版本而言,默认仅能支持 *.properties,还不支持 *.yml 文件,因此我们需要修改默认的构造工厂,那么先来看看默认的构造工厂是怎么实现的。

源码解读

PropertySource

public @interface PropertySource 
    /** 
	 * 加载资源的别名,如果不设置的话会从资源路径中提取文件名
	 */
    String name() default "";

    /** 
     * 加载资源的路径,可使用classpath,如: "classpath:/config/test.yml"
     * 如果有多个文件路径放在中,使用','号隔开,如:"classpath:/config/test1.yml","classpath:/config/test2.yml"
     * 除使用classpath外,还可使用文件的地址,如:"file:/rest/application.properties"
	 * 当然不加前缀也是可以的
     */
    String[] value();
    /** 
	 * 资源路径找不到文件后是否报错
	 */
    boolean ignoreResourceNotFound() default false;

    /** 
	 * 配置文件编码,一般设为 utf-8 
	 */
    String encoding() default "";
    
    /**
     * 读取资源文件的工厂类
     */
    Class<? extends PropertySourceFactory> factory() default PropertySourceFactory.class;

DefaultPropertySourceFactory

public class DefaultPropertySourceFactory implements PropertySourceFactory 
    public DefaultPropertySourceFactory() 
    
	/*
	* 如果显示指定了name,则用该名字创建 ResourcePropertySource,否则不用
	* EncodedResource是对Resource的简单包装,增加了具体编码的属性
	*/
    public PropertySource<?> createPropertySource(@Nullable String name, EncodedResource resource) throws IOException 
        return name != null ? new ResourcePropertySource(name, resource) : new ResourcePropertySource(resource);
    

ResourcePropertySource

public class ResourcePropertySource extends PropertiesPropertySource 
	/*
	* 资源的原始名字(父类中的name,表示资源的别名)
	*/
	private final String resourceName;
	……
	/**
	* 当有指定别名的时候,需要记录原始名 
	*/
	public ResourcePropertySource(String name, EncodedResource resource) throws IOException 
		super(name, PropertiesLoaderUtils.loadProperties(resource));
		this.resourceName = getNameForResource(resource.getResource());
	

	/**
	* 如果资源没有别名,则不用另外记录
	*/
	public ResourcePropertySource(EncodedResource resource) throws IOException 
		super(getNameForResource(resource.getResource()), PropertiesLoaderUtils.loadProperties(resource));
		this.resourceName = null;
	
	
	/**
	* 获取资源时,同时查询原始名和别名
	*/
	public ResourcePropertySource withName(String name) 
		if (this.name.equals(name)) 
			return this;
		
		// Store the original resource name if necessary...
		if (this.resourceName != null) 
			if (this.resourceName.equals(name)) 
				return new ResourcePropertySource(this.resourceName, null, this.source);
			
			else 
				return new ResourcePropertySource(name, this.resourceName, this.source);
			
		
		else 
			// Current name is resource name -> preserve it in the extra field...
			return new ResourcePropertySource(name, this.name, this.source);
		
	
	……

Yaml 构造工厂

由于默认的 DefaultPropertySourceFactory 仅支持 *.properties 后缀的文件,因此需要对 Yaml 自定义一个构造工厂,继承于默认的属性构造工厂(DefaultPropertySourceFactory )。

有两种方式可以通过 EncodedResource 资源加载 yaml

  • YamlPropertySourceLoader
private PropertySource<?> loadYaml(String name, EncodedResource resource) throws IOException 
	Resource res = resource.getResource();
	String sourceName = name != null ? name : res.getFilename();
	//注意这里的name不能为空,因为其中的源码包含了,Assert.hasText(name, "Property source name must contain at least one character");
	List<PropertySource<?>> sources = new YamlPropertySourceLoader().load(res.getFilename(), res);
	return sources.get(0);

  • YamlPropertiesFactoryBean
private PropertySource<?> loadYaml(String name, EncodedResource resource) 
	YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
	factory.setResources(resource.getResource());
	factory.afterPropertiesSet();
	Properties properties = factory.getObject();
	String sourceName = name != null ? name : resource.getResource().getFilename();
	//注意这里的name不能为空,因为其中的源码包含了,Assert.hasText(name, "Property source name must contain at least one character");
	return new PropertiesPropertySource(sourceName, properties);

同时支持 properties 和 yaml 的混合工厂

通过判断后缀,如果是 *.yaml 或者 *.yml 则通过上面方案构造,否则通过默认的工厂构造

public class MixPropertySourceFactory extends DefaultPropertySourceFactory 

	@Override
	public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException 
		if(resource == null) return super.createPropertySource(name, resource);
		Resource res = resource.getResource();
		String sourceName = name != null ? name : res.getFilename();
		if (!resource.getResource().exists()) 
			return new PropertiesPropertySource(null, new Properties());
	     else if(sourceName.endsWith(".yml") || sourceName.endsWith(".yaml")) 
	    	return loadYaml_1(name, resource);
	    	//return loadYaml_2(name, resource);
		 else 
			return super.createPropertySource(name, resource);
		
	
	
	private PropertySource<?> loadYaml_1(String name, EncodedResource resource) throws IOException 
		Resource res = resource.getResource();
		String sourceName = name != null ? name : res.getFilename();
		//注意这里的name不能为空,因为其中的源码包含了,Assert.hasText(name, "Property source name must contain at least one character");
		List<PropertySource<?>> sources = new YamlPropertySourceLoader().load(res.getFilename(), res);
		return sources.get(0);
	
	
	private PropertySource<?> loadYaml_2(String name, EncodedResource resource) 
		YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
		factory.setResources(resource.getResource());
		factory.afterPropertiesSet();
		Properties properties = factory.getObject();
		String sourceName = name != null ? name : resource.getResource().getFilename();
		//注意这里的name不能为空,因为其中的源码包含了,Assert.hasText(name, "Property source name must contain at least one character");
		return new PropertiesPropertySource(sourceName, properties);
	

创建测试用的 yaml 文件

在 resources 目录下,创建 config 文件夹,然后新建一个 bob.yml 文件,添加上如下测试文本。

井号( # )表示单行注释

接下来,我们要对如下类型分别进行测试

  • int
  • boolean
  • String
  • String[]
  • List<String>
  • Set<String>
  • Map<String, String>
  • List<Map<String, String>>

引号的区别

  • 不加引号,等同于单引号
  • 单引号,会对转义字符进行转义,从而使得转义字符失去转义效果,字符串原样输出
  • 双引号,转义字符会被解释为其应有的含义。

大小写、横杠、下划线
yaml 文件对大小写不敏感,在 java 注解中横杠和下划线写不写都可以读出数据

person:
 bob:
  age: 26
  male: true
  company: tencentQQ
  
  pet1:  #array
   - rabbit
   - tortoise
   - carp
   - tortoise
  pet2:  #list
   - rabbit
   - tortoise
   - carp
   - tortoise
  pet3:  #set
   - rabbit
   - tortoise
   - carp
   - tortoise
  plant: rose,lily,sunflower,pear,lily #list 行内写法
  languageEL: #使用StringEL表达式将字符串切分成数组
   "java,C#,python" 
  
  friend: teacher: 'Jane', musician: 'F.I.R', scientist: 'Einstein'
  friendEL:  #使用StringEL表达式获取map
   "teacher: 'Jane', musician: 'F.I.R', scientist: 'Einstein'"
  family: #map
   father: Trump
   mother: Tina 
   brother: Tom

  play-game:  #list<map>
    - name: GBA
      years: 5
    - name: Srike of Kings
      years: 3
    - name: Dragon Nest
      years: 2
      
  quote-1: 'a<br>man\\tof\\nmany\\ntalents, his age is $age and his company is $person.bob.company'
  quote-2: "a<br>tman\\tof\\nmany\\ntalents, his age is $age and his company is $person.bob.company"

使用 @Value 逐个注入属性

创建一个控制类,当访问指定网页时,将变量打印出来。

访问时注意大小写,且不能加后缀。比如,这里配置的 @GetMapping("/testValue"),那么就只能通过 localhost:8080/testValue 来访问

@Value 注解不用指定配置文件位置,就可以自动装配。

代码

@RestController
public class TestController 
	
	@Value("$person.bob.age")
	private int age;
	@Value("$person.bob.male")
	private boolean male;
	@Value("$person.bob.company")
	private String company;
	
//	@Value("$person.bob.pet1")
	private String[] pet1;
//	@Value("$person.bob.pet2")
	private List<String> pet2;
//	@Value("$person.bob.pet3")
	private Set<String> pet3;
	@Value("$person.bob.plant")
	private List<String> plant;
	@Value("#'$person.bob.languageEL'.split(',')")
	private List<String> languageEL;
	
//	@Value("$person.bob.friend")
	private Map<String, String> friend;
	@Value("#$person.bob.friendEL")
	private Map<String, String> friendEL;
//	@Value("#$person.bob.family")
	private Map<String, String> family;
//	@Value("#$person.bob.play-game")
	public List<Map<String, String>> playgame;
	
	@Value("$person.bob.quote-0")
	private String quote0;
	@Value("$person.bob.quote-1")
	private String quote1;
	@Value("$person.bob.quote-2")
	private String quote2;
	
	@GetMapping("/testValue")
	public YamlConfig testValue() 
		YamlConfig config = new YamlConfig(age, male, company, pet1, pet2, pet3, plant, languageEL, friend, friendEL, family, playgame, quote0, quote1, quote2); 
		System.out.println(config);
		return config;
	

输出结果

YamlConfig(age=26, male=true, company=tencentQQ, pet1=null, pet2=null, pet3=null, plant=[rose, lily, sunflower, pear, lily], languageEL=[java, C#, python], friend=null, friendEL=teacher=Jane, musician=F.I.R, scientist=Einstein, family=null, playgame=null, quote0=a<br>tman\\tof\\nmany\\ntalents, his age is 18 and his company is tencentQQ, quote1=a<br>man\\tof\\nmany\\ntalents, his age is 18 and his company is tencentQQ, quote2=a<br>tman	of
many
talents, his age is 18 and his company is tencentQQ)

总结

@Value 只适用于简单类型,如 String、int、Boolean。对于 List 和 Map 这种复杂类型,则需要借助 SpringEL 表达式来实现。因此它仅适合于获取少量且简单的属性。

使用 @ConfigurationProperties 批量自动装配

这里用到了 lombok,需要使用安装 lombok 插件,并在pom.xml 中添加 lombok 依赖

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.8</version>
    <scope>provided</scope>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-configuration-processor</artifactId>
	<optional>true</optional>
</dependency>

源代码

YamlConfig.java

@NoArgsConstructor
@AllArgsConstructor
@Data
@Validated //JSR303校验,例如@NotNull

@Component
//可以引用多个配置文件,中间用逗号隔开,配置文件可以以"classpath:"、或"file:"打头
@PropertySource(value= "config/bob.yml", encoding = "utf-8", factory = MixPropertySourceFactory.class)
@ConfigurationProperties(prefix = "person.bob", ignoreInvalidFields = true, ignoreUnknownFields = true)
public class YamlConfig 
	@NotNull
	private int age;
	private boolean male;
	private String company;
	private String[] pet1;
	private List<String> pet2;
	private Set<String> pet3;
	
	private List<String> plant;
	private List<String> languageEL;
	
	private Map<String, String> friend;
	@Value("#$person.bob.friendEL")
	private Map<String, String> friendEL;
	private Map<String, String> family;
	
	private List<Map<String, String>> playgame;
	
	private String quote0;
	private String quote1;
	private String quote2;
	

TestController.java

@RestController
public class TestController 
    @Autowired
    private YamlConfig config;

	@GetMapping("/testConf")
	public YamlConfig testConf() 
		System.out.println(config);
		return config;
	

输出结果

YamlConfig(age=26, male=true, company=tencentQQ, pet1=[rabbit, tortoise, carp, tortoise], pet2=[rabbit, tortoise, carp, tortoise], pet3=[rabbit, tortoise, carp], plant=[rose, lily, sunflower, pear, lily], languageEL=[java, C#, python], friend=teacher=Jane, musician=F.I.R, scientist=Einstein, friendEL=teacher=Jane, musician=F.I.R, scientist=Einstein, family=father=Trump, mother=Tina, brother=Tom, playgame=[name=GBA, years=5, name=Srike of Kings, years=3, name=Dragon Nest, years=2], quote0=a<br>tman\\tof\\nmany\\ntalents, his age is 18 and his company is tencentQQ, quote1=a<br>man\\tof\\nmany\\ntalents, his age is 18 and his company is tencentQQ, quote2=a<br>tman	of
many
talents, his age is 18 and his company is tencentQQ)

总结

该方案可以自动装配上面列举的所有类型。如果yaml里面的是StringEL的字符串,可以配合上@Value 注解进行解析。

使用环境变量 Environment 装配

源代码

@RestController
//首先要声明配置文件的位置
@PropertySource(value= "config/bob.yml", encoding = "utf-8", factory = MixPropertySourceFactory.class)
public class TestController 
	@Autowired
	private Environment env;

	@GetMapping("/testEnv")
	public YamlConfig testEnv() 
		int age = env.getProperty("person.bob.age", Integer.class);
		boolean male = env.getProperty("person.bob.male", Boolean.class);
		String company = env.getProperty("person.bob.company");

		String[] pet1 = env.getProperty("person.bob.pet1", String[].class);
		List<String> pet2 = env.getProperty("person.bob.pet2", List.class);
		Set<String> pet3 = env.getProperty("person.bob.pet3", Set.class);
		List<String> plant = env.getProperty("person.bob.plant", List.class);
		List<String> languageEL = env.getProperty("#'$person.bob.languageEL'.split(',')", List.class);

		Map<String, String> friend = env.getProperty("person.bob.friend", Map.class);
		Map<String, String> friendEL = env.getProperty("#$person.bob.friendEL", Map.class);
		Map<String, String> family = env.getProperty("person.bob.family", Map.class);
		List<Map<String, String>> playgame = env.getProperty("person.bob.play-game", List.class);

		String quote0 = env.getProperty("person.bob.quote-0");
		String quote1 = env.getProperty("person.bob.quote-1");
		String quote2 = env.getProperty("person.bob.quote-2");

		YamlConfig config = new YamlConfig(age, male, company, pet1, pet2, pet3, plant, languageEL, friend, friendEL,
				family, playgame, quote0, quote1, quote2);
		System.out.println(config);
		return config;
	

输出结果

总结

Environment 仅支持 boolean、int、string 和 list 的行内写法,功能比 @Value 还弱

以上是关于Spring Boot 2.X 装载 yaml 配置文件的键值对的主要内容,如果未能解决你的问题,请参考以下文章

Spring Boot Mongodb .yaml 配置

Spring Boot YAML 绑定:绑定属性失败

Spring Boot 获取yaml配置文件信息

Spring Boot - 加载多个 YAML 文件

Spring Boot学习笔记总结

Spring Boot2 系列教程理解Spring Boot 配置文件 application.properties