项目中经常需要使用到占位符来满足多环境不同配置信息的需求,比如:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns="http://www.springframework.org/schema/beans"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="myPropertyPlaceholderBean" class="com.example.demo1.PropertyPlaceholderBean"><property name="myPropertyName" value="${my.property.key}" /></bean></beans>
其中属性myPropertyName是带有’ ${}’ 符号,也就是占位符的变量,最终需要替换成具体的值,Spring会最终替换,那么它怎么做到的? 下面就通过打断点跟源码方式分析来分析说明。
还是以SpringBoot项目为例,在resources下定义结构如下:
以上结构是为了方便验证,随便定义的,大家可能有区别。
其中dev.properties定义两个key
test.env_var=123
my.property.key=justdoit
spring-bean1.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns="http://www.springframework.org/schema/beans"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="myPropertyPlaceholderBean" class="com.example.demo1.PropertyPlaceholderBean"><property name="myPropertyName" value="${my.property.key}" /></bean></beans>
spring-application.xml如下
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns="http://www.springframework.org/schema/beans"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"><context:property-placeholder location="classpath:properties/dev.properties" ignore-unresolvable="true" /><import resource="spring-bean1.xml"/>
</beans>
Spring boot启动类定义
@SpringBootApplication
@ImportResource({"classpath:spring-application.xml"})
public class ClientServerApplication {public static void main(String[] args) {SpringApplication.run(ClientServerApplication.class, args);}
}
好了,下面开始分析整个过程…
首先从PropertySourcesPlaceholderConfigurer开始说,因为占位符默认就是由它来实现的。 进入其源码看到它是一个BeanFactoryPostProcessor 大家都知道,spring bean生命周期过程会执行所有BeanFactoryPostProcessor的postProcessBeanFactory方法,所以,肯定会进入到这个方法:
这里看到它尝试从两个地方去读取属性配置,一个是
以Environment为属性源的environmentProperties,另外一个就是通过loadProperties(Properties props)加载本地资源文件作为属性源的localProperties,我这个例子是第二种情况。
可以看到,已经加载到我上面配置的两个key-value
接着进入到下一步:
this.processProperties(beanFactory, (ConfigurablePropertyResolver)(new PropertySourcesPropertyResolver(this.propertySources)));
看到propertyResolver.setPlaceholderPrefix(this.placeholderPrefix)这些是设置缺省时,占位符的默认配置,即’${}’
其中注意一点,StringValueResolver valueResolver定义的是labmda表达式,后面会使用到。
接着下一步
上面这里是开始遍历所有的bean,替换其中包含占位符的bean的属性对象。
接着进入方法:
遍历到我们自定义的bean,其中beanDefinition.getPropertyValues()是拿它的所有属性信息,如下图
遍历所有的属性,解析值,并且替换占位符
进入resolveValue方法,直接去到以下位置,因为属性类型是string嘛,所以直接跳到这里
可以看到我们bean中定义的占位符,接下来就是要替换它。接着看
发现此次是一个labmda表达式,就是上面提到的,所以执行回到上面的位置,
接着跟代码会进入到
继续进入
parseStringValue
从propertySources里面去解析配置,疑问来了??这个对象什么时候放进去的,其实就是最开始提到的两个读取配置的地方,
一个是
以Environment为属性源的environmentProperties,另外一个就是通过loadProperties(Properties props)加载本地资源文件作为属性源的localProperties。
看以下代码就明白了,
if (this.localOverride) {
this.propertySources.addFirst(localPropertySource);
} else {
this.propertySources.addLast(localPropertySource);
}
解析完占位符得到值以后,出来回到resolveValue方法处,也就是很多if else的方法处,字符串位置
将属性值原本是${my.property.key}替换成justdoit
到此,对象PropertyPlaceholderBean定义的属性myPropertyName就被替换成具体的某个值了,这里也就是被替换成了 justdoit
总结:
基于Spring bean的生命周期,BeanFactoryPostProcessor执行方法postProcessBeanFactory,解析获取到属性源即environmentProperties以及localProperties两种,跟着解析占位符,然后得到具体的值,最后set进去替换占位符为具体的属性值。