@RefreshScope 自動刷新原理(三)

前言

文本已收錄至個人GitHub倉庫,歡迎Star:github.com/bin39232820…
種一棵樹最好的時間是十年前,其次是如今
我知道不少人不玩qq了,可是懷舊一下,歡迎加入六脈神劍Java菜鳥學習羣,羣聊號碼:549684836 鼓勵你們在技術的路上寫博客git

絮叨

上篇文章和你們分析了 Nacos 的配置中心原理,分析了客戶端的原理 還有服務端的原理,那麼接下來就是咱們要配合這個@RefreshScope這個註解來完成咱們的自動配置github

BeanScope

在SpringIOC中,咱們熟知的BeanScope有單例(singleton)、原型(prototype), Bean的Scope影響了Bean的管理方式,例如建立Scope=singleton的Bean時,IOC會保存實例在一個Map中,保證這個Bean在一個IOC上下文有且僅有一個實例。SpringCloud新增了一個refresh範圍的scope,一樣用了一種獨特的方式改變了Bean的管理方式,使得其能夠經過外部化配置(.properties)的刷新,在應用不須要重啓的狀況下熱加載新的外部化配置的值。web

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

單獨管理Bean生命週期 建立Bean的時候若是是RefreshScope就緩存在一個專門管理的ScopeMap中,這樣就能夠管理Scope是Refresh的Bean的生命週期了 從新建立Bean 外部化配置刷新以後,會觸發一個動做,這個動做將上面的ScopeMap中的Bean清空,這樣,這些Bean就會從新被IOC容器建立一次,使用最新的外部化配置的值注入類中,達到熱加載新值的效果 下面咱們深刻源碼,來驗證咱們上述的講法。bash

管理RefreshBean的生命週期

首先,若想要一個Bean能夠自動熱加載配置值,這個Bean要被打上@RefreshScope註解,那麼就看看這個註解作了什麼:app

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Scope("refresh")
@Documented
public @interface RefreshScope {

	/**
	 * @see Scope#proxyMode()
	 * @return proxy mode
	 */
	ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;

}
複製代碼

能夠發現RefreshScope有一個屬性 proxyMode=ScopedProxyMode.TARGET_CLASS,這個是AOP動態代理用,以後會再來提這個分佈式

能夠看出其是一個複合註解,被標註了 @Scope("refresh") ,其將Bean的Scope變爲refresh這個類型,在SpringBoot中BootStrap類上打上@SpringBootApplication註解(裏面是一個@ComponentScan),就會掃描包中的註解驅動Bean,掃描到打上RefreshScope註解的Bean的時候,就會將其的BeanDefinition的scope變爲refresh,這有什麼用呢?post

建立一個Bean的時候,會去BeanFactory的doGetBean方法建立Bean,不一樣scope有不一樣的建立方式:學習

protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
                          @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {

  //....

  // Create bean instance.
  // 單例Bean的建立
  if (mbd.isSingleton()) {
    sharedInstance = getSingleton(beanName, () -> {
      try {
        return createBean(beanName, mbd, args);
      }
      //...
    });
    bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
  }

  // 原型Bean的建立
  else if (mbd.isPrototype()) {
    // It's a prototype -> create a new instance. // ... try { prototypeInstance = createBean(beanName, mbd, args); } //... bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd); } else { // 由上面的RefreshScope註解能夠知道,這裏scopeName=refresh String scopeName = mbd.getScope(); // 獲取Refresh的Scope對象 final Scope scope = this.scopes.get(scopeName); if (scope == null) { throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'"); } try { // 讓Scope對象去管理Bean Object scopedInstance = scope.get(beanName, () -> { beforePrototypeCreation(beanName); try { return createBean(beanName, mbd, args); } finally { afterPrototypeCreation(beanName); } }); bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd); } //... } } //... } //... } 複製代碼

這裏能夠看到幾件事情:ui

  • 單例和原型scope的Bean是硬編碼單獨處理的
  • 除了單例和原型Bean,其餘Scope是由Scope對象處理的
  • 具體建立Bean的過程都是由IOC作的,只不過Bean的獲取是經過Scope對象

這裏scope.get獲取的Scope對象爲RefreshScope,能夠看到,建立Bean仍是由IOC來作(createBean方法),可是獲取Bean,都由RefreshScope對象的get方法去獲取,其get方法在父類GenericScope中實現:

public Object get(String name, ObjectFactory<?> objectFactory) {
  // 將Bean緩存下來
  BeanLifecycleWrapper value = this.cache.put(name,
                                              new BeanLifecycleWrapper(name, objectFactory));
  this.locks.putIfAbsent(name, new ReentrantReadWriteLock());
  try {
    // 建立Bean,只會建立一次,後面直接返回建立好的Bean
    return value.getBean();
  }
  catch (RuntimeException e) {
    this.errors.put(name, e);
    throw e;
  }
}
複製代碼

首先這裏將Bean包裝起來緩存下來

這裏scope.get獲取的Scope對象爲RefreshScope,能夠看到,建立Bean仍是由IOC來作(createBean方法),可是獲取Bean,都由RefreshScope對象的get方法去獲取,其get方法在父類GenericScope中實現:

public Object get(String name, ObjectFactory<?> objectFactory) {
  // 將Bean緩存下來
  BeanLifecycleWrapper value = this.cache.put(name,
                                              new BeanLifecycleWrapper(name, objectFactory));
  this.locks.putIfAbsent(name, new ReentrantReadWriteLock());
  try {
    // 建立Bean,只會建立一次,後面直接返回建立好的Bean
    return value.getBean();
  }
  catch (RuntimeException e) {
    this.errors.put(name, e);
    throw e;
  }
}

複製代碼
private final ScopeCache cache;
// 這裏進入上面的 BeanLifecycleWrapper value = this.cache.put(name, new BeanLifecycleWrapper(name, objectFactory));
public BeanLifecycleWrapper put(String name, BeanLifecycleWrapper value) {
  return (BeanLifecycleWrapper) this.cache.put(name, value);
}

複製代碼

這裏的ScopeCache對象其實就是一個HashMap:

public class StandardScopeCache implements ScopeCache {

  private final ConcurrentMap<String, Object> cache = new ConcurrentHashMap<String, Object>();

  //...

  public Object get(String name) {
    return this.cache.get(name);
  }

  // 若是不存在,纔會put進去
  public Object put(String name, Object value) {
    // result若不等於null,表示緩存存在了,不會進行put操做
    Object result = this.cache.putIfAbsent(name, value);
    if (result != null) {
      // 直接返回舊對象
      return result;
    }
    // put成功,返回新對象
    return value;
  }
}

複製代碼

這裏就是將Bean包裝成一個對象,緩存在一個Map中,下次若是再GetBean,仍是那個舊的BeanWrapper。回到Scope的get方法,接下來就是調用BeanWrapper的getBean方法:

// 實際Bean對象,緩存下來了
private Object bean;

public Object getBean() {
  if (this.bean == null) {
    synchronized (this.name) {
      if (this.bean == null) {
        this.bean = this.objectFactory.getObject();
      }
    }
  }
  return this.bean;
}

複製代碼

能夠看出來,BeanWrapper中的bean變量即爲實際Bean,若是第一次get確定爲空,就會調用BeanFactory的createBean方法建立Bean,建立出來以後就會一直保存下來。

因而可知,RefreshScope管理了Scope=Refresh的Bean的生命週期。

從新建立RefreshBean

當配置中心刷新配置以後,有兩種方式能夠動態刷新Bean的配置變量值,(SpringCloud-Bus仍是Nacos差很少都是這麼實現的):

  • 向上下文發佈一個RefreshEvent事件
  • Http訪問/refresh這個EndPoint

不論是什麼方式,最終都會調用ContextRefresher這個類的refresh方法,那麼咱們由此爲入口來分析一下,熱加載配置的原理:

// 這就是咱們上面一直分析的Scope對象(實際上能夠看做一個保存refreshBean的Map)
private RefreshScope scope;

public synchronized Set<String> refresh() {
  // 更新上下文中Environment外部化配置值
  Set<String> keys = refreshEnvironment();
  // 調用scope對象的refreshAll方法
  this.scope.refreshAll();
  return keys;
}
複製代碼

咱們通常是使用@Value、@ConfigurationProperties去獲取配置變量值,其底層在IOC中則是經過上下文的Environment對象去獲取property值,而後依賴注入利用反射Set到Bean對象中去的。

那麼若是咱們更新Environment裏的Property值,而後從新建立一次RefreshBean,再進行一次上述的依賴注入,是否是就能完成配置熱加載了呢?@Value的變量值就能夠加載爲最新的了。

這裏說的刷新Environment對象並從新依賴注入則爲上述兩個方法作的事情:

  • Set keys = refreshEnvironment();
  • this.scope.refreshAll();

刷新Environment對象

下面簡單介紹一下如何刷新Environment裏的Property值

public synchronized Set<String> refreshEnvironment() {
  // 獲取刷新配置前的配置信息,對比用
  Map<String, Object> before = extract(
    this.context.getEnvironment().getPropertySources());
  // 刷新Environment
  addConfigFilesToEnvironment();
  // 這裏上下文的Environment已是新的值了
  // 進行新舊對比,結果返回有變化的值
  Set<String> keys = changes(before,
                          extract(this.context.getEnvironment().getPropertySources())).keySet();
  this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
  return keys;
}

複製代碼

咱們的重點在addConfigFilesToEnvironment方法,刷新Environment:

ConfigurableApplicationContext addConfigFilesToEnvironment() {
  ConfigurableApplicationContext capture = null;
  try {
    // 從上下文拿出Environment對象,copy一份
    StandardEnvironment environment = copyEnvironment(
      this.context.getEnvironment());
    // SpringBoot啓動類builder,準備新作一個Spring上下文啓動
    SpringApplicationBuilder builder = new SpringApplicationBuilder(Empty.class)
      // banner和web都關閉,由於只是想單純利用新的Spring上下文構造一個新的Environment
      .bannerMode(Mode.OFF).web(WebApplicationType.NONE)
      // 傳入咱們剛剛copy的Environment實例
      .environment(environment);
    // 啓動上下文
    capture = builder.run();
    // 這個時候,經過上下文SpringIOC的啓動,剛剛Environment對象就變成帶有最新配置值的Environment了
    // 獲取舊的外部化配置列表
    MutablePropertySources target = this.context.getEnvironment()
      .getPropertySources();
    String targetName = null;
    // 遍歷這個最新的Environment外部化配置列表
    for (PropertySource<?> source : environment.getPropertySources()) {
      String name = source.getName();
      if (target.contains(name)) {
        targetName = name;
      }
      // 某些配置源不作替換,讀者自行查看源碼
      // 通常的配置源都會進入if語句
      if (!this.standardSources.contains(name)) {
        if (target.contains(name)) {
          // 用新的配置替換舊的配置
          target.replace(name, source);
        }
        else {
          //....
        }
      }
    }
  }
  //....
}

複製代碼

能夠看到,這裏歸根結底就是SpringBoot啓動上下文那種方法,新作了一個Spring上下文,由於Spring啓動後會對上下文中的Environment進行初始化,獲取最新配置,因此這裏利用Spring的啓動,達到了獲取最新的Environment對象的目的。而後去替換舊的上下文中的Environment對象中的配置值便可。

從新建立RefreshBean

通過上述刷新Environment對象的動做,此時上下文中的配置值已是最新的了。思路回到ContextRefresher的refresh方法,接下來會調用Scope對象的refreshAll方法:

public void refreshAll() {
  // 銷燬Bean
  super.destroy();
  this.context.publishEvent(new RefreshScopeRefreshedEvent());
}

public void destroy() {
  List<Throwable> errors = new ArrayList<Throwable>();
  // 緩存清空
  Collection<BeanLifecycleWrapper> wrappers = this.cache.clear();
  // ...
}

複製代碼

還記得上面的管理RefreshBean生命週期那一節關於緩存的討論嗎,cache變量是一個Map保存着RefreshBean實例,這裏直接就將Map清空了。

思路回到BeanFactory的doGetBean的流程中,從IOC容器中獲取RefreshBean是交給RefreshScope的get方法作的:

public Object get(String name, ObjectFactory<?> objectFactory) {
  // 因爲剛剛清空了緩存Map,這裏就會put一個新的BeanLifecycleWrapper實例
  BeanLifecycleWrapper value = this.cache.put(name,
                                              new BeanLifecycleWrapper(name, objectFactory));
  this.locks.putIfAbsent(name, new ReentrantReadWriteLock());
  try {
    // 在這裏是新的BeanLifecycleWrapper實例調用getBean方法
    return value.getBean();
  }
  catch (RuntimeException e) {
    this.errors.put(name, e);
    throw e;
  }
}

複製代碼
public Object getBean() {
  // 因爲是新的BeanLifecycleWrapper實例,這裏必定爲null
  if (this.bean == null) {
    synchronized (this.name) {
      if (this.bean == null) {
        // 調用IOC容器的createBean,再建立一個Bean出來
        this.bean = this.objectFactory.getObject();
      }
    }
  }
  return this.bean;
}

複製代碼

能夠看到,此時RefreshBean被IOC容器從新建立一個出來了,通過IOC的依賴注入功能,@Value的就是一個新的配置值了。到這裏熱加載功能實現基本結束。

根據以上分析,咱們能夠看出只要每次咱們都從IOC容器中getBean,那麼拿到的RefreshBean必定是帶有最新配置值的Bean。

結尾

這塊目前爲止,咱們就瞭解完成了,小六六其實也是學習了個大概,不少一點一點的細節並非那麼清晰,爲了之後繼續學習作準備吧

平常求贊

好了各位,以上就是這篇文章的所有內容了,能看到這裏的人呀,都是真粉

創做不易,各位的支持和承認,就是我創做的最大動力,咱們下篇文章見

六脈神劍 | 文 【原創】若是本篇博客有任何錯誤,請批評指教,不勝感激 !

相關文章
相關標籤/搜索