Spring @PropertySource 注解实现读取 yml 文件


记一次在开发中使用 @PropertySource 注解加载 yml 文件

简介:Spring框架提供了@PropertySource注解,目的是加载指定的属性文件。
这个注解是非常具有实际意义的,特别是在SpringBoot环境下,意义重大。由于SpringBoot默认情况下它会去加载classpath下的application.properties文件,所以我看大绝大多数开发者是这么干的:把所有的配置项都写在这一个配置文件里
这是非常不好的习惯,非常容易造成配置文件的臃肿,不好维护到最后的不能维护。

比如我们常见的一些配置:jdbc的、redis的、feign的、elasticsearch的等等他们的边界都是十分清晰的,因此Spring提供给我们这个注解,能让我们很好的实现隔离性~

备注:此注解是Spring3.1后提供的,并不属于Spring Boot。

背景: 业务需要创建多个配置类,基于 yml 文件拥有简洁的层次结构,遂配置文件选择 yml 类型。
但在实际的开发中遇到使用 @PropertySource 注解无法加载 yml 配置文件问题。项目使用的spring而非springboot。

注: spring bootymlyaml 对应的加载类为 YamlPropertySourceLoader,因此不需要自己实现。

分析过程:

首先我们先来分析一下 @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(); /** 此属性为根据资源路径找不到文件后是否报错, 默认为是 false */ boolean ignoreResourceNotFound() default false; /** 此为读取文件的编码, 若配置中有中文建议使用 'utf-8' */ String encoding() default ""; /** * 关键:此为读取资源文件的工程类, 默认为: * 'PropertySourceFactory.class' */ Class<? extends PropertySourceFactory> factory() default PropertySourceFactory.class; }

从源码可以看出,读取资源文件 PropertySourceFactory 接口是关键,加下来打开 PropertySourceFactory 接口的源码:

public interface PropertySourceFactory { PropertySource<?> createPropertySource(@Nullable String var1, EncodedResource var2) throws IOException; }

发现其中只有一个创建属性资源接口的方法,接下来我们找到实现这个方法的类:

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

在这个类中我们发现其返回了一个对象 ResourcePropertySource ,找到 DefaultPropertySourceFactory 类使用的两个 ResourcePropertySource 类的构造方法:

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; }

在上面代码中,两个构造方法都使用了 PropertiesLoaderUtils.loadProperties() 这个属性的方法, 一直点下去, 会发现这么一段代码:

static void fillProperties(Properties props, EncodedResource resource, PropertiesPersister persister) throws IOException { InputStream stream = null; Reader reader = null; try { String filename = resource.getResource().getFilename(); // private static final String XML_FILE_EXTENSION = ".xml"; 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); } } finally { if (stream != null) { stream.close(); } if (reader != null) { reader.close(); } } }

由上可知,@PropertySource 注解也可以用来加载 xml 文件,接下来根据 persister.load(props, stream) 方法一直点下去会找到下面一段代码:

private void load0 (LineReader lr) throws IOException { char[] convtBuf = new char[1024]; int limit; int keyLen; int valueStart; char c; boolean hasSep; boolean precedingBackslash; /** * 每次读取一行 */ while ((limit = lr.readLine()) >= 0) { c = 0; keyLen = 0; valueStart = limit; hasSep = false; //System.out.println("line=<" + new String(lineBuf, 0, limit) + ">"); precedingBackslash = false; /** * 遍历一行字的每一个字符 * 若字符中出现 '='、':'、' '、'\t'、'\f' 则跳出循环 */ while (keyLen < limit) { c = lr.lineBuf[keyLen]; //need check if escaped. // 如果当前遍历字符为 '=' 或 ':' 则跳出循环 if ((c == '=' || c == ':') && !precedingBackslash) { valueStart = keyLen + 1; hasSep = true; break; } // 如果当前遍历字符为 ' ' 或 '\t' 或 '\f' 跳出循环, // 但在接下来的循环中还需要继续遍历知道找到 '=' 或 ':' else if ((c == ' ' || c == '\t' || c == '\f') && !precedingBackslash) { valueStart = keyLen + 1; break; } // 检查是否转义 if (c == '\\') { precedingBackslash = !precedingBackslash; } else { precedingBackslash = false; } // 每次循环,keyLen + 1 keyLen++; } /** * 判断 valueStart(值的开始下标)是否小于读取行的长度,若小于,则进入循环 */ while (valueStart < limit) { c = lr.lineBuf[valueStart]; // 判断当前字符是否等于空格、制表符、换页符。都不等于则进入循环 if (c != ' ' && c != '\t' && c != '\f') { // 当 hasSep 为 false 时代表上个 while (keyLen < limit) 循环跳出时 c 为 空格或制表符或换页符 // 这里继续循环直到找到'='或':'号为止 // 由此可见 在配置文件中'=' 或 ':' 号前可有空格、制表符、换页符 if (!hasSep && (c == '=' || c == ':')) { hasSep = true; } else { break; } } // 每次循环,valueStart + 1 valueStart++; } // 获取配置文件中的 key,value 并保存 String key = loadConvert(lr.lineBuf, 0, keyLen, convtBuf); String value = loadConvert(lr.lineBuf, valueStart, limit - valueStart, convtBuf); put(key, value); } }

上面 load0 方法每次读取一行,然后根据 '='':' 来获取 keyvalue,而 yml 具有鲜明层次结构的特点则不能由此方法读取。

综上分析可知,@PropertySource 注解读取属性文件的关键在于 PropertySourceFactory 接口中的 createPropertySource 方法,所以我们想要实现 @PropertySource 注解读取 yml 文件就需要实现 createPropertySource 方法,在 @PropertySource 注解其是通过 DefaultPropertySourceFactory 类来实现这个方法,我们只需要继承此类,并重写其 createPropertySource 方法即可,实现代码如下:

@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()); return sources.get(0); }

注: spring bootymlyaml 对应的加载类为 YamlPropertySourceLoader,因此不需要自己实现。

测试

@Component @PropertySource(value = "test.yml", encoding = "utf-8", factory = TestFactory.class) @ConfigurationProperties(prefix = "com.test") public class IdCardServerConfig { private String serverCode; ... }

LazzMan 2022年4月15日 10:27 收藏文档