1、@value 注解使用
先配置本地 application.properties
如下:
apple.name=abc
代码如下:
@propertysource("application.properties") public class apple { @value("${apple.name}") public string name; } @componentscan public class atvaluetest { public static void main(string[] args) { annotationconfigapplicationcontext context = new annotationconfigapplicationcontext(atvaluetest.class); apple bean = context.getbean(apple.class); system.out.println("bean.name = " + bean.name); } }
结果如下:
bean.name = abc
可以看到最终在 apple 中可以获取到配置文件中的值,那么 spring 是怎样解析获取到该值的呢?下面开始分析下。
2、@value 注解源码分析
分析源码之前,我么可以简单思考下, spring 会怎么样去处理这个 @value 注解的?然后再带着我们的问题去看看 spring 是不是和我们想的一样呢?
简单分析有三个步骤,如下:
- 首先 spring 肯定要去解析收集该注解,找到被 @value 注解修饰的所有属性,注意此时属性的值是一个占位符 ${apple.name},并不是真正的值
- 然后加载一个资源文件,可以是本地 application.properties、也可以是其他,比如 environment 、xml
- 最后去判断哪个类上的属性有 @value 修饰,就把占位符替换成资源文件中配置的值
在脑海中有了大致思路,再去追踪源码就事半功倍了。
2.1、spring 解析并收集 @value 修饰的属性
spring 解析收集 @value 修饰的属性和解析收集 @autowired 注解一模一样,懂这个,@autowired 注解解析流程也就懂了。
spring 提供很多 beanfactorypostprocessor、beanpostprocessor 接口作为扩展,从而使 spring 非常强大,因为我们属性赋值相当于是在实例化之后的事,所以这里的 @value 解析就是 beanpostprocessor 接口的应用啦!
一个大家非常熟悉的类 autowiredannotationbeanpostprocessor,spring 的 di(依赖注入) 就是这个 beanpostprocessor 完成的,接下来看 spring 源码,如下:
可以看到 spring 是在实例化之后开始去收集的,这个时序非常重要,一定要注意
然后进入 autowiredannotationbeanpostprocessor 类核心部分,如下:
对过来的每个类进行筛选判断是否有被 @value、@autowired 修饰的方法或者属性,如果找到有,就会将这个类记录下来,放到一个 injectionmetadatacache 缓存中,为后续的 di 依赖注作准备,注意哦,解析并收集到的结果最终放到 spring 的 injectionmetadatacache 缓存中
进入 buildautowiringmetadata() 方法内部,如下:
获取到这个类上所有的属性,然后遍历每个属性,判断是否有 @value、@autowired 修饰,如果有,直接封装成 autowiredfieldelement 对象,然后保存到一个名为 currelements list 容器中
最后在封装到 injectionmetadata 对象中,最终返回出去放到 injectionmetadatacache 缓存中保存,不用管他封装到哪个对象,反正这里就是扫描并解析到了哪些属性,或者方法后续需要做处理就可以了。
上面源码中 findautowiredannotation() 方法内部逻辑如下:
在 autowiredannotationbeanpostprocessor 类创建的时候,spring 就默认往 autowiredannotationtypes 容器中添加两个元素,如下:
至此,解析收集 @value 修饰的属性已经完成,最终将收集到的结果放到了 injectionmetadatacache 缓存中保存,后续需要使用直接可以从这个缓存中获取即可。
2.2、spring 为 @value 修饰属性赋值
上面通过 postprocessmergedbeandefinition() 方法收集好了 @value 注解修饰的属性,那么下面要做的就是去为这个属性进行赋值。
进入属性填充的方法,源码如下:
然后进入到 autowiredannotationbeanpostprocessor 类中,源码如下:
注意此时的 injectionmetadatacache 缓存中早已经有值了,因为前面我们就已经收集完成了 @value 修饰的属性,所以这里直接从缓存中就可以获取到。
然后进入 metadata.inject(bean, beanname, pvs)
代码内部,如下:
最终通过 resolvefieldvalue() 方法获取到属性值,然后通过反射 field.set() 方法给这个属性赋值,如下:
至此,整个 @value 的流程就算完成,下面就是对这个 resolvefieldvalue() 方法进一步分析,看下是怎么获取到属性值的,是怎么样将 $ 符号替换成解析成真正的值的
2.3、spring $ 占位符替换成真正的值
继续深入分析 resolvefieldvalue() 方法,核心源码如下:
下面这段逻辑是去解析 ${apple.name} 占位符的,这里面为什么需要递归 parsestringvalue(),因为怕你出现这种形式的占位符 ${ ${apple.name} },最终获取到 key = apple.name,然后拿着这个 key 就去资源文件(xml、application.properties、environment 等)中查找是否配置了这个 key 的值
注意这里是函数式写法,传入一个方法体 this::getpropertyasrawstring,后面会回调到这里。
当调用 resolveplaceholder() 方法时,回调到 getpropertyasrawstring() 方法,源码如下:
可以看到最终会调用到 getproperty() 方法获取到对应 key = apple.name 的值
从 propertysources 资源中获取 key = apple.name 的值,只要在这里获取到一个值就直接 return 出去即可
propertysources 这里会有三个,如下所示:
propertiespropertysource
:封装操作系统属性键值对systemenvironmentpropertysource
:封装 jvm environment 里面的键值对resourcepropertysource
:封装 application.properties、xml 中键值对
然后 debug 发现最终是从 resourcepropertysource 资源对象中获取到 apple.name 对应的值 abc,最终将 ${apple.name} 替换成真正的值 abc,最终通过反射将该值 abc 赋值到 apple 类中的 name 属性上。
那么这里肯定有很多人会有这样的疑问?这三个对象是从哪里来的呢?如果要想知道这个,需要下面一些知道做铺垫,那么继续往下看 !
2.4、理解 propertysource 和 mutablepropertysource
在 spring 中需要加载一些额外的配置文件,比如操作系统相关的配置,jvm 环境变量相关的配置,自定义配置文件等等。那么这些文件加载到代码中可定要有一个类来封装它,这个类就是 propertysource,先来看看 propertysource 的源码如下:
public abstract class propertysource<t> { protected final log logger = logfactory.getlog(getclass()); // 给这个配置文件起个名字呗 protected final string name; // 配置文件中所有的 key-value 键值对 // t 只要是 key-value 键值对即可,比如: map、properties 都可以 protected final t source; public string getname() { return this.name; } public t getsource() { return this.source; } // 根据 name 获取到对应的配置文件 @nullable public abstract object getproperty(string name); }
看完这个 propertysource 类的结构,我们看看上面三个类中封装的属性到底是啥?如下所示:
propertiespropertysource
:封装操作系统属性键值对(os.name、file.encoding、user.name 等等)
systemenvironmentpropertysource
:封装 jvm environment 里面的键值对(path、java_home)
resourcepropertysource
:封装 application.properties、xml 中键值对
理解了 propertysource 这个类之后,在来理解 mutablepropertysource 就非常容易。
先来看看 mutablepropertysource 的源码,如下:
public class mutablepropertysources { private final list<propertysource<?>> propertysourcelist = new copyonwritearraylist<>(); }
从源码中可以看到,就是做了一个收集,将所有的 propertysource 收集到一个 propertysourcelist 容器中进行管理,至于为什么这样做呢?
个人理解是为了更方便的查找属性,将所有的资源文件汇集到一起,然后想要找一个 key = apple.name 就可以直接遍历一下所有的资源文件,看下这个 key 在哪个资源文件中能够找到,找到立即返回。
但是此时就会存在一个问题,那就是文件的加载顺序,可以发现最先加载 propertiespropertysource、其次 systemenvironmentpropertysource,最后才是自定义的配置文件 resourcepropertysource!比如,我们在 application.properties 中配置 user.home=abc 如下所示:
user.home=abc
@component @propertysource("application.properties") public class apple { @value("${user.home}") public string name; }
输出结果:
name = /users/gongwm
并不是 abc,因为 propertiespropertysource 优先于 resourcepropertysource 被加载。
下面这个源码是获取资源文件,可以发现只要获取到就会立即 return,所以最终决定权交给了往 propertysources 容器添加的顺序决定,那么我们来看看上面三个文件分别是在什么时候方进去的?
2.5、spring 资源文件装载源码分析?
直接进入源码分析,如下:
然后再 standardenvironment 的构造方法中,隐式调用父类 abstractenvironment 的构造方法,源码如下:
可以发现在这里直接 new 创建了 mutablepropertysources 对象
最终可以发现这两个 systemproperties、systemenvironment 资源文件都是在这里被加载的,添加到了 mutablepropertysources 对象中
对于自定义的配置文件在 configurationclasspostprocessor 类中被加载,源码如下:
最终就是在这里被加载进去的,注意这个添加方法是 addlast() 也就是往后面追加,这个方法就体现了这些资源文件的加载顺序,那么有 addlast() ,必然有 addfirst() 等等 api,此时 propertysourcelist 容器中就已经保存了三个资源文件,并且顺序是这样的propertiespropertysource(优先级最高) -> systemenvironmentpropertysource -> resourcepropertysource (优先级最低)
最后有一点要注意,不要把 mutablepropertysources 和 mutablepropertyvalues 搞混了,两完全不是一一码事!具体想看 mutablepropertyvalues 是啥,可以转到另一篇文章!
2.6、在 environment 中添加自定义属性
借助 beandefinitionregistrypostprocessor 类来实现这个功能,如下所示:
@component public class appendattrtoenvironment implements beandefinitionregistrypostprocessor, resourceloaderaware { private resourceloader resourceloader; @override public void postprocessbeanfactory(configurablelistablebeanfactory beanfactory) throws beansexception { // 从容器中获取到 environment 对象 standardenvironment bean = (standardenvironment)beanfactory.getbean(environment.class); // 然后再获取到 mutablepropertysources 资源文件管家(它会收集打包所有的资源文件) mutablepropertysources propertysources = bean.getpropertysources(); // 创建第一个资源文件,其中有个属性 key666 值为 hangman properties properties = new properties(); properties.put("key666", "hangman"); propertiespropertysource propertiessource = new propertiespropertysource("mypropertysource",properties); // 添加到 mutablepropertysources 资源文件管家中,注意使用 addlast() 添加的,也就是往后追加 propertysources.addlast(propertiessource); // 创建第二资源文件,看下面的 abc.properties 配置文件 resource resource = resourceloader.getresource("abc.properties"); resourcepropertysource localresource = new resourcepropertysource("myresourcesource",resource); // 添加到 mutablepropertysources 资源文件管家中,注意是添加到最前面了,这样就会被最先加载 propertysources.addfirst(localresource); } @override public void postprocessbeandefinitionregistry(beandefinitionregistry registry) throws beansexception {} @override public void setresourceloader(resourceloader resourceloader) { this.resourceloader = resourceloader; } }
创建一个 abc.properties
配置文件如下所示:
key666 = 6666key777 = ggg
然后在一个 apple 类中去获取对应的值,如下所示:
@component @propertysource("application.properties") public class apple implements environmentaware { @value("${apple.name}") public string name; @override public void setenvironment(environment environment) { string property = environment.getproperty("apple.name"); string key666 = environment.getproperty("key666"); string key777 = environment.getproperty("key777"); system.out.println("key666 = " + key666); system.out.println("key777 = " + key777); system.out.println("name = " + name); system.out.println("property = " + property); } }
输出结果如下:
key666 = 6666
key777 = ggg
name = abc
property = abc
bean.name = abc
这里会发现一个问题,key = key666 的这个键值对,在 propertiessource 资源和 localresource 资源文件同时出现,最终因为 localresource 资源是通过 addfirst() 方法添加到 mutablepropertysources 管家容器最前面,优先生效。
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。
发表评论