之前写过一篇关于介绍Spring占位符替换原理的博客,传送门 :Spring的占位符是怎么工作的
在这篇文章基础上,再介绍一下@Value替换原理,两篇文章有一定的相关性。
继续以上一篇的工程为例,项目结构一样,这里就不再展示出来了,详情可查看上一篇文章。
另外我定义了一个类,内容如下
@RestController
@RequestMapping("/demo_client")
public class DemoClientController {
// @Value("${config.name}")
// private String name;
@Value("${my.property.key}")
private String myPropertyName;
}
希望从配置中拿到配置,然后赋值给到myPropertyName属性。
说到赋值,那肯定会想到spring的依赖注入DI,很显然这个实现动态替换变量就是依赖注入原理完成的。Spring容器在启动过程中会先实例化对象,然后初始化,也就是填充对象属性。
@Value注解属性填充,它实现是通过一个叫AutowiredAnnotationBeanPostProcessor的bean前置处理器来完成的,它是一个BeanPostProcessor,Spring填充属性时候会调用其中postProcessProperties方法。
为什么是这个AutowiredAnnotationBeanPostProcessor类来处理的,要从bean生命周期来说了,这里不展开,稍微提下,
可以看到,凡是@Autowired和@Value都是交给它来填充属性的。
好,开始分析占位符变量替换过程.
工程启动后,会进入到org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#populateBean填充属性方法,变量BeanPostProcessor,其中就包含了AutowiredAnnotationBeanPostProcessor这个,打开看看它里面有什么东西。
其中有个缓存的东西,里面放了以beanName为key,Value是InjectionMetadata对象,即需依赖注入的对象。这些依赖注入的对象是在方法 applyMergedBeanDefinitionPostProcessors执行时预先放进去的,其实就是扫描解析所有带有@Autowired@Value@Resource@Inject等注解的bean,然后缓存到此cache.以便后面填充属性时使用,代码比较长,需要花点时间看看。
因为我们定义的是DemoClientController,所以找到它看看,果然是有2个属性。
接着执行下一步,进入此方法
开始填充属性myPropertyName
继续深入
最终会进入到此方法
其中embeddedValueResolvers是PropertySourcesPropertyResolver以及PropertySourcesPlaceholderConfigurer,这两个对象是在属性填充之前已经准备好,上一篇文章最开始加载配置资源的也有提到,org.springframework.context.support.PropertySourcesPlaceholderConfigurer#postProcessBeanFactory方法,具体看这个方法。
接着请求方法resolveStringValue,所以会执行到PropertySourcesPlaceholderConfigurer的processProperties方法中去
这个方法上一篇文章已介绍过,最终会执行到以下方法
然后拿到my.property.key的值,最终spring容器会通过反射赋值到bean的属性,即DemoClientController#myPropertyName赋值完成。
好了,到这一步@Value注解流程解析完成。
另外,注意到上面的my.property.key配置,我是把它放在dev.properties中的,但在spring boot项目,我们一般喜欢放在application-xx.yml中,那么是不是流程会有区别? 接着再分析下这种情况:
先在application.yml定义一个名叫config.name的变量,如下
server:
port: 8999
spring:
application:
name: eureka-service-1
config:
name: huangd
DemoClientController稍微改动
@RestController
@RequestMapping("/demo_client")
public class DemoClientController {
@Value("${config.name}")
private String name;
// @Value("${my.property.key}")
// private String myPropertyName;
}
将myPropertyName注释,改拿config.name配置。
跟之前一样启动工程,前面一部分没有任何变化,不同的地方在于,
发现这时候不再是从name=localProperties这个对象中拿配置,而是从另外一个拿,
是要从name='environmentProperties’中去拿配置,因为application.yml的配置是放在它里面的。还有从上图看到,发现它里面有9个对象,都是干什么的,我们不需要关心,反正肯定是针对某个场景取不同的对象,接着看它是从哪个对象拿配置的。
上图看到,执行到此方法这里,这也说明,就是从environment去拿的配置,好继续往下走,
开始遍历上面9个对象,找这个key为config.name的值,最终是在
OriginTrackedMapPropertySource这里面找到了这个配置。
至于OriginTrackedMapPropertySource初始化是什么时候触发,它是在这个地方初始化的,应用启动时会执行到此步骤
拿到了配置值以后,后面的流程跟之前一样了,流程结束。
好了,两种加载配置方式都已经分析完成,大体上是一样的,只是根据配置key所在不同的配置文件读取来源不一样。