一、@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)
@Documented
public @interface Scope {
/**
* Alias for {@link #scopeName}.
* @see #scopeName
*/
@AliasFor("scopeName")
String value() default "";
/**
* singleton 表示該bean是單例的。(默認)
* prototype 表示該bean是多例的,即每次使用該bean時都會新建一個對象。
* Request 在一次http請求中,一個bean對應一個實例。
* session 在一個httpSession中,一個bean對應一個實例
*/
@AliasFor("value")
String scopeName() default "";
/**
* DEFAULT 不使用代理。(默認)
* NO 不使用代理,等價於DEFAULT。
* INTERFACES 使用基於接口的代理(JDK dynamic proxy)。
* TARGET_CLASS 使用基於類的代理(CGLIB)。
*/
ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;
}
@Scope有兩個主要屬性value 和 proxyMode,其中proxyMode就是@RefreshScope 實現的本質了。
proxyMode屬性是一個ScopedProxyMode類型的枚舉對象。
public enum ScopedProxyMode {
DEFAULT,
NO,
INTERFACES,// JDK 動態代理
TARGET_CLASS;// CGLIB 動態代理
private ScopedProxyMode() {
}
}
當proxyMode屬性的值為ScopedProxyMode.TARGET_CLASS時,會給當前創建的bean 生成一個代理對象,會通過代理對象來訪問,每次訪問都會創建一個新的對象。
2. @RefreshScope註解
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Scope("refresh")
@Documented
public @interface RefreshScope {
/**
* @see Scope#proxyMode()
*/
ScopedProxyMode proxyMode() default ScopedProxyMode.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接口
public interface Scope {
/**
* Return the object with the given name from the underlying scope,
* {@link org.springframework.beans.factory.ObjectFactory#getObject() creating it}
* if not found in the underlying storage mechanism.
* <p>This is the central operation of a Scope, and the only operation
* that is absolutely required.
* @param name the name of the object to retrieve
* @param objectFactory the {@link ObjectFactory} to use to create the scoped
* object if it is not present in the underlying storage mechanism
* @return the desired object (never {@code null})
* @throws IllegalStateException if the underlying scope is not currently active
*/
Object get(String name, ObjectFactory<?> objectFactory);
@Nullable
Object remove(String name);
void registerDestructioncallback(String name, Runnable callback);
@Nullable
Object resolveContextualObject(String key);
@Nullable
String getConversationId();
}
Object get(String name, ObjectFactory<?> objectFactory)
這個方法幫助我們來創建一個新的bean ,也就是說,@RefreshScope 在調用刷新的時候會使用此方法來給我們創建新的對象,這樣就可以通過spring 的裝配機制將屬性重新注入了,也就實現了所謂的動態刷新。
RefreshScope extends GenericScope, GenericScope implements Scope`
GenericScope 實現了 Scope 最重要的 get(String name, ObjectFactory<?> objectFactory) 方法,在GenericScope 裡面 包裝了一個內部類 BeanLifecycleWrapperCache 來對加了 @RefreshScope 從而創建的對象進行緩存,使其在不刷新時獲取的都是同一個對象。(這裡你可以把 BeanLifecycleWrapperCache 想像成為一個大Map 緩存了所有@RefreshScope 標註的對象)
知道了對象是緩存的,所以在進行動態刷新的時候,只需要清除緩存,重新創建就好了。
// ContextRefresher 外面使用它來進行方法調用 ============================== 我是分割線
public synchronized Set<String> refresh() {
Set<String> keys = refreshEnvironment();
this.scope.refreshAll();
return keys;
}
// RefreshScope 內部代碼 ============================== 我是分割線
@ManagedOperation(description = "Dispose of the current instance of all beans in this scope and force a refresh on next method execution.")
public void refreshAll() {
super.destroy();
this.context.publishEvent(new RefreshScopeRefreshedEvent());
}
// GenericScope 里的方法 ============================== 我是分割線
//進行對象獲取,如果沒有就創建並放入緩存
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
BeanLifecycleWrapper value = this.cache.put(name,
new BeanLifecycleWrapper(name, objectFactory));
locks.putIfAbsent(name, new ReentrantReadWritelock());
try {
return value.getBean();
}
catch (RuntimeException e) {
this.errors.put(name, e);
throw e;
}
}
// 初始化Bean
public Object getBean() {
if (this.bean == null) {
String var1 = this.name;
synchronized(this.name) {
if (this.bean == null) {
this.bean = this.objectFactory.getObject();
}
}
}
return this.bean;
}
//進行緩存的數據清理
@Override
public void destroy() {
List<Throwable> errors = new ArrayList<Throwable>();
Collection<BeanLifecycleWrapper> wrappers = this.cache.clear();
for (BeanLifecycleWrapper wrapper : wrappers) {
try {
Lock lock = locks.get(wrapper.getName()).writeLock();
lock.lock();
try {
wrapper.destroy();
}
finally {
lock.unlock();
}
}
catch (RuntimeException e) {
errors.add(e);
}
}
if (!errors.isEmpty()) {
throw wrapIfNecessary(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
@Component
public class TestConfig {
public static int url;
@Value("${pesticide.url}")
public void setUrl(int url) {
TestConfig.url = url;
}
public void getUrl() {
}
}
@RestController
@RequestMapping("test")
public class TestController {
@Autowired
private TestConfig testConfig;
@GetMapping("testConfig")
public int testConfig(){
System.out.println("TestConfig:"+ TestConfig.url);
testConfig.getUrl();
System.out.println("TestConfig:"+ TestConfig.url);
return TestConfig.url;
}
}
1.url初始配置的值為1
請求接口日誌:
TestConfig:1
TestConfig:1
2.修改url配置的值為2,動態刷新成功
請求接口日誌:
TestConfig:1
TestConfig:2
這裡就出現了問題,不調用@RefreshScope生產的代理對象testConfig的方法前(注意,該方法內無代碼),取到的值還是為1;調了之後,取到的值為2.後續再次請求接口,取到的值都為2。
TestConfig:2
TestConfig:2
TestConfig:2
TestConfig: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
@RefreshScope
public class TestConfig {
@Value("${test.value}")
private String value;
}
測試接口
@RestController
public class TestController {
@Autowired
private TestConfig testConfig;
@RequestMapping("test11")
public void test11() {
// 代理對象
System.out.println("@Autowired bean==========" + testConfig.getClass().getName());
// 代理對象
TestConfig bean = SpringUtils.getBean(TestConfig.class);
System.out.println("Class bean==========" + bean.getClass().getName());
// 代理對象
Object bean1 = SpringUtils.getBean("testConfig");
System.out.println("name(testConfig) bean==========" + bean1.getClass().getName());
// 原類對象
Object bean2 = 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