從攜程Apollo客戶端源碼學(xué)到的知識(一)

知識點:如何執(zhí)行被注解ApolloConfigChangeListener標(biāo)注的方法

一憔足、起因

事情的故事的是這個樣子的,前面的文章我們自定義了一個starter①酪耕,并且集成了Apollo导梆,可以自動從Apollo中獲取配置,但是我們發(fā)現(xiàn)它沒辦法保證在Apollo修改了配置之后,同步更新到本地看尼。我們找到官方的WIKI介紹②递鹉,我用人話重新組織了下他們的語言:我們“不支持@ConfigurationProperties自動注入,需要自己寫點東西才行藏斩,具體是啥躏结,想知道直接查看附錄的超鏈接。

二狰域、方案

根據(jù)附錄鏈接媳拴,我很快定位并寫了測試方法,很快....真的很快兆览!代碼如下屈溉,經(jīng)過測試發(fā)現(xiàn),隨時修改抬探,隨時變化了语婴,達到了我想要的效果

package com.example.fg;

import com.ctrip.framework.apollo.model.ConfigChange;
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
 * @author cattle -  稻草鳥人
 * @date 2020/4/14 下午2:01
 */
@Slf4j
@Component
public class ApolloRefreshListener implements ApplicationContextAware {

    private ApplicationContext applicationContext;


    @ApolloConfigChangeListener(value = {"application","promotion"})
    private void onChange(ConfigChangeEvent changeEvent) {
        for (String key : changeEvent.changedKeys()) {
            ConfigChange change = changeEvent.getChange(key);
            log.info("change - {}", change.toString());
        }

        this.applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

但是....事情沒完淫奔!

從上面的代碼我們發(fā)現(xiàn)@ApolloConfigChangeListener注解中的value寫死了,這可愁死我了坑填,我可不想要寫死蚓耽,為啥呢?這話可能扯的有點遠疑枯,涉及到我對軟件架構(gòu)的思路,以后有機會寫寫「攘。總之,我不想寫死憋他。所以我就開始了翻看apollo-client源碼孩饼,這才會有了這篇文章

三、學(xué)習(xí)

1)執(zhí)行過程

跟著@ApolloConfigChangeListener經(jīng)過一番云雨...不對竹挡,一頓操作镀娶,讓我找到了它的祖宗,大概明白了該注解到底是怎么起作用的揪罕。下面我大概說明一下apollo-client整個工程的啟動順序

ApolloApplicationContextInitializer實現(xiàn)了ApplicationContextInitializer接口用于初始化一些內(nèi)容梯码。

在初始化之前會先去判斷apollo.bootstrap.enabled是否為true然后會獲取apollo.bootstrap.namespaces配置的namespace并分別獲取到所有namespaces的Config對象

for (String namespace : namespaceList) {
      Config config = ConfigService.getConfig(namespace);
            .....
}

下面我貼一下獲取Config執(zhí)行的代碼片段

ConfigService

public static Config getConfig(String namespace) {
    return s_instance.getManager().getConfig(namespace);
  }

DefaultConfigManager

public Config getConfig(String namespace) {
    Config config = m_configs.get(namespace);

    if (config == null) {
      synchronized (this) {
        config = m_configs.get(namespace);

        if (config == null) {
          ConfigFactory factory = m_factoryManager.getFactory(namespace);

          config = factory.create(namespace);
          m_configs.put(namespace, config);
        }
      }
    }

    return config;
  }

DefaultConfigFactory

public Config create(String namespace) {
    ConfigFileFormat format = determineFileFormat(namespace);
    if (ConfigFileFormat.isPropertiesCompatible(format)) {
      return new DefaultConfig(namespace, createPropertiesCompatibleFileConfigRepository(namespace, format));
    }
    return new DefaultConfig(namespace, createLocalConfigRepository(namespace));
  }

DefaultConfig

public DefaultConfig(String namespace, ConfigRepository configRepository) {
    m_namespace = namespace;
    m_resourceProperties = loadFromResource(m_namespace);
    m_configRepository = configRepository;
    m_configProperties = new AtomicReference<>();
    m_warnLogRateLimiter = RateLimiter.create(0.017); // 1 warning log output per minute
    initialize();
  }

  private void initialize() {
    try {
      updateConfig(m_configRepository.getConfig(), m_configRepository.getSourceType());
    } catch (Throwable ex) {
      Tracer.logError(ex);
      logger.warn("Init Apollo Local Config failed - namespace: {}, reason: {}.",
          m_namespace, ExceptionUtil.getDetailMessage(ex));
    } finally {
      //register the change listener no matter config repository is working or not
      //so that whenever config repository is recovered, config could get changed
      m_configRepository.addChangeListener(this);
    }
  }

從上面的代碼我們知道ApolloApplicationContextInitializer啟動的時候獲取了一個或者多個DefaultConfig。DefaultConfig再初始化的時候m_configRepository.addChangeListener(this);好啰。這段代碼成功的引起的我的注意轩娶,就像一個仙女從你面前路過,就算是直男也應(yīng)該有所反應(yīng)框往,除非他在打游戲鳄抒!這段代碼成功的將當(dāng)前DefaultConfig對象加入到了抽象類AbstractConfigRepositoryRepositoryChangeListener的集合中,

我們仔細觀察一下AbstractConfigRepository的幾個方法,它基本上就是一個標(biāo)準的模板方法模式靶斫Α瓤鼻!通過調(diào)用trySync()方法執(zhí)行sync(),sync()是抽象方法說明它是由子類實現(xiàn)的闹司。

[圖片上傳失敗...(image-5b8c3a-1587292509810)]

這里娱仔,我們介紹下RemoteConifgRepository。它是在什么時候初始化的呢游桩,我們繼續(xù)擼...發(fā)現(xiàn)DefaultConfigFactorynew DefaultConfig(namespace, createLocalConfigRepository(namespace))時createLocalConfigRepository(namespace)里面實例化一個RemoteConifgRepository牲迫。

OK,到這里我們簡單在總結(jié)下過程借卧,首先項目啟動的時候初始化了RemoteConifgRepository然后初始化DefaultConfig盹憎,并且將DefaultConfig加入到了RepositoryChangeListener的集合中。RemoteConfigRepository的初始化方法很有意思铐刘,我們注意到它的構(gòu)造器有如下三個方法都會執(zhí)行trySync(),而trySync()方法會執(zhí)行sync(),sync()方法則會執(zhí)行fireRepositoryChange然后執(zhí)行onRepositoryChange再執(zhí)行fireConfigChange最后執(zhí)行ConfigChangeListeneronChange方法.....

public RemoteConfigRepository(String namespace) {
    ....
    //啟動時執(zhí)行一次
    this.trySync();
    //定時執(zhí)行
    this.schedulePeriodicRefresh();
    //長連接拉取
    this.scheduleLongPollingRefresh();
  }

終于到正題了陪每,到底是哪個ConfigChangeListener執(zhí)行onChange方法,onChange方法的參數(shù)ConfigChangeEvent又是怎么得到的呢

2)添加ConfigChangeListener

在步驟一中镰吵,我們看到了在自定義的onChange方法上增加了一個ApolloConfigChangeListener注解檩禾,那么這個注解是如何生效并且添加ConfigChangeListener的呢

它是通過Service Provider Interface(SPI)機制,實現(xiàn)了一個DefaultApolloConfigRegistrarHelper疤祭,這里我們關(guān)注ApolloAnnotationProcessor類盼产。ApolloAnnotationProcessor的父親也就是BeanPostProcessor利用了BeanPostProcessor功能,在類初始化之前執(zhí)行了自己的processMethod方法勺馆。該方法則會將自定義的listener方法添加到DefaultConfig

protected void processMethod(final Object bean, String beanName, final Method method) {
    ApolloConfigChangeListener annotation = AnnotationUtils
        .findAnnotation(method, ApolloConfigChangeListener.class);
    if (annotation == null) {
      return;
    }
....

    String[] namespaces = annotation.value();
    String[] annotatedInterestedKeys = annotation.interestedKeys();
    String[] annotatedInterestedKeyPrefixes = annotation.interestedKeyPrefixes();
    ConfigChangeListener configChangeListener = new ConfigChangeListener() {
      @Override
      public void onChange(ConfigChangeEvent changeEvent) {
        ReflectionUtils.invokeMethod(method, bean, changeEvent);
      }
    };
  ....

    // 添加自定義的方法到DefaultConfig中的ConfigChangeListener集合屬性中
    for (String namespace : namespaces) {
      Config config = ConfigService.getConfig(namespace);

      if (interestedKeys == null && interestedKeyPrefixes == null) {
        config.addChangeListener(configChangeListener);
      } else {
        config.addChangeListener(configChangeListener, interestedKeys, interestedKeyPrefixes);
      }
    }
  }

到這里我們基本了解作者的意圖了戏售,目的就是為了 執(zhí)行被注解ApolloConfigChangeListener標(biāo)注的自定義的方法,執(zhí)行過程我們也了如指掌了草穆。那么.....問題來了灌灾,到底怎樣做ApolloConfigChangeListener上的namespace才可以不寫死呢,隨著項目配置自動獲取所有的namespace呢悲柱?

四锋喜、附錄

創(chuàng)建屬于自己的starterhttps://mp.weixin.qq.com/s/X3kJyFHrn7tul4PiGlfT5g

Apollo指南https://github.com/ctripcorp/apollo/wiki/Java客戶端使用指南#323-spring-annotation支持

一起學(xué)習(xí)吧
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市豌鸡,隨后出現(xiàn)的幾起案子跑芳,更是在濱河造成了極大的恐慌,老刑警劉巖直颅,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件博个,死亡現(xiàn)場離奇詭異,居然都是意外死亡功偿,警方通過查閱死者的電腦和手機盆佣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進店門往堡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人共耍,你說我怎么就攤上這事虑灰。” “怎么了痹兜?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵穆咐,是天一觀的道長。 經(jīng)常有香客問我字旭,道長对湃,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任遗淳,我火速辦了婚禮拍柒,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘屈暗。我一直安慰自己拆讯,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布养叛。 她就那樣靜靜地躺著种呐,像睡著了一般。 火紅的嫁衣襯著肌膚如雪弃甥。 梳的紋絲不亂的頭發(fā)上爽室,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天,我揣著相機與錄音潘飘,去河邊找鬼。 笑死掉缺,一個胖子當(dāng)著我的面吹牛卜录,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播眶明,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼艰毒,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了搜囱?” 一聲冷哼從身側(cè)響起丑瞧,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蜀肘,沒想到半個月后绊汹,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡扮宠,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年西乖,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡获雕,死狀恐怖薄腻,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情届案,我是刑警寧澤庵楷,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站楣颠,受9級特大地震影響尽纽,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜球碉,卻給世界環(huán)境...
    茶點故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一蜓斧、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧睁冬,春花似錦挎春、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至施禾,卻和暖如春脚线,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背弥搞。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工邮绿, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人攀例。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓船逮,卻偏偏與公主長得像,于是被迫代替她去往敵國和親粤铭。 傳聞我的和親對象是個殘疾皇子挖胃,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,092評論 2 355

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