點擊關注公眾號,實用技術文章及時了解
在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 實現配置的動態刷新需要滿足一下幾點條件:
@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 方法來獲取對象。
如屬性發生變更
在下一次使用對象的時候,會調用GenericScope get(String name, ObjectFactory<?> objectFactory) 方法創建一個新的對象,並存入緩存中,此時新對象因為Spring 的裝配機制就是新的屬性了。
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使用注意事項考慮使用的bean是否是@RefreshScope生成的那個scopedTarget.beanName的 bean
springboot某些低版本貌似有問題,在Controller類上使用不會生效(網上有這麼說的,沒具體研究)
直接使用@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問題,由上面上面所講可以總結得到:
下面舉例說明:
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注入的是代理對象
修改配置的值,測試
test:value:2
動態刷新後,代理對象沒有變化,由@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:因為公眾號平台更改了推送規則,如果不想錯過內容,記得讀完點一下「在看」,加個「星標」,這樣每次新文章推送才會第一時間出現在你的訂閱列表里。點「在看」支持我們吧!