關(guān)于Spring中@Value注解使用和源碼分析
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); } }
結(jié)果如下:
bean.name = abc
可以看到最終在 Apple 中可以獲取到配置文件中的值,那么 Spring 是怎樣解析獲取到該值的呢?下面開(kāi)始分析下。
2、@Value 注解源碼分析
分析源碼之前,我么可以簡(jiǎn)單思考下, Spring 會(huì)怎么樣去處理這個(gè) @Value 注解的?然后再帶著我們的問(wèn)題去看看 Spring 是不是和我們想的一樣呢?
簡(jiǎn)單分析有三個(gè)步驟,如下:
- 首先 Spring 肯定要去解析收集該注解,找到被 @Value 注解修飾的所有屬性,注意此時(shí)屬性的值是一個(gè)占位符 ${apple.name},并不是真正的值
- 然后加載一個(gè)資源文件,可以是本地 application.properties、也可以是其他,比如 Environment 、xml
- 最后去判斷哪個(gè)類(lèi)上的屬性有 @Value 修飾,就把占位符替換成資源文件中配置的值
在腦海中有了大致思路,再去追蹤源碼就事半功倍了。
2.1、Spring 解析并收集 @Value 修飾的屬性
Spring 解析收集 @Value 修飾的屬性和解析收集 @Autowired 注解一模一樣,懂這個(gè),@Autowired 注解解析流程也就懂了。
Spring 提供很多 BeanFactoryPostProcessor、BeanPostProcessor 接口作為擴(kuò)展,從而使 Spring 非常強(qiáng)大,因?yàn)槲覀儗傩再x值相當(dāng)于是在實(shí)例化之后的事,所以這里的 @Value 解析就是 BeanPostProcessor 接口的應(yīng)用啦!
一個(gè)大家非常熟悉的類(lèi) AutowiredAnnotationBeanPostProcessor,Spring 的 DI(依賴(lài)注入) 就是這個(gè) BeanPostProcessor 完成的,接下來(lái)看 Spring 源碼,如下:
可以看到 Spring 是在實(shí)例化之后開(kāi)始去收集的,這個(gè)時(shí)序非常重要,一定要注意
然后進(jìn)入 AutowiredAnnotationBeanPostProcessor 類(lèi)核心部分,如下:
對(duì)過(guò)來(lái)的每個(gè)類(lèi)進(jìn)行篩選判斷是否有被 @Value、@Autowired 修飾的方法或者屬性,如果找到有,就會(huì)將這個(gè)類(lèi)記錄下來(lái),放到一個(gè) injectionMetadataCache 緩存中,為后續(xù)的 DI 依賴(lài)注作準(zhǔn)備,注意哦,解析并收集到的結(jié)果最終放到 Spring 的 injectionMetadataCache 緩存中
進(jìn)入 buildAutowiringMetadata() 方法內(nèi)部,如下:
獲取到這個(gè)類(lèi)上所有的屬性,然后遍歷每個(gè)屬性,判斷是否有 @Value、@Autowired 修飾,如果有,直接封裝成 AutowiredFieldElement 對(duì)象,然后保存到一個(gè)名為 currElements List 容器中
最后在封裝到 InjectionMetadata 對(duì)象中,最終返回出去放到 injectionMetadataCache 緩存中保存,不用管他封裝到哪個(gè)對(duì)象,反正這里就是掃描并解析到了哪些屬性,或者方法后續(xù)需要做處理就可以了。
上面源碼中 findAutowiredAnnotation() 方法內(nèi)部邏輯如下:
在 AutowiredAnnotationBeanPostProcessor 類(lèi)創(chuàng)建的時(shí)候,Spring 就默認(rèn)往 autowiredAnnotationTypes 容器中添加兩個(gè)元素,如下:
至此,解析收集 @Value 修飾的屬性已經(jīng)完成,最終將收集到的結(jié)果放到了 injectionMetadataCache 緩存中保存,后續(xù)需要使用直接可以從這個(gè)緩存中獲取即可。
2.2、Spring 為 @Value 修飾屬性賦值
上面通過(guò) postProcessMergedBeanDefinition() 方法收集好了 @Value 注解修飾的屬性,那么下面要做的就是去為這個(gè)屬性進(jìn)行賦值。
進(jìn)入屬性填充的方法,源碼如下:
然后進(jìn)入到 AutowiredAnnotationBeanPostProcessor 類(lèi)中,源碼如下:
注意此時(shí)的 injectionMetadataCache 緩存中早已經(jīng)有值了,因?yàn)榍懊嫖覀兙鸵呀?jīng)收集完成了 @Value 修飾的屬性,所以這里直接從緩存中就可以獲取到。
然后進(jìn)入 metadata.inject(bean, beanName, pvs)
代碼內(nèi)部,如下:
最終通過(guò) resolveFieldValue() 方法獲取到屬性值,然后通過(guò)反射 field.set() 方法給這個(gè)屬性賦值,如下:
至此,整個(gè) @Value 的流程就算完成,下面就是對(duì)這個(gè) resolveFieldValue() 方法進(jìn)一步分析,看下是怎么獲取到屬性值的,是怎么樣將 $ 符號(hào)替換成解析成真正的值的
2.3、Spring $ 占位符替換成真正的值
繼續(xù)深入分析 resolveFieldValue() 方法,核心源碼如下:
下面這段邏輯是去解析 ${apple.name} 占位符的,這里面為什么需要遞歸 parseStringValue(),因?yàn)榕履愠霈F(xiàn)這種形式的占位符 ${ ${apple.name} },最終獲取到 key = apple.name,然后拿著這個(gè) key 就去資源文件(xml、application.properties、Environment 等)中查找是否配置了這個(gè) key 的值
注意這里是函數(shù)式寫(xiě)法,傳入一個(gè)方法體 this::getPropertyAsRawString,后面會(huì)回調(diào)到這里。
當(dāng)調(diào)用 resolvePlaceholder() 方法時(shí),回調(diào)到 getPropertyAsRawString() 方法,源碼如下:
可以看到最終會(huì)調(diào)用到 getProperty() 方法獲取到對(duì)應(yīng) key = apple.name 的值
從 propertySources 資源中獲取 key = apple.name 的值,只要在這里獲取到一個(gè)值就直接 return 出去即可
propertySources 這里會(huì)有三個(gè),如下所示:
PropertiesPropertySource
:封裝操作系統(tǒng)屬性鍵值對(duì)SystemEnvironmentPropertySource
:封裝 JVM Environment 里面的鍵值對(duì)ResourcePropertySource
:封裝 application.properties、xml 中鍵值對(duì)
然后 debug 發(fā)現(xiàn)最終是從 ResourcePropertySource 資源對(duì)象中獲取到 apple.name 對(duì)應(yīng)的值 abc,最終將 ${apple.name} 替換成真正的值 abc,最終通過(guò)反射將該值 abc 賦值到 Apple 類(lèi)中的 name 屬性上。
那么這里肯定有很多人會(huì)有這樣的疑問(wèn)?這三個(gè)對(duì)象是從哪里來(lái)的呢?如果要想知道這個(gè),需要下面一些知道做鋪墊,那么繼續(xù)往下看 !
2.4、理解 PropertySource 和 MutablePropertySource
在 Spring 中需要加載一些額外的配置文件,比如操作系統(tǒng)相關(guān)的配置,JVM 環(huán)境變量相關(guān)的配置,自定義配置文件等等。那么這些文件加載到代碼中可定要有一個(gè)類(lèi)來(lái)封裝它,這個(gè)類(lèi)就是 PropertySource,先來(lái)看看 PropertySource 的源碼如下:
public abstract class PropertySource<T> { protected final Log logger = LogFactory.getLog(getClass()); // 給這個(gè)配置文件起個(gè)名字唄 protected final String name; // 配置文件中所有的 key-value 鍵值對(duì) // T 只要是 key-value 鍵值對(duì)即可,比如: Map、Properties 都可以 protected final T source; public String getName() { return this.name; } public T getSource() { return this.source; } // 根據(jù) name 獲取到對(duì)應(yīng)的配置文件 @Nullable public abstract Object getProperty(String name); }
看完這個(gè) PropertySource 類(lèi)的結(jié)構(gòu),我們看看上面三個(gè)類(lèi)中封裝的屬性到底是啥?如下所示:
PropertiesPropertySource
:封裝操作系統(tǒng)屬性鍵值對(duì)(os.name、file.encoding、user.name 等等)
SystemEnvironmentPropertySource
:封裝 JVM Environment 里面的鍵值對(duì)(PATH、JAVA_HOME)
ResourcePropertySource
:封裝 application.properties、xml 中鍵值對(duì)
理解了 PropertySource 這個(gè)類(lèi)之后,在來(lái)理解 MutablePropertySource 就非常容易。
先來(lái)看看 MutablePropertySource 的源碼,如下:
public class MutablePropertySources { private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<>(); }
從源碼中可以看到,就是做了一個(gè)收集,將所有的 PropertySource 收集到一個(gè) propertySourceList 容器中進(jìn)行管理,至于為什么這樣做呢?
個(gè)人理解是為了更方便的查找屬性,將所有的資源文件匯集到一起,然后想要找一個(gè) key = apple.name 就可以直接遍歷一下所有的資源文件,看下這個(gè) key 在哪個(gè)資源文件中能夠找到,找到立即返回。
但是此時(shí)就會(huì)存在一個(gè)問(wèn)題,那就是文件的加載順序,可以發(fā)現(xiàn)最先加載 PropertiesPropertySource、其次 SystemEnvironmentPropertySource,最后才是自定義的配置文件 ResourcePropertySource!比如,我們?cè)?application.properties 中配置 user.home=abc 如下所示:
user.home=abc
@Component @PropertySource("application.properties") public class Apple { @Value("${user.home}") public String name; }
輸出結(jié)果:
name = /Users/gongwm
并不是 abc,因?yàn)?PropertiesPropertySource 優(yōu)先于 ResourcePropertySource 被加載。
下面這個(gè)源碼是獲取資源文件,可以發(fā)現(xiàn)只要獲取到就會(huì)立即 return,所以最終決定權(quán)交給了往 propertySources 容器添加的順序決定,那么我們來(lái)看看上面三個(gè)文件分別是在什么時(shí)候方進(jìn)去的?
2.5、Spring 資源文件裝載源碼分析?
直接進(jìn)入源碼分析,如下:
然后再 StandardEnvironment 的構(gòu)造方法中,隱式調(diào)用父類(lèi) AbstractEnvironment 的構(gòu)造方法,源碼如下:
可以發(fā)現(xiàn)在這里直接 new 創(chuàng)建了 MutablePropertySources 對(duì)象
最終可以發(fā)現(xiàn)這兩個(gè) systemProperties、systemEnvironment 資源文件都是在這里被加載的,添加到了 MutablePropertySources 對(duì)象中
對(duì)于自定義的配置文件在 ConfigurationClassPostProcessor 類(lèi)中被加載,源碼如下:
最終就是在這里被加載進(jìn)去的,注意這個(gè)添加方法是 addLast() 也就是往后面追加,這個(gè)方法就體現(xiàn)了這些資源文件的加載順序,那么有 addLast() ,必然有 addFirst() 等等 API,此時(shí) propertySourceList 容器中就已經(jīng)保存了三個(gè)資源文件,并且順序是這樣的PropertiesPropertySource(優(yōu)先級(jí)最高) -> SystemEnvironmentPropertySource -> ResourcePropertySource (優(yōu)先級(jí)最低)
最后有一點(diǎn)要注意,不要把 MutablePropertySources 和 MutablePropertyValues 搞混了,兩完全不是一一碼事!具體想看 MutablePropertyValues 是啥,可以轉(zhuǎn)到另一篇文章!
2.6、在 Environment 中添加自定義屬性
借助 BeanDefinitionRegistryPostProcessor 類(lèi)來(lái)實(shí)現(xiàn)這個(gè)功能,如下所示:
@Component public class AppendAttrToEnvironment implements BeanDefinitionRegistryPostProcessor, ResourceLoaderAware { private ResourceLoader resourceLoader; @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { // 從容器中獲取到 Environment 對(duì)象 StandardEnvironment bean = (StandardEnvironment)beanFactory.getBean(Environment.class); // 然后再獲取到 MutablePropertySources 資源文件管家(它會(huì)收集打包所有的資源文件) MutablePropertySources propertySources = bean.getPropertySources(); // 創(chuàng)建第一個(gè)資源文件,其中有個(gè)屬性 key666 值為 hangman Properties properties = new Properties(); properties.put("key666", "hangman"); PropertiesPropertySource propertiesSource = new PropertiesPropertySource("myPropertySource",properties); // 添加到 MutablePropertySources 資源文件管家中,注意使用 addLast() 添加的,也就是往后追加 propertySources.addLast(propertiesSource); // 創(chuàng)建第二資源文件,看下面的 abc.properties 配置文件 Resource resource = resourceLoader.getResource("abc.properties"); ResourcePropertySource localResource = new ResourcePropertySource("myResourceSource",resource); // 添加到 MutablePropertySources 資源文件管家中,注意是添加到最前面了,這樣就會(huì)被最先加載 propertySources.addFirst(localResource); } @Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {} @Override public void setResourceLoader(ResourceLoader resourceLoader) { this.resourceLoader = resourceLoader; } }
創(chuàng)建一個(gè) abc.properties
配置文件如下所示:
key666 = 6666key777 = ggg
然后在一個(gè) Apple 類(lèi)中去獲取對(duì)應(yīng)的值,如下所示:
@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); } }
輸出結(jié)果如下:
key666 = 6666
key777 = ggg
name = abc
property = abc
bean.name = abc
這里會(huì)發(fā)現(xiàn)一個(gè)問(wèn)題,key = key666 的這個(gè)鍵值對(duì),在 propertiesSource 資源和 localResource 資源文件同時(shí)出現(xiàn),最終因?yàn)?localResource 資源是通過(guò) addFirst() 方法添加到 MutablePropertySources 管家容器最前面,優(yōu)先生效。
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
使用Java實(shí)現(xiàn)一個(gè)解析CURL腳本小工具
文章介紹了如何使用Java實(shí)現(xiàn)一個(gè)解析CURL腳本的工具,該工具可以將CURL腳本中的Header解析為KV Map結(jié)構(gòu),獲取URL路徑、請(qǐng)求類(lèi)型,解析URL參數(shù)列表和Body請(qǐng)求體,感興趣的小伙伴跟著小編一起來(lái)看看吧2025-02-02Springboot中yml文件沒(méi)有葉子圖標(biāo)的解決
這篇文章主要介紹了Springboot中yml文件沒(méi)有葉子圖標(biāo)的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-09-09Logger.getLogger()與LogFactory.getLog()的區(qū)別詳解
LogFactory來(lái)自common-logging包。如果用LogFactory.getLog,你可以用任何實(shí)現(xiàn)了通用日志接口的日志記錄器替換log4j,而程序不受影響2013-09-09Mybatis-plus與Mybatis依賴(lài)沖突問(wèn)題解決方法
,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧這篇文章主要介紹了Mybatis-plus與Mybatis依賴(lài)沖突問(wèn)題解決方法2021-04-04完美解決idea沒(méi)有tomcat server選項(xiàng)的問(wèn)題
這篇文章主要介紹了完美解決idea沒(méi)有tomcat server選項(xiàng)的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-01-01Windows編寫(xiě)jar啟動(dòng)腳本和關(guān)閉腳本的操作方法
腳本文件,通常放入/bin目錄下,編寫(xiě)啟動(dòng)腳本需要保證能夠識(shí)別到對(duì)應(yīng)的jar文件,其次需要保證能夠識(shí)別到/config中的配置文件信息,這篇文章主要介紹了Windows編寫(xiě)jar啟動(dòng)腳本和關(guān)閉腳本的操作方法,需要的朋友可以參考下2022-12-12Java concurrency集合之CopyOnWriteArraySet_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
CopyOnWriteArraySet基于CopyOnWriteArrayList實(shí)現(xiàn),其唯一的不同是在add時(shí)調(diào)用的是CopyOnWriteArrayList的addIfAbsent(若沒(méi)有則增加)方法2017-06-06java?數(shù)組實(shí)現(xiàn)學(xué)生成績(jī)統(tǒng)計(jì)教程
這篇文章主要介紹了java?數(shù)組實(shí)現(xiàn)學(xué)生成績(jī)統(tǒng)計(jì)教程,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12