close

點擊關注公眾號,實用技術文章及時了解


一、@RefreshScope動態刷新原理

在SpringIOC中,BeanScope(Bean的作用域)影響了Bean的管理方式。

Bean的作用域:

例如創建Scope=singleton的Bean時,IOC會保存實例在一個Map中,保證這個Bean在一個IOC上下文有且僅有一個實例。

SpringCloud新增了一個自定義的作用域:refresh(可以理解為「動態刷新」),同樣用了一種獨特的方式改變了Bean的管理方式,使得其可以通過外部化配置(.properties)的刷新,在應用不需要重啟的情況下熱加載新的外部化配置的值。

這個scope是如何做到熱加載的呢?RefreshScope主要做了以下動作:

單獨管理Bean生命周期

創建Bean的時候如果是RefreshScope就緩存在一個專門管理的ScopeMap中,這樣就可以管理Scope是Refresh的Bean的生命周期了(所以含RefreshScope的其實一共創建了兩個bean)。

重新創建Bean

外部化配置刷新之後,會觸發一個動作,這個動作將上面的ScopeMap中的Bean清空,這樣這些Bean就會重新被IOC容器創建一次,使用最新的外部化配置的值注入類中,達到熱加載新值的效果。

spring cloud config或sprring cloud alibaba nacos作為配置中心,其實現原理就是通過@RefreshScope 來實現對象屬性的的動態更新。

@RefreshScope 實現配置的動態刷新需要滿足一下幾點條件:

@Scope註解
@RefreshScope註解
RefreshScope類
GenericScope類
Scope接口
ContextRefresher類

@RefreshScope 能實現動態刷新全仰仗着@Scope 這個註解。

1. @Scope註解@Target({ElementType.TYPE,ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic@interfaceScope{/***Aliasfor{@link#scopeName}.*@see#scopeName*/@AliasFor("scopeName")Stringvalue()default"";/***singleton表示該bean是單例的。(默認)*prototype表示該bean是多例的,即每次使用該bean時都會新建一個對象。*request在一次http請求中,一個bean對應一個實例。*session在一個httpSession中,一個bean對應一個實例*/@AliasFor("value")StringscopeName()default"";/***DEFAULT不使用代理。(默認)*NO不使用代理,等價於DEFAULT。*INTERFACES使用基於接口的代理(jdkdynamicproxy)。*TARGET_CLASS使用基於類的代理(cglib)。*/ScopedProxyModeproxyMode()defaultScopedProxyMode.DEFAULT;}

@Scope有兩個主要屬性value 和 proxyMode,其中proxyMode就是@RefreshScope 實現的本質了。

proxyMode屬性是一個ScopedProxyMode類型的枚舉對象。

publicenumScopedProxyMode{DEFAULT,NO,INTERFACES,//JDK動態代理TARGET_CLASS;//CGLIB動態代理privateScopedProxyMode(){}}

當proxyMode屬性的值為ScopedProxyMode.TARGET_CLASS時,會給當前創建的bean 生成一個代理對象,會通過代理對象來訪問,每次訪問都會創建一個新的對象。

2. @RefreshScope註解@Target({ElementType.TYPE,ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Scope("refresh")@Documentedpublic@interfaceRefreshScope{/***@seeScope#proxyMode()*/ScopedProxyModeproxyMode()defaultScopedProxyMode.TARGET_CLASS;}

它使用就是 @Scope ,一個scopeName="refresh"的@Scope。

proxyMode值為ScopedProxyMode.TARGET_CLASS,通過CGLIB動態代理的方式生成Bean。

使用 @RefreshScope 註解的 bean,不僅會生成一個beanName的bean,默認情況下同時會生成 scopedTarget.beanName的 bean。

@RefreshScope不能單獨使用,需要和其他其他bean註解結合使用,如:@Controller、@Service、@Component、@Repository等。

3. Scope接口publicinterfaceScope{/***Returntheobjectwiththegivennamefromtheunderlyingscope,*{@linkorg.springframework.beans.factory.ObjectFactory#getObject()creatingit}*ifnotfoundintheunderlyingstoragemechanism.*<p>ThisisthecentraloperationofaScope,andtheonlyoperation*thatisabsolutelyrequired.*@paramnamethenameoftheobjecttoretrieve*@paramobjectFactorythe{@linkObjectFactory}tousetocreatethescoped*objectifitisnotpresentintheunderlyingstoragemechanism*@returnthedesiredobject(never{@codenull})*@throwsIllegalStateExceptioniftheunderlyingscopeisnotcurrentlyactive*/Objectget(Stringname,ObjectFactory<?>objectFactory);@NullableObjectremove(Stringname);voidregisterDestructionCallback(Stringname,Runnablecallback);@NullableObjectresolveContextualObject(Stringkey);@NullableStringgetConversationId();}Objectget(Stringname,ObjectFactory<?>objectFactory)

這個方法幫助我們來創建一個新的bean ,也就是說,@RefreshScope 在調用刷新的時候會使用此方法來給我們創建新的對象,這樣就可以通過spring 的裝配機制將屬性重新注入了,也就實現了所謂的動態刷新。

RefreshScopeextendsGenericScope,GenericScopeimplementsScope`

GenericScope 實現了 Scope 最重要的 get(String name, ObjectFactory<?> objectFactory) 方法,在GenericScope 裡面 包裝了一個內部類 BeanLifecycleWrapperCache 來對加了 @RefreshScope 從而創建的對象進行緩存,使其在不刷新時獲取的都是同一個對象。(這裡你可以把 BeanLifecycleWrapperCache 想象成為一個大Map 緩存了所有@RefreshScope 標註的對象)

知道了對象是緩存的,所以在進行動態刷新的時候,只需要清除緩存,重新創建就好了。

//ContextRefresher外面使用它來進行方法調用==============================我是分割線publicsynchronizedSet<String>refresh(){Set<String>keys=refreshEnvironment();this.scope.refreshAll();returnkeys;}//RefreshScope內部代碼==============================我是分割線@ManagedOperation(description="Disposeofthecurrentinstanceofallbeansinthisscopeandforcearefreshonnextmethodexecution.")publicvoidrefreshAll(){super.destroy();this.context.publishEvent(newRefreshScopeRefreshedEvent());}//GenericScope里的方法==============================我是分割線//進行對象獲取,如果沒有就創建並放入緩存@OverridepublicObjectget(Stringname,ObjectFactory<?>objectFactory){BeanLifecycleWrappervalue=this.cache.put(name,newBeanLifecycleWrapper(name,objectFactory));locks.putIfAbsent(name,newReentrantReadWriteLock());try{returnvalue.getBean();}catch(RuntimeExceptione){this.errors.put(name,e);throwe;}}//初始化BeanpublicObjectgetBean(){if(this.bean==null){Stringvar1=this.name;synchronized(this.name){if(this.bean==null){this.bean=this.objectFactory.getObject();}}}returnthis.bean;}//進行緩存的數據清理@Overridepublicvoiddestroy(){List<Throwable>errors=newArrayList<Throwable>();Collection<BeanLifecycleWrapper>wrappers=this.cache.clear();for(BeanLifecycleWrapperwrapper:wrappers){try{Locklock=locks.get(wrapper.getName()).writeLock();lock.lock();try{wrapper.destroy();}finally{lock.unlock();}}catch(RuntimeExceptione){errors.add(e);}}if(!errors.isEmpty()){throwwrapIfNecessary(errors.get(0));}this.errors.clear();}

通過觀看源代碼我們得知,我們截取了三個片段所得之,ContextRefresher 就是外層調用方法用的。

GenericScope類中有一個成員變量BeanLifecycleWrapperCache,用於緩存所有已經生成的Bean,在調用get方法時嘗試從緩存加載,如果沒有的話就生成一個新對象放入緩存,並通過初始化getBean其對應的Bean。

destroy 方法負責再刷新時緩存的清理工作。清空緩存後,下次訪問對象時就會重新創建新的對象並放入緩存了。

所以在重新創建新的對象時,也就獲取了最新的配置,也就達到了配置刷新的目的。

4. @RefreshScope 實現流程

需要動態刷新的類標註@RefreshScope 註解。

@RefreshScope 註解標註了@Scope 註解,並默認了ScopedProxyMode.TARGET_CLASS; 屬性,此屬性的功能就是再創建一個代理,在每次調用的時候都用它來調用GenericScope get 方法來獲取對象。

如屬性發生變更

調用 ContextRefresher refresh() -->> RefreshScope refreshAll() 進行緩存清理方法調用;
發送刷新事件通知,GenericScope 真正的清理方法destroy() 實現清理緩存。

在下一次使用對象的時候,會調用GenericScope get(String name, ObjectFactory<?> objectFactory) 方法創建一個新的對象,並存入緩存中,此時新對象因為Spring 的裝配機制就是新的屬性了。

5. @RefreshScope原理總結

1.SpringCloud程序的存在一個自動裝配的類,這個類默認情況下會自動初始化一個RefreshScope實例,該實例是GenericScope的子類,然後註冊到容器中。(RefreshAutoConfiguration.java,)

2.當容器啟動的時候,GenericScope會自己把自己註冊到scope中(ConfigurableBeanFactory#registerScope)(GenericScope)

3.然後當自定義的Bean(被@RefreshScope修飾)註冊的時候,會被容器讀取到其作用域為refresh。(AnnotatedBeanDefinitionReader#doRegisterBean)

通過上面三步,一個帶有@RefreshScope的自定義Bean就被註冊到容器中來,其作用域為refresh。

4.當我們後續進行以來查找的時候,會繞過Singleton和Prototype分支,進入最後一個分支,通過調用Scope接口的get()獲取到該refresh作用域的實例。(AbstractBeanFactory.doGetBean)

二、@RefreshScope注意事項1. @RefreshScope使用注意事項
@RefreshScope作用的類,不能是final類,否則啟動時會報錯。
@RefreshScope不能單獨使用,需要和其他其他bean註解結合使用,如:@Controller、@Service、@Component、@Repository、@Configuration等。
@RefreshScope 最好不要修飾在 @Scheduled、listener、Timmer等類中,因為配置的刷新會導致原來的對象被清除,需要重新使用對象才能出發生成新對象(但因為對象沒了,又沒法重新使用對象,死循環)
2. @RefreshScope動態刷新失效

考慮使用的bean是否是@RefreshScope生成的那個scopedTarget.beanName的 bean

springboot某些低版本貌似有問題,在Controller類上使用不會生效(網上有這麼說的,沒具體研究)

解決方法1:註解上加屬性@RefreshScope(proxyMode = ScopedProxyMode.DEFAULT)
解決方法2:直接使用其他類單獨封裝配置參數,使用@RefreshScope+@Value方式
解決方法3:直接使用@ConfigurationProperties
3. 不使用@RefreshScope也能實現動態刷新

直接使用@ConfigurationProperties,並不需要加@RefreshScope就能實現動態更新。

@ConfigurationProperties實現動態刷新的原理:

@ConfigurationProperties有ConfigurationPropertiesRebinder這個監聽器,監聽着EnvironmentChangeEvent事件。當發生EnvironmentChange事件後,會重新構造原來的加了@ConfigurationProperties註解的Bean對象。這個是Spring Cloud的默認實現。

4. 靜態變量利用@RefreshScope動態刷新的坑(求大佬解答)@RefreshScope@ComponentpublicclassTestConfig{publicstaticinturl;@Value("${pesticide.url}")publicvoidsetUrl(inturl){TestConfig.url=url;}publicvoidgetUrl(){}}@RestController@RequestMapping("test")publicclassTestController{@AutowiredprivateTestConfigtestConfig;@GetMapping("testConfig")publicinttestConfig(){System.out.println("TestConfig:"+TestConfig.url);testConfig.getUrl();System.out.println("TestConfig:"+TestConfig.url);returnTestConfig.url;}}1.url初始配置的值為1

請求接口日誌:

TestConfig:1TestConfig:12.修改url配置的值為2,動態刷新成功

請求接口日誌:

TestConfig:1TestConfig:2

這裡就出現了問題,不調用@RefreshScope生產的代理對象testConfig的方法前(注意,該方法內無代碼),取到的值還是為1;調了之後,取到的值為2.後續再次請求接口,取到的值都為2。

TestConfig:2TestConfig:2TestConfig:2TestConfig:2

個人大膽猜想原因:參考上面@RefreshScope 實現流程可知,在第2步驟動態刷新成功時,此時僅僅是再創建類一個代理對象,並清除了實際對象的緩存;當再次通過代理對象來使用,才會觸發創建一個新的實例對象,此時才會更新url的值。所以使用靜態變量來是實現動態刷新時,一點要注意:使用對象才能出發創建新的實際對象,更新靜態變量的值。

Spring Cloud的參考文檔指出:

@RefreshScope在@Configuration類上工作,但可能導致令人驚訝的行為:例如,這並不意味着該類中定義的所有@Beans本身都是@RefreshScope。具體來說,依賴於這些bean的任何東西都不能依賴於刷新啟動時對其進行更新,除非它本身在@RefreshScope中從刷新的@Configuration重新初始化(在刷新中將其重建並重新注入其依賴項,此時它們將被刷新)。

三、使用@RefreshScope的bean問題

這裡之所以要會討論使用@RefreshScope的bean問題,由上面上面所講可以總結得到:

使用 @RefreshScope 註解的 bean,不僅會生成一個名為beanName的bean,默認情況下同時會生成名為scopedTarget.beanName的bean
使用 @RefreshScope 註解的會生成一個代理對象,通過這個代理對象來調用名為scopedTarget.beanName的 bean
刷新操作會導致原來的名為scopedTarget.beanName的bean被清除,再次使用會新生成新的名為scopedTarget.beanName的bean,但原來的代理對象不會變動

下面舉例說明:

nacos配置

test:value:1

配置類獲取配置值

@Data@Component@RefreshScopepublicclassTestConfig{@Value("${test.value}")privateStringvalue;}

測試接口

@RestControllerpublicclassTestController{@AutowiredprivateTestConfigtestConfig;@RequestMapping("test11")publicvoidtest11(){//代理對象System.out.println("@Autowiredbean=========="+testConfig.getClass().getName());//代理對象TestConfigbean=SpringUtils.getBean(TestConfig.class);System.out.println("Classbean=========="+bean.getClass().getName());//代理對象Objectbean1=SpringUtils.getBean("testConfig");System.out.println("name(testConfig)bean=========="+bean1.getClass().getName());//原類對象Objectbean2=SpringUtils.getBean("scopedTarget.testConfig");System.out.println("name(scopedTarget.testConfig)bean=========="+bean2.getClass().getName());System.out.println("================================================================================");}}

測試

@Autowired注入的是代理對象

通過Class得到的是代理對象
通過名為beanName的得到的是代理對象
通過名為scopedTarget.beanName的得到的是由@RefreshScope生成的那個原類對象

修改配置的值,測試

test:value:2

動態刷新後,代理對象沒有變化,由@RefreshScope生成的那個原類對象被清除後重新生成了一個新的原類對象

小結:
@Autowired方式注入的是代理對象
beanName的得到的是代理對象
scopedTarget.beanName的得到的@RefreshScope生成的那個原類對象
代理對象不會隨着配置刷新而更新
@RefreshScope生成的那個原類對象會隨着配置的刷新而更新(屬性時清除原來的,使用時才生成新的)
四、其它配置刷新方式

這種方法必須有 spring-boot-starter-actuator 這個starter才行。

POST http://localhost:7031/refresh

refresh的底層原理詳見:org.springframework.cloud.context.refresh.ContextRefresher#refresh

SpringCloud2.0以後,沒有/refresh手動調用的刷新配置地址。

SpringCloud2.0前

加入依賴

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency>

在類上,變量上打上@RefreshScope的註解

在啟動的時候,都會看到

RequestMappingHandlerMapping:Mapped"{/refresh,methods=[post]}"

也就是SpringCloud暴露了一個接口 /refresh 來給我們去刷新配置,但是SpringCloud 2.0.0以後,有了改變。

SpringCloud 2.0後

我們需要在bootstrap.yml裡面加上需要暴露出來的地址

management:endpoints:web:exposure:include:refresh,health

現在的地址也不是/refresh了,而是/actuator/refresh

感謝閱讀,希望對你有所幫助:)

來源:blog.csdn.net/JokerLJG/article/details/120254643

推薦

Java面試題寶典

技術內卷群,一起來學習!!

PS:因為公眾號平台更改了推送規則,如果不想錯過內容,記得讀完點一下「在看」,加個「星標」,這樣每次新文章推送才會第一時間出現在你的訂閱列表里。點「在看」支持我們吧!

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()