【轉(zhuǎn)】Spring Cloud 是如何實(shí)現(xiàn)熱更新的

轉(zhuǎn)自:http://www.scienjus.com/spring-cloud-refresh/

作為一篇源碼分析的文章之景,本文雖然介紹 Spring Cloud 的熱更新機(jī)制,但是實(shí)際全文內(nèi)容都不會(huì)與 Spring Cloud Config 以及 Spring Cloud Bus 有關(guān)脯丝,因?yàn)榍罢咧皇翘峁┝艘粋€(gè)遠(yuǎn)端的配置源壶唤,而后者也只是提供了集群環(huán)境下的事件觸發(fā)機(jī)制绞吁,與核心流程均無(wú)太大關(guān)系朦前。

ContextRefresher

顧名思義厘唾,ContextRefresher 用于刷新 Spring 上下文褥符,在以下場(chǎng)景會(huì)調(diào)用其 refresh 方法。

  1. 請(qǐng)求 /refresh Endpoint抚垃。
  2. 集成 Spring Cloud Bus 后喷楣,收到 RefreshRemoteApplicationEvent 事件(任意集成 Bus 的應(yīng)用,請(qǐng)求 /bus/refresh Endpoint 后都會(huì)將事件推送到整個(gè)集群)鹤树。

這個(gè)方法包含了整個(gè)刷新邏輯铣焊,也是本文分析的重點(diǎn)。

首先看一下這個(gè)方法的實(shí)現(xiàn):

public synchronized Set<String> refresh() {
  Map<String, Object> before = extract(
      this.context.getEnvironment().getPropertySources());
  addConfigFilesToEnvironment();
  Set<String> keys = changes(before,
      extract(this.context.getEnvironment().getPropertySources())).keySet();
  this.context.publishEvent(new EnvironmentChangeEvent(keys));
  this.scope.refreshAll();
  return keys;
}

首先是第一步 extract罕伯,這個(gè)方法接收了當(dāng)前環(huán)境中的所有屬性源(PropertySource)曲伊,并將其中的非標(biāo)準(zhǔn)屬性源的所有屬性匯總到一個(gè) Map 中返回。

這里的標(biāo)準(zhǔn)屬性源指的是 StandardEnvironment 和 StandardServletEnvironment追他,前者會(huì)注冊(cè)系統(tǒng)變量(System Properties)和環(huán)境變量(System Environment)坟募,后者會(huì)注冊(cè) Servlet 環(huán)境下的 Servlet Context 和 Servlet Config 的初始參數(shù)(Init Params)和 JNDI 的屬性。個(gè)人理解是因?yàn)檫@些屬性無(wú)法改變湿酸,所以不進(jìn)行刷新婿屹。

第二步 addConfigFilesToEnvironment 是核心邏輯,它創(chuàng)建了一個(gè)新的 Spring Boot 應(yīng)用并初始化:

SpringApplicationBuilder builder = new SpringApplicationBuilder(Empty.class)
    .bannerMode(Banner.Mode.OFF).web(false).environment(environment);
// Just the listeners that affect the environment (e.g. excluding logging
// listener because it has side effects)
builder.application()
    .setListeners(
        Arrays.asList(new BootstrapApplicationListener(),
            new ConfigFileApplicationListener()));
capture = builder.run();

這個(gè)應(yīng)用只是為了重新加載一遍屬性源推溃,所以只配置了 BootstrapApplicationListener 和 ConfigFileApplicationListener昂利,最后將新加載的屬性源替換掉原屬性源,至此屬性源本身已經(jīng)完成更新了。
此時(shí)屬性源雖然已經(jīng)更新了蜂奸,但是配置項(xiàng)都已經(jīng)注入到了對(duì)應(yīng)的 Spring Bean 中犁苏,需要重新進(jìn)行綁定,所以又觸發(fā)了兩個(gè)操作:

  1. 將刷新后發(fā)生更改的 Key 收集起來(lái)扩所,發(fā)送一個(gè) EnvironmentChangeEvent 事件围详。
  2. 調(diào)用 RefreshScope.refreshAll 方法。

EnvironmentChangeEvent

在上文中祖屏,ContextRefresher 發(fā)布了一個(gè) EnvironmentChangeEvent 事件助赞,接下來(lái)看看這個(gè)事件產(chǎn)生了哪些影響。

The application will listen for an EnvironmentChangeEvent and react to the change in a couple of standard ways (additional ApplicationListeners can be added as @Beans by the user in the normal way). When an EnvironmentChangeEvent is observed it will have a list of key values that have changed, and the application will use those to:

  1. Re-bind any @ConfigurationProperties beans in the context
  2. Set the logger levels for any properties in logging.level.*

官方文檔的介紹中提到袁勺,這個(gè)事件主要會(huì)觸發(fā)兩個(gè)行為:

  1. 重新綁定上下文中所有使用了 @ConfigurationProperties 注解的 Spring Bean雹食。
  2. 如果 logging.level.* 配置發(fā)生了改變,重新設(shè)置日志級(jí)別期丰。

這兩段邏輯分別可以在 ConfigurationPropertiesRebinder 和 LoggingRebinder 中看到群叶。

ConfigurationPropertiesRebinder

這個(gè)類(lèi)乍一看代碼量特別少,只需要一個(gè) ConfigurationPropertiesBeans 和一個(gè)ConfigurationPropertiesBindingPostProcessor钝荡,然后調(diào)用 rebind 每個(gè) Bean 即可街立。但是這兩個(gè)對(duì)象是從哪里來(lái)的呢?

public void rebind() {
  for (String name : this.beans.getBeanNames()) {
    rebind(name);
  }
}

ConfigurationPropertiesBeans 需要一個(gè) ConfigurationBeanFactoryMetaData埠通, 這個(gè)類(lèi)邏輯很簡(jiǎn)單赎离,它是一個(gè) BeanFactoryPostProcessor 的實(shí)現(xiàn),將所有的 Bean 都存在了內(nèi)部的一個(gè) Map 中植阴。

而 ConfigurationPropertiesBeans 獲得這個(gè) Map 后蟹瘾,會(huì)查找每一個(gè) Bean 是否有 @ConfigurationProperties 注解,如果有的話就放到自己的 Map 中掠手。

繞了一圈好不容易拿到所有需要重新綁定的 Bean 后憾朴,綁定的邏輯就要簡(jiǎn)單許多了:

public boolean rebind(String name) {
  if (!this.beans.getBeanNames().contains(name)) {
    return false;
  }
  if (this.applicationContext != null) {
    try {
      Object bean = this.applicationContext.getBean(name);
      if (AopUtils.isCglibProxy(bean)) {
        bean = getTargetObject(bean);
      }
      this.binder.postProcessBeforeInitialization(bean, name);
      this.applicationContext.getAutowireCapableBeanFactory()
          .initializeBean(bean, name);
      return true;
    }
    catch (RuntimeException e) {
      this.errors.put(name, e);
      throw e;
    }
  }
  return false;
}

其中 postProcessBeforeInitialization 方法將 Bean 重新綁定了所有屬性,并做了校驗(yàn)等操作喷鸽。

而 initializeBean 的實(shí)現(xiàn)如下:

protected Object initializeBean(final String beanName, final Object bean, RootBeanDefinition mbd) {
  Object wrappedBean = bean;
  if(mbd == null || !mbd.isSynthetic()) {
    wrappedBean = this.applyBeanPostProcessorsBeforeInitialization(bean, beanName);
  }
  try {
    this.invokeInitMethods(beanName, wrappedBean, mbd);
  } catch (Throwable var6) {
    throw new BeanCreationException(mbd != null?mbd.getResourceDescription():null, beanName, "Invocation of init method failed", var6);
  }
  if(mbd == null || !mbd.isSynthetic()) {
    wrappedBean = this.applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
  }
  return wrappedBean;
}

其中主要做了三件事:

  1. applyBeanPostProcessorsBeforeInitialization:調(diào)用所有 BeanPostProcessor 的 postProcessBeforeInitialization 方法众雷。
  2. invokeInitMethods:如果 Bean 繼承了 InitializingBean,執(zhí)行 afterPropertiesSet 方法做祝,或是如果 Bean 指定了 init-method 屬性砾省,如果有則調(diào)用對(duì)應(yīng)方法
  3. applyBeanPostProcessorsAfterInitialization:調(diào)用所有 BeanPostProcessor 的 postProcessAfterInitialization 方法。

之后 ConfigurationPropertiesRebinder 就完成整個(gè)重新綁定流程了混槐。

LoggingRebinder

相比之下 LoggingRebinder 的邏輯要簡(jiǎn)單許多编兄,它只是調(diào)用了 LoggingSystem 的方法重新設(shè)置了日志級(jí)別,具體邏輯就不在本文詳述了声登。

RefreshScope

首先看看這個(gè)類(lèi)的注釋?zhuān)?/p>

Note that all beans in this scope are only initialized when first accessed, so the scope forces lazy initialization semantics. The implementation involves creating a proxy for every bean in the scope, so there is a flag
If a bean is refreshed then the next time the bean is accessed (i.e. a method is executed) a new instance is created. All lifecycle methods are applied to the bean instances, so any destruction callbacks that were registered in the bean factory are called when it is refreshed, and then the initialization callbacks are invoked as normal when the new instance is created. A new bean instance is created from the original bean definition, so any externalized content (property placeholders or expressions in string literals) is re-evaluated when it is created.

這里提到了兩個(gè)重點(diǎn):

  1. 所有 @RefreshScope 的 Bean 都是延遲加載的狠鸳,只有在第一次訪問(wèn)時(shí)才會(huì)初始化
  2. 刷新 Bean 也是同理揣苏,下次訪問(wèn)時(shí)會(huì)創(chuàng)建一個(gè)新的對(duì)象

再看一下方法實(shí)現(xiàn):

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

這個(gè)類(lèi)中有一個(gè)成員變量 cache,用于緩存所有已經(jīng)生成的 Bean件舵,在調(diào)用 get 方法時(shí)嘗試從緩存加載卸察,如果沒(méi)有的話就生成一個(gè)新對(duì)象放入緩存,并通過(guò) getBean 初始化其對(duì)應(yīng)的 Bean:

public Object get(String name, ObjectFactory<?> objectFactory) {
  if (this.lifecycle == null) {
    this.lifecycle = new StandardBeanLifecycleDecorator(this.proxyTargetClass);
  }
  BeanLifecycleWrapper value = this.cache.put(name,
      new BeanLifecycleWrapper(name, objectFactory, this.lifecycle));
  try {
    return value.getBean();
  }
  catch (RuntimeException e) {
    this.errors.put(name, e);
    throw e;
  }
}

所以在銷(xiāo)毀時(shí)只需要將整個(gè)緩存清空铅祸,下次獲取對(duì)象時(shí)自然就可以重新生成新的對(duì)象坑质,也就自然綁定了新的屬性:

public void destroy() {
  List<Throwable> errors = new ArrayList<Throwable>();
  Collection<BeanLifecycleWrapper> wrappers = this.cache.clear();
  for (BeanLifecycleWrapper wrapper : wrappers) {
    try {
      wrapper.destroy();
    }
    catch (RuntimeException e) {
      errors.add(e);
    }
  }
  if (!errors.isEmpty()) {
    throw wrapIfNecessary(errors.get(0));
  }
  this.errors.clear();
}

清空緩存后,下次訪問(wèn)對(duì)象時(shí)就會(huì)重新創(chuàng)建新的對(duì)象并放入緩存了临梗。

而在清空緩存后涡扼,它還會(huì)發(fā)出一個(gè) RefreshScopeRefreshedEvent 事件,在某些 Spring Cloud 的組件中會(huì)監(jiān)聽(tīng)這個(gè)事件并作出一些反饋夜焦。

Zuul

Zuul 在收到這個(gè)事件后壳澳,會(huì)將自身的路由設(shè)置為 dirty 狀態(tài):

private static class ZuulRefreshListener implements ApplicationListener<ApplicationEvent> {
  @Autowired
  private ZuulHandlerMapping zuulHandlerMapping;
  
  @Override
  public void onApplicationEvent(ApplicationEvent event) {
    if (event instanceof ContextRefreshedEvent
        || event instanceof RefreshScopeRefreshedEvent
        || event instanceof RoutesRefreshedEvent) {
      this.zuulHandlerMapping.setDirty(true);
    }
  }
}

并且當(dāng)路由實(shí)現(xiàn)為 RefreshableRouteLocator 時(shí),會(huì)嘗試刷新路由:

public void setDirty(boolean dirty) {
  this.dirty = dirty;
  if (this.routeLocator instanceof RefreshableRouteLocator) {
    ((RefreshableRouteLocator) this.routeLocator).refresh();
  }
}

當(dāng)狀態(tài)為 dirty 時(shí)茫经,Zuul 會(huì)在下一次接受請(qǐng)求時(shí)重新注冊(cè)路由,以更新配置:

if (this.dirty) {
  synchronized (this) {
    if (this.dirty) {
      registerHandlers();
      this.dirty = false;
    }
  }
}

Eureka

在 Eureka 收到該事件時(shí)萎津,對(duì)于客戶端和服務(wù)端都有不同的處理方式:

protected static class EurekaClientConfigurationRefresher {
  @Autowired(required = false)
  private EurekaClient eurekaClient;
  @Autowired(required = false)
  private EurekaAutoServiceRegistration autoRegistration;
  @EventListener(RefreshScopeRefreshedEvent.class)
  public void onApplicationEvent(RefreshScopeRefreshedEvent event) {
    //This will force the creation of the EurkaClient bean if not already created
    //to make sure the client will be reregistered after a refresh event
    if(eurekaClient != null) {
      eurekaClient.getApplications();
    }
    if (autoRegistration != null) {
      // register in case meta data changed
      this.autoRegistration.stop();
      this.autoRegistration.start();
    }
  }
}

對(duì)于客戶端來(lái)說(shuō)卸伞,只是調(diào)用了下 eurekaClient.getApplications,理論上這個(gè)方法是沒(méi)有任何效果的锉屈,但是查看上面的注釋?zhuān)约奥?lián)想到 RefreshScope 的延時(shí)初始化特性荤傲,這個(gè)方法調(diào)用應(yīng)該只是為了強(qiáng)制初始化新的 EurekaClient。

事實(shí)上這里很有趣的是颈渊,在 EurekaClientAutoConfiguration 中遂黍,實(shí)際為了 EurekaClient 提供了兩種初始化方案,分別對(duì)應(yīng)是否有 RefreshScope俊嗽,所以以上的猜測(cè)應(yīng)該是正確的雾家。

而對(duì)于服務(wù)端來(lái)說(shuō),EurekaAutoServiceRegistration 會(huì)將服務(wù)端先標(biāo)記為下線绍豁,在進(jìn)行重新上線芯咧。

總結(jié)

至此,Spring Cloud 的熱更新流程就到此結(jié)束了竹揍,從這些源碼中可以總結(jié)出以下結(jié)論:

  1. 通過(guò)使用 ContextRefresher 可以進(jìn)行手動(dòng)的熱更新敬飒,而不需要依靠 Bus 或是 Endpoint。
  2. 熱更新會(huì)對(duì)兩類(lèi) Bean 進(jìn)行配置刷新芬位,一類(lèi)是使用了 @ConfigurationProperties 的對(duì)象无拗,另一類(lèi)是使用了 @RefreshScope 的對(duì)象。
  3. 這兩種對(duì)象熱更新的機(jī)制不同昧碉,前者在同一個(gè)對(duì)象中重新綁定了所有屬性英染,后者則是利用了 RefreshScope 的緩存和延遲加載機(jī)制阴孟,生成了新的對(duì)象。
  4. 通過(guò)自行監(jiān)聽(tīng) EnvironmentChangeEvent 事件税迷,也可以獲得更改的配置項(xiàng)永丝,以便實(shí)現(xiàn)自己的熱更新邏輯。
  5. 在使用 Eureka 的項(xiàng)目中要謹(jǐn)慎的使用熱更新箭养,過(guò)于頻繁的更新可能會(huì)使大量項(xiàng)目頻繁的標(biāo)記下線和上線慕嚷,需要注意。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末毕泌,一起剝皮案震驚了整個(gè)濱河市喝检,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌撼泛,老刑警劉巖挠说,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異愿题,居然都是意外死亡损俭,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)潘酗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)杆兵,“玉大人,你說(shuō)我怎么就攤上這事仔夺∷鲈啵” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵缸兔,是天一觀的道長(zhǎng)日裙。 經(jīng)常有香客問(wèn)我,道長(zhǎng)惰蜜,這世上最難降的妖魔是什么昂拂? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮蝎抽,結(jié)果婚禮上政钟,老公的妹妹穿的比我還像新娘。我一直安慰自己樟结,他們只是感情好养交,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著瓢宦,像睡著了一般碎连。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上驮履,一...
    開(kāi)封第一講書(shū)人閱讀 49,144評(píng)論 1 285
  • 那天鱼辙,我揣著相機(jī)與錄音廉嚼,去河邊找鬼。 笑死倒戏,一個(gè)胖子當(dāng)著我的面吹牛怠噪,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播杜跷,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼傍念,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了葛闷?” 一聲冷哼從身側(cè)響起憋槐,我...
    開(kāi)封第一講書(shū)人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎淑趾,沒(méi)想到半個(gè)月后阳仔,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡扣泊,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年近范,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片旷赖。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡顺又,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出等孵,到底是詐尸還是另有隱情,我是刑警寧澤蹂空,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布俯萌,位于F島的核電站,受9級(jí)特大地震影響上枕,放射性物質(zhì)發(fā)生泄漏咐熙。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一辨萍、第九天 我趴在偏房一處隱蔽的房頂上張望棋恼。 院中可真熱鬧,春花似錦锈玉、人聲如沸爪飘。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)师崎。三九已至,卻和暖如春椅棺,著一層夾襖步出監(jiān)牢的瞬間犁罩,已是汗流浹背齐蔽。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留床估,地道東北人含滴。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像丐巫,于是被迫代替她去往敵國(guó)和親谈况。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容