Spring死磕系列-@PropertySource注解
PropertySource注解介绍
该注解给Environment
中添加PropertySource
提供了一种便捷且申明式的机制,@PropertySource
注解是配合@Configuration
标记的配置类一块使用的。
PropertySource注解使用
Demo演示-类布局
Demo演示-主配置类
@Configuration
@PropertySources({
@PropertySource("classpath:mysql.properties"),
@PropertySource("classpath:oracle.properties")})
@PropertySource(value = "classpath:redis.properties")
public class DataSourceConfiguration {
}
Demo演示-资源文件定义
mysql.user.name=mysql
mysql.user.password=123456
mysql.driver.class=jdbc:mysql://ip:port/database_name
oracle.user.name=oracle
oracle.user.password=${mysql.user.password}
oracle.driver.class=jdbc:oracle:thin:@ip:port:database_name
reids.name=redis
redis.password=${mysql.user.password}
Demo演示-测试类
@RunWith(SpringJUnit4ClassRunner.class)
@SpringJUnitConfig(classes = DataSourceConfiguration.class)
public class DataSourceConfigurationTest {
@Autowired
ApplicationContext context;
@Test
public void testUserName() {
String userName = context.getEnvironment().getProperty("mysql.user.name");
assertThat(userName, equalTo("mysql"));
}
@Test
public void testPassword() {
String password = context.getEnvironment().getProperty("oracle.user.password");
assertThat(password, equalTo("123456"));
}
@Test
public void testRedisName() {
String redisName = context.getEnvironment().getProperty("reids.name");
assertThat(redisName, equalTo("redis"));
}
@Test
public void testRedisPassword() {
String redisPassword = context.getEnvironment().getProperty("redis.password");
assertThat(redisPassword, equalTo("123456"));
}
}
Demo演示-运行结果
PropertySource注解定义
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(PropertySources.class)
public @interface PropertySource {
/**
* 给该PropertySource起个名。如果没有,factory()属性指定的PropertySource工厂将会根据里面的资源信息生成一个名字
*/
String name() default "";
/**
* 代表需要被加载资源的位置。note:资源位置不支持通配符(eg:**//*.properties),每个location必须明确指定一个资源
*/
String[] value();
/**
* 如果value属性中指定的资源没有找到是否会忽略,默认false(没有找到会报错)
* @since 4.0
*/
boolean ignoreResourceNotFound() default false;
/**
* 指定编码集
* @since 4.3
*/
String encoding() default "";
/**
* 自定义一个PropertySource工厂(职责就是根据给定的resource创建PropertySource对象)
* 默认会使用DefaultPropertySourceFactory或ResourcePropertySource
* @since 4.3
*/
Class<? extends PropertySourceFactory> factory() default PropertySourceFactory.class;
}
PropertySource注解解析流程
我们通过Import解析过程中知道,在ConfigurationClassParser
类对配置类上所有可能出现的注解进行解析,本文我们只关注PropertySource注解的解析。
ConfigurationClassParser类
protected final SourceClass doProcessConfigurationClass(
ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)
throws IOException {
if (configClass.getMetadata().isAnnotated(Component.class.getName())) {
// Recursively process any member (nested) classes first
processMemberClasses(configClass, sourceClass, filter);
}
// Process any @PropertySource annotations
// 遍历从sourceClass上收集的PropertySources和PropertySource注解
for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), PropertySources.class,
org.springframework.context.annotation.PropertySource.class)) {
if (this.environment instanceof ConfigurableEnvironment) {
//如果environment是ConfigurableEnvironment的对象才进行属性源的处理
processPropertySource(propertySource);
}
else {
//否则会被忽略
logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
"]. Reason: Environment must implement ConfigurableEnvironment");
}
}
// Process any @ComponentScan annotations
// 省略处理@ComponentScan逻辑
// Process any @Import annotations
// 省略处理@Import逻辑
// Process any @ImportResource annotations
// 省略处理@ImportResource逻辑
// Process individual @Bean methods
// 省略处理@Bean方法逻辑
// Process default methods on interfaces
// 省略处理接口中定义的default methods
// Process superclass, if any
// 省略处理父类逻辑
// No superclass -> processing is complete
return null;
}
通过查看
ConfigurationClassParser
类中对@PropertiesSource
注解的解析,大致流程:
- 获取该配置类上所有的@PropertySources和@PropertySource注解的属性,封装成Set返回。
- 进行遍历,其实AnnotationAttributes类本质上是一个Map<String, Object>,就是该注解中 属性->值 的键值对信息。
- 然后对遍历元素进行解析,通过
@PropertiesSource
注解中factory指定的工厂创建PropertySource
对象,然后放到Environment
中。如何从配置类上解析注解封装成Set自己查看源码,下面主要看一下后面的流程
通过debug调试,可以清晰看到,已经把我们配置类上所有PropertySource注解解析封装成AnnotationAttributes对象。由于
PropertySources
注解是PropertySource
注解的容器,里面元素全部是PropertySource
,在处理容器注解时就是将里面的元素进行遍历处理。
PropertySource属性解析
private void processPropertySource(AnnotationAttributes propertySource) throws IOException {
String name = propertySource.getString("name");
if (!StringUtils.hasLength(name)) {
name = null;
}
String encoding = propertySource.getString("encoding");
if (!StringUtils.hasLength(encoding)) {
encoding = null;
}
String[] locations = propertySource.getStringArray("value");
Assert.isTrue(locations.length > 0, "At least one @PropertySource(value) location is required");
boolean ignoreResourceNotFound = propertySource.getBoolean("ignoreResourceNotFound");
Class<? extends PropertySourceFactory> factoryClass = propertySource.getClass("factory");
PropertySourceFactory factory = (factoryClass == PropertySourceFactory.class ?
DEFAULT_PROPERTY_SOURCE_FACTORY : BeanUtils.instantiateClass(factoryClass));
//上面都是获取@PropertySource中的属性,并进行默认赋值
for (String location : locations) {
try {
//解析location中的占位符。说明我们的location是可以灵活指定
String resolvedLocation = this.environment.resolveRequiredPlaceholders(location);
//通过解析后的location创建一个代表该资源的Resource对象,此时资源还没有真正被加载,仅仅是一个引用指向了该资源
Resource resource = this.resourceLoader.getResource(resolvedLocation);
//通过上面解析好的属性,交给PropertySourceFactory创建代表该属性源的PropertySource对象
//然后真正执行给Environment添加PropertySource操作
addPropertySource(factory.createPropertySource(name, new EncodedResource(resource, encoding)));
}
catch (IllegalArgumentException | FileNotFoundException | UnknownHostException | SocketException ex) {
//异常处理,记录日志
}
}
}
走到这步我们的属性源文件仅仅转换成一个Resource对象进行表示,其里面的内容还没有被加载进来
在工厂创建PropertySource对象时,才真正去加载属性文件中的内容(但里面的占位符是没有解析的),而且如果没有指定名称,工厂会创建一个名字。我们可以看到创建一个
ResourcePropertySource
对象,此时的name属性用来保存资源名称,source属性保存资源文件解析出来的属性键值对。
将属性源添加到Environment
private void addPropertySource(PropertySource<?> propertySource) {
// 这里propertySource全部都有名字,如果没有指定,在调用该方法之前PropertySourceFactory会生成一个
// 此时已经加载资源文件中的内容,并在保存在propertySource的source字段,但属性如果存在占位符,此时还没有解析
String name = propertySource.getName();
// 1. 从environment中获取可以管理多个PropertySource的对象
MutablePropertySources propertySources = ((ConfigurableEnvironment) this.environment).getPropertySources();
// 2. 处理已经存在属性源情况
if (this.propertySourceNames.contains(name)) {
PropertySource<?> existing = propertySources.get(name);
// 如果已经存在属性源,再添加,存在一些属性的合并,通过CompositePropertySource对象完成
if (existing != null) {
PropertySource<?> newSource = (propertySource instanceof ResourcePropertySource ?
((ResourcePropertySource) propertySource).withResourceName() : propertySource);
// 2.1 如果存在的PropertySource是CompositePropertySource类型
if (existing instanceof CompositePropertySource) {
((CompositePropertySource) existing).addFirstPropertySource(newSource);
}
else {
// 2.2 如果存在的PropertySource不是CompositePropertySource类型
if (existing instanceof ResourcePropertySource) {
existing = ((ResourcePropertySource) existing).withResourceName();
}
CompositePropertySource composite = new CompositePropertySource(name);
composite.addPropertySource(newSource);
composite.addPropertySource(existing);
propertySources.replace(name, composite);
}
return;
}
}
// 3. 处理属性源不存在情况
// 3.1 如果此时还没有任何属性源,则直接添加到最后(这个分支只会执行一次)
if (this.propertySourceNames.isEmpty()) {
propertySources.addLast(propertySource); // 添加属性源
}
else {
// 3.2 此时有属性源,则添加到最后一个元素的前面
String firstProcessed = this.propertySourceNames.get(this.propertySourceNames.size() - 1);
propertySources.addBefore(firstProcessed, propertySource); // 添加属性源
}
// 4. 最后将该属性源名称添加到名称列表里
this.propertySourceNames.add(name);
}
通过上面的分析,从解析配置类开始,收集配置类上
PropertySources
和PropertySource
的属性,封装成Set返回,然后遍历,对每一个PropertySource
的属性进行解析并对未指定的值设置默认值,将解析好的属性交给PropertySourceFactory工厂,工厂根据这些属性创建出PropertySource对象,最后将没有添加过的属性源添加到Environment
中统一管理多个属性源的MutablePropertySources对象,将添加过的属性源进行一些属性的合并。
我们第一次进来发现从
Environment
中获取的MutablePropertySources
已经有两个属性源了,分别是保存系统属性的PropertiesPropertySource
和系统环境变量的SystemEnvironmentPropertySource
但此时属性源的名称列表还是空的,所有走第一次添加属性源逻辑,在最后添加属性源。
第一次添加后的属性源列表和属性源名称列表如上图所示
往后如果该属性源没有被添加且不是第一次添加的属性源,则始终会在最后一个属性源的前面插入
PropertySource类层次结构
- PropertySource:该类是代表name/value属性对的抽象基类,其中
getSource()
方法就是获取任何类型的对象,该对象可以是java.util.Properties
、java.util.Map
、ServletContext
、ServletConfig
,这些对象都是用来封装name/value属性对的。通常该类不是单独使用,而是通过PropertySources
对象使用, 该PropertySources
对象聚集了所有属性源并与PropertyResolver
结合使用,PropertyResolver
可以基于优先级进行搜索。
PropertySourceFactoty及其实现类
- PropertySourceFactory:基于资源创建PropertySource的策略接口
public interface PropertySourceFactory {
/**
* 通过给定的资源(resource)创建PropertySource对象
*/
PropertySource<?> createPropertySource(@Nullable String name, EncodedResource resource) throws IOException;
}
- 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));
}
}
PropertySources类及其实现类
- PropertySources:该类持有一个或多个封装了属性对的PropertySource对象
public interface PropertySources extends Iterable<PropertySource<?>> {
/**
* Return a sequential {@link Stream} containing the property sources.
* @since 5.1
*/
default Stream<PropertySource<?>> stream() {
return StreamSupport.stream(spliterator(), false);
}
/**
* 是否存在给定名称的属性源
*/
boolean contains(String name);
/**
* 返回指定名称的属性源,如果不存在返回null
*/
@Nullable
PropertySource<?> get(String name);
}
- MutablePropertySources:
PropertySources
类的默认实现,该类提供了对属性源的一下操作,下面列出主要方法。- addAfter(String relativePropertySourceName, PropertySource<?> propertySource):void 在指定属性源的后面添加
- addBefore(String relativePropertySourceName, PropertySource<?> propertySource):void 在指定属性源的前面添加
- addFirst(PropertySource<?> propertySource):void 在最前面添加
- addLast(PropertySource<?> propertySource):void 在最后面添加