1. 程式人生 > 其它 >@Value:讀取配置的值注入給 bean 的屬性

@Value:讀取配置的值注入給 bean 的屬性

面試官:Spring中的@Value用過麼,介紹一下

我:@Value可以標註在欄位上面,可以將外部配置檔案中的資料,比如可以將資料庫的一些配置資訊放在配置檔案中,然後通過@Value的方式將其注入到bean的一些欄位中

面試官:那就是說@Value的資料來源於配置檔案了?

我:嗯,我們專案最常用更多就是通過@Value來引用Properties檔案中的配置

面試官:@Value資料來源還有其他方式麼?

我:此時我異常開心,剛好問的我都研究過,我說:當然有,可以將配置資訊放在db或者其他儲存介質中,容器啟動的時候,可以將這些資訊載入到Environment中,@Value中應用的值最終是通過Environment來解析的,所以只需要擴充套件一下Environment就可以實現了。

面試官:不錯嘛,看來你對spring研究的還是可以,是不是喜歡研究spring原始碼?

我:笑著說,嗯,平時有空的時候確實喜歡搗鼓搗鼓原始碼,感覺自己對spring瞭解的還可以,不能算精通,也算是半精通吧

面試官:看著我笑了笑,那@Value的注入的值可以動態重新整理麼?

我:應該可以吧,我記得springboot中有個@RefreshScope註解就可以實現你說的這個功能

面試官:那你可以說一下@RefreshScope是如何實現的麼,可以大概介紹一下?

我:嗯。。。這個之前看過一點,不過沒有看懂

面試官:沒關係,你可以回去了再研究一下;你期望工資多少?

我:3萬吧

面試官:今天的面試還算是可以的,不過如果@RefreshScope能回答上來就更好了,這塊是個加分項,不過也確實有點難度,2.5萬如何?

我:(心中默默想了想:2.5萬,就是一個問題沒有回答好,砍了5000,有點狠啊,我要回去再研究研究,3萬肯定是沒問題的),我說:最低2.9萬

面試官:那謝謝你,今天面試就到這裡,出門右拐,不送!

我有個好習慣,每次面試回去之後,都會進行復盤,把沒有搞定的問題一定要想辦法搞定,這樣才不虛。

這次面試問題如下

  1. @Value的用法

  2. @Value資料來源

  3. @Value動態重新整理的問題

下面我們一個個來整理一下,將這幾個問題搞定,助大家在疫情期間面試能夠過關斬將,拿高薪。

@Value的用法

系統中需要連線db,連線db有很多配置資訊。

系統中需要傳送郵件,傳送郵件需要配置郵件伺服器的資訊。

還有其他的一些配置資訊。

我們可以將這些配置資訊統一放在一個配置檔案中,上線的時候由運維統一修改。

那麼系統中如何使用這些配置資訊呢,spring中提供了@Value註解來解決這個問題。

通常我們會將配置資訊以key=value的形式儲存在properties配置檔案中。

通過@Value("${配置檔案中的key}")來引用指定的key對應的value。

@Value使用步驟

步驟一:使用@PropertySource註解引入配置檔案

將@PropertySource放在類上面,如下

@PropertySource({"配置檔案路徑1","配置檔案路徑2"...})

@PropertySource註解有個value屬性,字串陣列型別,可以用來指定多個配置檔案的路徑。

如:

@Component
@PropertySource({"classpath:com/javacode2018/lesson002/demo18/db.properties"})
publicclassDbConfig{
}

步驟二:使用@Value註解引用配置檔案的值

通過@Value引用上面配置檔案中的值:

語法

@Value("${配置檔案中的key:預設值}")
@Value("${配置檔案中的key}")

如:

@Value("${password:123}")

上面如果password不存在,將123作為值

@Value("${password}")

上面如果password不存在,值為${password}

假如配置檔案如下

jdbc.url=jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-8
jdbc.username=javacode
jdbc.password=javacode

使用方式如下:

@Value("${jdbc.url}")
privateStringurl;

@Value("${jdbc.username}")
privateStringusername;

@Value("${jdbc.password}")
privateStringpassword;

下面來看案例

案例

來個配置檔案db.properties
jdbc.url=jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-8
jdbc.username=javacode
jdbc.password=javacode
來個配置類,使用@PropertySource引入上面的配置檔案
packagecom.javacode2018.lesson002.demo18.test1;

importorg.springframework.beans.factory.annotation.Configurable;
importorg.springframework.context.annotation.ComponentScan;
importorg.springframework.context.annotation.PropertySource;

@Configurable
@ComponentScan
@PropertySource({"classpath:com/javacode2018/lesson002/demo18/db.properties"})
publicclassMainConfig1{
}
來個類,使用@Value來使用配置檔案中的資訊
packagecom.javacode2018.lesson002.demo18.test1;

importorg.springframework.beans.factory.annotation.Value;
importorg.springframework.stereotype.Component;

@Component
publicclassDbConfig{

@Value("${jdbc.url}")
privateStringurl;

@Value("${jdbc.username}")
privateStringusername;

@Value("${jdbc.password}")
privateStringpassword;

publicStringgetUrl(){
returnurl;
}

publicvoidsetUrl(Stringurl){
this.url=url;
}

publicStringgetUsername(){
returnusername;
}

publicvoidsetUsername(Stringusername){
this.username=username;
}

publicStringgetPassword(){
returnpassword;
}

publicvoidsetPassword(Stringpassword){
this.password=password;
}

@Override
publicStringtoString(){
return"DbConfig{"+
"url='"+url+'\''+
",username='"+username+'\''+
",password='"+password+'\''+
'}';
}
}

上面重點在於註解@Value註解,注意@Value註解中的

來個測試用例
packagecom.javacode2018.lesson002.demo18;

importcom.javacode2018.lesson002.demo18.test1.DbConfig;
importcom.javacode2018.lesson002.demo18.test1.MainConfig1;
importorg.junit.Test;
importorg.springframework.context.annotation.AnnotationConfigApplicationContext;

publicclassValueTest{

@Test
publicvoidtest1(){
AnnotationConfigApplicationContextcontext=newAnnotationConfigApplicationContext();
context.register(MainConfig1.class);
context.refresh();

DbConfigdbConfig=context.getBean(DbConfig.class);
System.out.println(dbConfig);
}
}
執行輸出
DbConfig{url='jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-8',username='javacode',password='javacode'}

上面用起來比較簡單,很多用過的人看一眼就懂了,這也是第一個問題,多數人都是ok的,下面來看@Value中資料來源除了配置檔案的方式,是否還有其他方式。

@Value資料來源

通常情況下我們@Value的資料來源於配置檔案,不過,還可以用其他方式,比如我們可以將配置檔案的內容放在資料庫,這樣修改起來更容易一些。

我們需要先了解一下@Value中資料來源於spring的什麼地方。

spring中有個類

org.springframework.core.env.PropertySource

可以將其理解為一個配置源,裡面包含了key->value的配置資訊,可以通過這個類中提供的方法獲取key對應的value資訊

內部有個方法:

publicabstractObjectgetProperty(Stringname);

通過name獲取對應的配置資訊。

系統有個比較重要的介面

org.springframework.core.env.Environment

用來表示環境配置資訊,這個介面有幾個方法比較重要

StringresolvePlaceholders(Stringtext);
MutablePropertySourcesgetPropertySources();

resolvePlaceholders用來解析${text}的,@Value註解最後就是呼叫這個方法來解析的。

getPropertySources返回MutablePropertySources物件,來看一下這個類

publicclassMutablePropertySourcesimplementsPropertySources{

privatefinalList<PropertySource<?>>propertySourceList=newCopyOnWriteArrayList<>();

}

內部包含一個propertySourceList列表。

spring容器中會有一個Environment物件,最後會呼叫這個物件的resolvePlaceholders方法解析@Value。

大家可以捋一下,最終解析@Value的過程:

1.將@Value註解的value引數值作為Environment.resolvePlaceholders方法引數進行解析
2.Environment內部會訪問MutablePropertySources來解析
3.MutablePropertySources內部有多個PropertySource,此時會遍歷PropertySource列表,呼叫PropertySource.getProperty方法來解析key對應的值

通過上面過程,如果我們想改變@Value資料的來源,只需要將配置資訊包裝為PropertySource物件,丟到Environment中的MutablePropertySources內部就可以了。

下面我們就按照這個思路來一個。

來個郵件配置資訊類,內部使用@Value注入郵件配置資訊

packagecom.javacode2018.lesson002.demo18.test2;

importorg.springframework.beans.factory.annotation.Value;
importorg.springframework.stereotype.Component;

/**
*郵件配置資訊
*/

@Component
publicclassMailConfig{

@Value("${mail.host}")
privateStringhost;

@Value("${mail.username}")
privateStringusername;

@Value("${mail.password}")
privateStringpassword;

publicStringgetHost(){
returnhost;
}

publicvoidsetHost(Stringhost){
this.host=host;
}

publicStringgetUsername(){
returnusername;
}

publicvoidsetUsername(Stringusername){
this.username=username;
}

publicStringgetPassword(){
returnpassword;
}

publicvoidsetPassword(Stringpassword){
this.password=password;
}

@Override
publicStringtoString(){
return"MailConfig{"+
"host='"+host+'\''+
",username='"+username+'\''+
",password='"+password+'\''+
'}';
}
}

再來個類DbUtilgetMailInfoFromDb方法模擬從db中獲取郵件配置資訊,存放在map中

packagecom.javacode2018.lesson002.demo18.test2;

importjava.util.HashMap;
importjava.util.Map;

publicclassDbUtil{
/**
*模擬從db中獲取郵件配置資訊
*
*@return
*/

publicstaticMap<String,Object>getMailInfoFromDb(){
Map<String,Object>result=newHashMap<>();
result.put("mail.host","smtp.qq.com");
result.put("mail.username","路人");
result.put("mail.password","123");
returnresult;
}
}

來個spring配置類

packagecom.javacode2018.lesson002.demo18.test2;

importorg.springframework.context.annotation.ComponentScan;
importorg.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan
publicclassMainConfig2{
}

下面是重點程式碼

@Test
publicvoidtest2(){
AnnotationConfigApplicationContextcontext=newAnnotationConfigApplicationContext();

/*下面這段是關鍵start*/
//模擬從db中獲取配置資訊
Map<String,Object>mailInfoFromDb=DbUtil.getMailInfoFromDb();
//將其丟在MapPropertySource中(MapPropertySource類是spring提供的一個類,是PropertySource的子類)
MapPropertySourcemailPropertySource=newMapPropertySource("mail",mailInfoFromDb);
//將mailPropertySource丟在Environment中的PropertySource列表的第一個中,讓優先順序最高
context.getEnvironment().getPropertySources().addFirst(mailPropertySource);
/*上面這段是關鍵end*/

context.register(MainConfig2.class);
context.refresh();
MailConfigmailConfig=context.getBean(MailConfig.class);
System.out.println(mailConfig);
}

註釋比較詳細,就不詳細解釋了。

直接執行,看效果

MailConfig{host='smtp.qq.com',username='路人',password='123'}

有沒有感覺很爽,此時你們可以隨意修改DbUtil.getMailInfoFromDb,具體資料是從db中來,來時從redis或者其他介質中來,任由大家發揮。

上面重點是下面這段程式碼,大家需要理解

/*下面這段是關鍵start*/
//模擬從db中獲取配置資訊
Map<String,Object>mailInfoFromDb=DbUtil.getMailInfoFromDb();
//將其丟在MapPropertySource中(MapPropertySource類是spring提供的一個類,是PropertySource的子類)
MapPropertySourcemailPropertySource=newMapPropertySource("mail",mailInfoFromDb);
//將mailPropertySource丟在Environment中的PropertySource列表的第一個中,讓優先順序最高
context.getEnvironment().getPropertySources().addFirst(mailPropertySource);
/*上面這段是關鍵end*/

咱們繼續看下一個問題

如果我們將配置資訊放在db中,可能我們會通過一個介面來修改這些配置資訊,然後儲存之後,希望系統在不重啟的情況下,讓這些值在spring容器中立即生效。

@Value動態重新整理的問題的問題,springboot中使用@RefreshScope實現了。

實現@Value動態重新整理

先了解一個知識點

這塊需要先講一個知識點,用到的不是太多,所以很多人估計不太瞭解,但是非常重要的一個點,我們來看一下。

這個知識點是自定義bean作用域,對這塊不瞭解的先看一下這篇文章:bean作用域詳解

bean作用域中有個地方沒有講,來看一下@Scope這個註解的原始碼,有個引數是:

ScopedProxyModeproxyMode()defaultScopedProxyMode.DEFAULT;

這個引數的值是個ScopedProxyMode型別的列舉,值有下面4中

publicenumScopedProxyMode{
DEFAULT,
NO,
INTERFACES,
TARGET_CLASS;
}

前面3個,不講了,直接講最後一個值是幹什麼的。

當@Scope中proxyMode為TARGET_CLASS的時候,會給當前建立的bean通過cglib生成一個代理物件,通過這個代理物件來訪問目標bean物件。

理解起來比較晦澀,還是來看程式碼吧,容易理解一些,來個自定義的Scope案例。

自定義一個bean作用域的註解

packagecom.javacode2018.lesson002.demo18.test3;

importorg.springframework.context.annotation.Scope;
importorg.springframework.context.annotation.ScopedProxyMode;

importjava.lang.annotation.*;

@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Scope(BeanMyScope.SCOPE_MY)//@1
public@interfaceMyScope{
/**
*@seeScope#proxyMode()
*/

ScopedProxyModeproxyMode()defaultScopedProxyMode.TARGET_CLASS;//@2
}

@1:使用了@Scope註解,value為引用了一個常量,值為my,一會下面可以看到。

@2:注意這個地方,引數名稱也是proxyMode,型別也是ScopedProxyMode,而@Scope註解中有個和這個同樣型別的引數,spring容器解析的時候,會將這個引數的值賦給@MyScope註解上面的@Scope註解的proxyMode引數,所以此處我們設定proxyMode值,最後的效果就是直接改變了@Scope中proxyMode引數的值。此處預設值取的是ScopedProxyMode.TARGET_CLASS

@MyScope註解對應的Scope實現如下

packagecom.javacode2018.lesson002.demo18.test3;

importorg.springframework.beans.factory.ObjectFactory;
importorg.springframework.beans.factory.config.Scope;
importorg.springframework.lang.Nullable;

/**
*@seeMyScope作用域的實現
*/

publicclassBeanMyScopeimplementsScope{

publicstaticfinalStringSCOPE_MY="my";//@1

@Override
publicObjectget(Stringname,ObjectFactory<?>objectFactory){
System.out.println("BeanMyScope>>>>>>>>>get:"+name);//@2
returnobjectFactory.getObject();//@3
}

@Nullable
@Override
publicObjectremove(Stringname){
returnnull;
}

@Override
publicvoidregisterDestructionCallback(Stringname,Runnablecallback){

}

@Nullable
@Override
publicObjectresolveContextualObject(Stringkey){
returnnull;
}

@Nullable
@Override
publicStringgetConversationId(){
returnnull;
}
}

@1:定義了一個常量,作為作用域的值

@2:這個get方法是關鍵,自定義作用域會自動呼叫這個get方法來建立bean物件,這個地方輸出了一行日誌,為了一會方便看效果

@3:通過objectFactory.getObject()獲取bean例項返回。

下面來建立個類,作用域為上面自定義的作用域

packagecom.javacode2018.lesson002.demo18.test3;

importorg.springframework.stereotype.Component;

importjava.util.UUID;

@Component
@MyScope//@1
publicclassUser{

privateStringusername;

publicUser(){
System.out.println("---------建立User物件"+this);//@2
this.username=UUID.randomUUID().toString();//@3
}

publicStringgetUsername(){
returnusername;
}

publicvoidsetUsername(Stringusername){
this.username=username;
}

}

@1:使用了自定義的作用域@MyScope

@2:建構函式中輸出一行日誌

@3:給username賦值,通過uuid隨機生成了一個

來個spring配置類,載入上面@Compontent標註的元件

packagecom.javacode2018.lesson002.demo18.test3;

importorg.springframework.context.annotation.ComponentScan;
importorg.springframework.context.annotation.Configuration;

@ComponentScan
@Configuration
publicclassMainConfig3{
}

下面重點來了,測試用例

@Test
publicvoidtest3()throwsInterruptedException{
AnnotationConfigApplicationContextcontext=newAnnotationConfigApplicationContext();
//將自定義作用域註冊到spring容器中
context.getBeanFactory().registerScope(BeanMyScope.SCOPE_MY,newBeanMyScope());//@1
context.register(MainConfig3.class);
context.refresh();

System.out.println("從容器中獲取User物件");
Useruser=context.getBean(User.class);//@2
System.out.println("user物件的class為:"+user.getClass());//@3

System.out.println("多次呼叫user的getUsername感受一下效果\n");
for(inti=1;i<=3;i++){
System.out.println(String.format("********\n第%d次開始呼叫getUsername",i));
System.out.println(user.getUsername());
System.out.println(String.format("第%d次呼叫getUsername結束\n********\n",i));
}
}

@1:將自定義作用域註冊到spring容器中

@2:從容器中獲取User對應的bean

@3:輸出這個bean對應的class,一會認真看一下,這個型別是不是User型別的

程式碼後面又搞了3次迴圈,呼叫user的getUsername方法,並且方法前後分別輸出了一行日誌。

見證奇蹟的時候到了,執行輸出

從容器中獲取User物件
user物件的class為:classcom.javacode2018.lesson002.demo18.test3.User$$EnhancerBySpringCGLIB$$80233127
多次呼叫usergetUsername感受一下效果

********
第1次開始呼叫getUsername
BeanMyScope>>>>>>>>>get:scopedTarget.user
---------建立User物件com.javacode2018.lesson002.demo18.test3.User@6a370f4
7b41aa80-7569-4072-9d40-ec9bfb92f438
第1次呼叫getUsername結束
********

********
第2次開始呼叫getUsername
BeanMyScope>>>>>>>>>get:scopedTarget.user
---------建立User物件com.javacode2018.lesson002.demo18.test3.User@1613674b
01d67154-95f6-44bb-93ab-05a34abdf51f
第2次呼叫getUsername結束
********

********
第3次開始呼叫getUsername
BeanMyScope>>>>>>>>>get:scopedTarget.user
---------建立User物件com.javacode2018.lesson002.demo18.test3.User@27ff5d15
76d0e86f-8331-4303-aac7-4acce0b258b8
第3次呼叫getUsername結束
********

從輸出的前2行可以看出:

  1. 呼叫context.getBean(User.class)從容器中獲取bean的時候,此時並沒有呼叫User的建構函式去建立User物件

  2. 第二行輸出的型別可以看出,getBean返回的user物件是一個cglib代理物件。

後面的日誌輸出可以看出,每次呼叫user.getUsername方法的時候,內部自動呼叫了BeanMyScope#get 方法和 User的建構函式。

通過上面的案例可以看出,當自定義的Scope中proxyMode=ScopedProxyMode.TARGET_CLASS的時候,會給這個bean建立一個代理物件,呼叫代理物件的任何方法,都會呼叫這個自定義的作用域實現類(上面的BeanMyScope)中get方法來重新來獲取這個bean物件。

動態重新整理@Value具體實現

那麼我們可以利用上面講解的這種特性來實現@Value的動態重新整理,可以實現一個自定義的Scope,這個自定義的Scope支援@Value註解自動重新整理,需要使用@Value註解自動重新整理的類上面可以標註這個自定義的註解,當配置修改的時候,呼叫這些bean的任意方法的時候,就讓spring重啟初始化一下這個bean,這個思路就可以實現了,下面我們來寫程式碼。

先來自定義一個Scope:RefreshScope

packagecom.javacode2018.lesson002.demo18.test4;

importorg.springframework.context.annotation.Scope;
importorg.springframework.context.annotation.ScopedProxyMode;

importjava.lang.annotation.*;

@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Scope(BeanRefreshScope.SCOPE_REFRESH)
@Documented
public@interfaceRefreshScope{
ScopedProxyModeproxyMode()defaultScopedProxyMode.TARGET_CLASS;//@1
}

要求標註@RefreshScope註解的類支援動態重新整理@Value的配置

@1:這個地方是個關鍵,使用的是ScopedProxyMode.TARGET_CLASS

這個自定義Scope對應的解析類

下面類中有幾個無關的方法去掉了,可以忽略

packagecom.javacode2018.lesson002.demo18.test4;


importorg.springframework.beans.factory.ObjectFactory;
importorg.springframework.beans.factory.config.Scope;
importorg.springframework.lang.Nullable;

importjava.util.concurrent.ConcurrentHashMap;

publicclassBeanRefreshScopeimplementsScope{

publicstaticfinalStringSCOPE_REFRESH="refresh";

privatestaticfinalBeanRefreshScopeINSTANCE=newBeanRefreshScope();

//來個map用來快取bean
privateConcurrentHashMap<String,Object>beanMap=newConcurrentHashMap<>();//@1

privateBeanRefreshScope(){
}

publicstaticBeanRefreshScopegetInstance(){
returnINSTANCE;
}

/**
*清理當前
*/

publicstaticvoidclean(){
INSTANCE.beanMap.clear();
}

@Override
publicObjectget(Stringname,ObjectFactory<?>objectFactory){
Objectbean=beanMap.get(name);
if(bean==null){
bean=objectFactory.getObject();
beanMap.put(name,bean);
}
returnbean;
}

}

上面的get方法會先從beanMap中獲取,獲取不到會呼叫objectFactory的getObject讓spring建立bean的例項,然後丟到beanMap中

上面的clean方法用來清理beanMap中當前已快取的所有bean

來個郵件配置類,使用@Value註解注入配置,這個bean作用域為自定義的@RefreshScope

packagecom.javacode2018.lesson002.demo18.test4;

importorg.springframework.beans.factory.annotation.Value;
importorg.springframework.stereotype.Component;

/**
*郵件配置資訊
*/

@Component
@RefreshScope//@1
publicclassMailConfig{

@Value("${mail.username}")//@2
privateStringusername;

publicStringgetUsername(){
returnusername;
}

publicvoidsetUsername(Stringusername){
this.username=username;
}

@Override
publicStringtoString(){
return"MailConfig{"+
"username='"+username+'\''+
'}';
}
}

@1:使用了自定義的作用域@RefreshScope

@2:通過@Value注入mail.username對一個的值

重寫了toString方法,一會測試時候可以看效果。

再來個普通的bean,內部會注入MailConfig

packagecom.javacode2018.lesson002.demo18.test4;

importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.stereotype.Component;

@Component
publicclassMailService{
@Autowired
privateMailConfigmailConfig;

@Override
publicStringtoString(){
return"MailService{"+
"mailConfig="+mailConfig+
'}';
}
}

程式碼比較簡單,重寫了toString方法,一會測試時候可以看效果。

來個類,用來從db中獲取郵件配置資訊

packagecom.javacode2018.lesson002.demo18.test4;

importjava.util.HashMap;
importjava.util.Map;
importjava.util.UUID;

publicclassDbUtil{
/**
*模擬從db中獲取郵件配置資訊
*
*@return
*/

publicstaticMap<String,Object>getMailInfoFromDb(){
Map<String,Object>result=newHashMap<>();
result.put("mail.username",UUID.randomUUID().toString());
returnresult;
}
}

來個spring配置類,掃描載入上面的元件

packagecom.javacode2018.lesson002.demo18.test4;

importorg.springframework.context.annotation.ComponentScan;
importorg.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan
publicclassMainConfig4{
}

來個工具類

內部有2個方法,如下:

packagecom.javacode2018.lesson002.demo18.test4;

importorg.springframework.context.support.AbstractApplicationContext;
importorg.springframework.core.env.MapPropertySource;

importjava.util.Map;

publicclassRefreshConfigUtil{
/**
*模擬改變資料庫中都配置資訊
*/

publicstaticvoidupdateDbConfig(AbstractApplicationContextcontext){
//更新context中的mailPropertySource配置資訊
refreshMailPropertySource(context);

//清空BeanRefreshScope中所有bean的快取
BeanRefreshScope.getInstance().clean();
}

publicstaticvoidrefreshMailPropertySource(AbstractApplicationContextcontext){
Map<String,Object>mailInfoFromDb=DbUtil.getMailInfoFromDb();
//將其丟在MapPropertySource中(MapPropertySource類是spring提供的一個類,是PropertySource的子類)
MapPropertySourcemailPropertySource=newMapPropertySource("mail",mailInfoFromDb);
context.getEnvironment().getPropertySources().addFirst(mailPropertySource);
}

}

updateDbConfig方法模擬修改db中配置的時候需要呼叫的方法,方法中2行程式碼,第一行程式碼呼叫refreshMailPropertySource方法修改容器中郵件的配置資訊

BeanRefreshScope.getInstance().clean()用來清除BeanRefreshScope中所有已經快取的bean,那麼呼叫bean的任意方法的時候,會重新出發spring容器來建立bean,spring容器重新建立bean的時候,會重新解析@Value的資訊,此時容器中的郵件配置資訊是新的,所以@Value注入的資訊也是新的。

來個測試用例

@Test
publicvoidtest4()throwsInterruptedException{
AnnotationConfigApplicationContextcontext=newAnnotationConfigApplicationContext();
context.getBeanFactory().registerScope(BeanRefreshScope.SCOPE_REFRESH,BeanRefreshScope.getInstance());
context.register(MainConfig4.class);
//重新整理mail的配置到Environment
RefreshConfigUtil.refreshMailPropertySource(context);
context.refresh();

MailServicemailService=context.getBean(MailService.class);
System.out.println("配置未更新的情況下,輸出3次");
for(inti=0;i<3;i++){//@1
System.out.println(mailService);
TimeUnit.MILLISECONDS.sleep(200);
}

System.out.println("模擬3次更新配置效果");
for(inti=0;i<3;i++){//@2
RefreshConfigUtil.updateDbConfig(context);//@3
System.out.println(mailService);
TimeUnit.MILLISECONDS.sleep(200);
}
}

@1:迴圈3次,輸出mailService的資訊

@2:迴圈3次,內部先通過@3來模擬更新db中配置資訊,然後在輸出mailService資訊

見證奇蹟的時刻,來看效果

配置未更新的情況下,輸出3
MailService{mailConfig=MailConfig{username='df321543-8ca7-4563-993a-bd64cbf50d53'}}
MailService{mailConfig=MailConfig{username='df321543-8ca7-4563-993a-bd64cbf50d53'}}
MailService{mailConfig=MailConfig{username='df321543-8ca7-4563-993a-bd64cbf50d53'}}
模擬3次更新配置效果
MailService{mailConfig=MailConfig{username='6bab8cea-9f4f-497d-a23a-92f15d0d6e34'}}
MailService{mailConfig=MailConfig{username='581bf395-f6b8-4b87-84e6-83d3c7342ca2'}}
MailService{mailConfig=MailConfig{username='db337f54-20b0-4726-9e55-328530af6999'}}

上面MailService輸出了6次,前3次username的值都是一樣的,後面3次username的值不一樣了,說明修改配置起效了。

小結

動態@Value實現的關鍵是@Scope中proxyMode引數,值為ScopedProxyMode.DEFAULT,會生成一個代理,通過這個代理來實現@Value動態重新整理的效果,這個地方是關鍵。

有興趣的可以去看一下springboot中的@RefreshScope註解原始碼,和我們上面自定義的@RefreshScope類似,實現原理類似的。

案例原始碼

https://gitee.com/javacode2018/spring-series

路人甲java所有案例程式碼以後都會放到這個上面,大家watch一下,可以持續關注動態。

參考:https://mp.weixin.qq.com/s?__biz=MzA5MTkxMDQ4MQ==&mid=2648934401&idx=1&sn=98e726ec9adda6d40663f624705ba2e4&chksm=8862103fbf15992981183abef03b4774ab1dfd990a203a183efb8d118455ee4b477dc6cba50d&token=636643900&lang=zh_CN&scene=21#wechat_redirect