Spring Cloud 使用 @RefreshScope 註解配置動態刷新

java小悠 發佈 2024-02-29T23:42:33.579073+00:00

一、@RefreshScope動態刷新原理在SpringIOC中,BeanScope(Bean的作用域)影響了Bean的管理方式。

一、@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 configsprring 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.當我們後續進行以來查找的時候,會繞過SingletonPrototype分支,進入最後一個分支,通過調用Scope接口的get()獲取到該refresh作用域的實例。(AbstractBeanFactory.doGetBean

二、@RefreshScope注意事項

1. @RefreshScope使用注意事項

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

2. @RefreshScope動態刷新失效

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

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

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

3. 不使用@RefreshScope也能實現動態刷新

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

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

@ConfigurationPropertiesConfigurationPropertiesRebinder這個監聽器,監聽著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

關鍵字: