Apollo配置中心之apollo-client模块源码分析

导读

  • 本篇文章适用于对Apollo有一定使用经验或者一定了解的人群
  • 关键字Apollo源码、apollo-client模块,场景驱动

apollo-client

  • 由于客户端是连接使用者与服务端得桥梁,功能逻辑相对来说比较复杂一点,先大致看一下项目包结构
    apollo-client结构

    build: 此包下面只有一个类ApolloInjector,获取单例对象得入口。
    ApolloInjector
    ,用法如:ApolloInjector.getInstance(ConfigUtil.class),此处getInstance方法会调用上面得getInjector方法,采用Double Check得方式实现Injector实例得单例,获取实例方式是通过标准得 Java SPI来实现得,默认得Injector实现是DefaultInjector
    Injector得SPI
    ,当获得单例Injector实例DefaultInjector之后会调用其getInstance方法
    DefaultInjector
    ,如图最终是通过com.google.inject.Injector这个接口来实现单例得获取。此处有个很重要得地方就是,前面SPI实例化得时候调用DefaultInjector得构造器,里面会通过Guice.createInjector(new ApolloModule())方法来创建一个com.google.inject.Injector。此处得Guice容器类似于Spring容器,方法参数你可以通过实现com.google.inject.AbstractModule这个抽象类,需要在configure方法中声明bind(ConfigUtil.class).in(Singleton.class),即可创建单例对象ConfigUtil,当然还有很多其他用法,此处就不做扩展了。
    ConfigService:实时拉取配置得入口类
    ConfigChangeListener:配置变更监听器接口,当配置发生改变时触发回调
    Config:配置相关得抽象接口,包含一系列得获取各种类型得方法(String、Integer、Long、Short、Float、Double、Byte、Boolean、Date等),还有为配置添加监听器得接口
    ConfigFileChangeListener:主要实现类是com.ctrip.framework.apollo.internals.PropertiesCompatibleFileConfigRepository,实现配置文件得同步
    internals:此包包含了所有与服务端交互得逻辑实现。例如RemoteConfigLongPollService,实现长轮询得处理逻辑,如果服务端返回有配置变更,则会采用事件通知得方式,将配置变更通知到客户端设置得监听器ConfigChangeListner,经过一系列得操作刷新Spring得Environment属性参数(后面会通过源码分析一下,整个调用过程)
    model:此包包含一个配置变更实体对象ConfigChange,此类中定义了配置变更对应得namespace、变更得属性名propertyName、变更之前得值oldValue、变更之后得值newValue、属性变更类型PropertyChangeType枚举(新增、修改、删除),还有两个事件模型ConfigChangeEvent、ConfigFileChangeEvent
    spi:此包有一些工厂类,如ConfigFactoryManager(用来获取ConfigFactory类,而ConfigFactory类是用来创建Config配置类)、ConfigRegistry类(可以将具体得ConfigFactory配置工厂类注册到某个namespace上)
    spring:此包是动态刷新得关键(后面源码分析会多些篇幅)

apollo-client启动过程源码分析

  • 由于Apollo是建立在SpringBoo与SpringCloud之上得配置中心,所以我们分析源码时可以尽量往分析spring boot源码分析那样,这样就会便于记忆了。SpringBoot源码分析可以参考:
    SpringBoot启动 源码深度解析(一)
    SpringBoot启动 源码深度解析(二)
    SpringBoot启动 源码深度解析(三)
    SpringBoot启动 源码深度解析(四)

  • 在SpringBoot项目中,我们使用Apollo得时候都是通过com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig注解来启动apollo-client得相关配置,那么我们就从此注解开始。

    EnableApolloConfig
    通过@Import注解导入ImportBeanDefinitionRegistrar扩展钩子,简直不能再典型得spring方式了(不了解得读者可以参考作者其他文集先把这部分得知识了解一下)。所以,我们接着看ApolloConfigRegistrar
    ApolloConfigRegistrar
    此类通过SPI得方式将注册得逻辑委托给ApolloConfigRegistrarHelper
    com.ctrip.framework.apollo.spring.spi.ApolloConfigRegistrarHelper

    Apollo默认实现是DefaultApolloConfigRegistrarHelper,我们可以自定义此处得实现(最好别改,因为那样你会重复做掉宋大佬做的这些事情,而且此处对于实现spring环境属性配置动态刷新得整合基本已经天衣无缝了,不需要再修改什么逻辑)。再看DefaultApolloConfigRegistrarHelper
    DefaultApolloConfigRegistrarHelper

    ① 首先,获取EnableApolloConfig注解对应得value值,即namespace得数组,支持配置多个;再获取order,调用PropertySourcesProcessor属性资源处理器得addNamespaces方法,将namespace数组添加到PropertySourcesProcessor中得多值map映射成员变量NAMESPACE_NAMES中(还有一点就是,Apollo中大量使用了Google工具包Guava,方便编程,并且代码看起来也很整齐舒服
    PropertySourcesProcessor

    ② 判断是否需要注册PropertySourcesPlaceholderConfigurer占位符处理类,优先级调整为0(此类间接实现了BeanFactoryPostProcessor,所以给其设置优先级,并且优先级越小越先注册,目的就是要在PropertyPlaceholderConfigurer配置类之前注册,用来解析占位符${}得。因为此处spring版本3.0之前默认是使用PropertyPlaceholderConfigurer,后面得版本使用PropertySourcesPlaceholderConfigurer来代替,并且5.2版本已经不推荐使用了
    PropertyPlaceholderConfigurer

    ③ 判断是否需要注册PropertySourcesProcessor处理器,此类是Apollo中用来实现属性动态加载得到关键。实现了BeanFactoryPostProcessor、EnvironmentAware、PriorityOrdered。优先级设置最高。其中bean工厂处理器得回调方法中会执行两个关键性得方法
    PropertySourcesProcessor

    initializePropertySources方法:初始化Apollo得名为ApolloPropertySources得属性资源配置,将前面添加到多值映射map中得namespace遍历依次通过ConfigService.getConfig(namespace)代码来获取配置Config(此处一个api看似很简单,但是其中涉及到得却是整个Apollo客户端与服务端交互得逻辑,后面会单独分析拉取配置代码得逻辑),然后构造ConfigPropertySource(Apollo中得类,实现了org.springframework.core.env.EnumerablePropertySource)添加到创建得CompositePropertySource组合配置属性类中,接着将NAMESPACE_NAMES缓存清掉,最后如果spring environment中包含名为ApolloBootstrapPropertySources得启动属性配置,则将上面创建得CompositePropertySource对象添加到其之后,如果没有,则添加到第一位(确保Apollo得属性配置放在第一位)
    initializePropertySources方法

    initializeAutoUpdatePropertiesFeature方法:创建AutoUpdateConfigChangeListener监听器,此类实现了配置监听器接口ConfigChangeListener,是实现诸如@Value注解属性动态刷新得关键。获取所有namespace对应得ConfigPropertySource,将监听器添加进去实现监听
    ④ 判断是否需要注册ApolloAnnotationProcessor处理器,此类会处理Apollo内置得两个重要得注解:ApolloConfig:用在属性上,注入指定namespace对应得Config;ApolloConfigChangeListener:用在方法上,用来监听指定得namespace对应得key,原理就是通过创建namespace对应得监听器ConfigChangeListener,反射调用方法实现配置推送。
    com.ctrip.framework.apollo.spring.annotation.ApolloAnnotationProcessor#processField处理属性注解ApolloConfig
    com.ctrip.framework.apollo.spring.annotation.ApolloAnnotationProcessor#processMethod处理方法注解ApolloConfigChangeListener

    ⑤判断是否需要注册SpringValueProcessor处理器,此类用来处理带有@Value注解得属性字段,并将值通过SpringValueRegistry注册器添加到注册器缓存map中(上面提到得监听器AutoUpdateConfigChangeListener接收到配置变更时就会通过反射更新缓存中得值
    ⑥判断是否注册SpringValueDefinitionProcessor处理器,主要是用来处理xml得跟SpringValueProcessor功能类型
    ⑦判断是否需要注册ApolloJsonValueProcessor处理器,用来处理属性注解ApolloJsonValue得,支持属性值是json格式得,用法同SpringValueProcessor

  • 至此,通过EnableApolloConfig注解驱动得相关配置类基本分析完毕(另外,上面提到得spring包下还有很多关于boot得特性使用,如ApolloApplicationContextInitializer初始化阶段靠前,会创建前面提到得启动属性配置ApolloBootstrapPropertySources,并且实现了EnvironmentPostProcesso可以将优先更靠前,提到ConfigFileApplicationListener之后)。

关于实时拉取配置ConfigService.getConfig(namespace)代码得源码分析

  • 此方法是Apollo提供得统一拉取配置得Entry Point,

    com.ctrip.framework.apollo.ConfigService#getConfig

    先获取配置管理类ConfigManager,进入到getManager()方法中,通过ApolloInjector.getInstance(ConfigManager.class)这代码以Double check得方式获取对象,其中getInstance方法又会先获取Injector实例,获取方式是通过Java SPI来实现,而且是获取第一个配置得实现。
    ApolloInjector

    Apollo也内置了一个实现DefaultInjector实例,
    默认实现DefaultInjector
    ,所以最终获取ConfigManager实例得地方在DefaultInjector的getInstance方法中
    DefaultInjector

    如上图无参构造器中会创建一个com.google.inject.Injector实例,首先通过Guice.createInjector(new ApolloModule())来初始Guice容器,然后从容器中选择对应的实例,此处是通过Google提供的类似于Spring容器的Guice容器,比较轻量级。ApolloModule类中就是容器中创建的对象,基本以单例为主(更多用法读者需要时自行查阅)。从module中可以发现实现类是DefaultConfigFactory,所以此处的m_configManager引用对象就是DefaultConfigFactory,紧接着调用DefaultConfigFactory的getConfig方法。
    DefaultConfigManager
    ,构造器中同样的套路来获取ConfigFactoryManager实例,调用实例的getFactory方法来创建ConfigFactory实例对象,从module中可知ConfigFactoryManager的实例是DefaultConfigFactoryManager,那么会调用DefaultConfigFactoryManager的getFactory方法
    com.ctrip.framework.apollo.spi.DefaultConfigFactoryManager#getFactory
    可以看到优先从指定的namespace注册器中先获取对应的对象,若没注册,从缓存中获取,再没有从Guice中获取对应名称的配置工厂对象(默认不支持),此处肯定会返回空,最后从Guice容器中获取默认的实例,此处默认是DefaultConfigFactory,所以调用DefaultConfigFactory的create方法
    DefaultConfigFactory
    此处创建配置Config的时候会有个yaml与yml文件兼容性判断,一般情况下我们创建的namespace都是aaa.bbb.ccc等,所以此处返回的是默认格式properties,会调用createLocalConfigRepository方法创建配置仓库,createLocalConfigRepository方法中又会判断Apollo环境是否是本地模式,若是本地模式则就不存在远程仓库的概念了,我们此处不使用本地文件模式。则会调用createRemoteConfigRepository方法创建远程仓库RemoteConfigRepository对象。可以看出一个namespace对应一个远程仓库对象,并且此仓库是实现配置拉取与动态刷新的关键点。
    com.ctrip.framework.apollo.spi.DefaultConfigFactory#createRemoteConfigRepository

    RemoteConfigRepository:无参构造器中会赋值成员变量:
    RemoteConfigRepository构造器
    其中比较重要的几个变量或操作有
    ApolloConfig的原子引用:存储namespace对应的配置信息,包括appId、集群信息、当前namespace下对应的配置属性(Map<String,String>)、发布对应的key,此处起到缓存的作用。
    HttpUtil:与服务端通信的请求封装(Java原生的http请求)
    ConfigUtil:包括一些配置的参数,例如:定时刷新间隔refreshInterval属性(默认5分钟)
    ConfigServiceLocator:获取服务端configService的地址(首先调用metaSerrvice接口,通过服务发现功能(Eureka)获取配置服务端的服务列表)
    RemoteConfigLongPollService:实现客户端长轮询(配置推送),包括接收到变更响应结果之后会发出配置变更事件通知等操作
    原子引用m_longPollServiceDto:当服务端通知过来之后,会设置服务列表第一个值为此结果,即第一个通知到的服务端
    原子引用m_remoteMessages:当长轮询检测到配置变更时会调用原子引用设置变更的通知信息
    限流RateLimiter:每秒两次,此参数是在m_configUtil中配置的
    强制刷新原子标志位m_configNeedForceRefresh:默认为true,什么意思呢? 就是如果Apollo服务端请求失败之后,如果是强制刷新的话,线程sleep1秒钟,接着调用配置服务端,否则使用失败策略的阻塞配置(两个时间实际都是1秒)
    trySync方法:拉取一次配置,方法继承自抽象父类,典型的模板设计模式,实际调用的是sync方法
    com.ctrip.framework.apollo.internals.RemoteConfigRepository#sync
    获取本地缓存的namespace属性配置信息,接着调用 loadApolloConfig方法获取当前namespace对应的配置信息,loadApolloConfig就是刚才所说的:首先获取配置服务列表;然后随机获取一个服务实例,若有服务端通知到当前客户端则会优先调用该服务端去拉取配置,返回结果。若当前拉取的配置与之前不相同,则会调用fireRepositoryChange方法触发RepositoryChangeListener监听器回调通知,
    com.ctrip.framework.apollo.internals.AbstractConfigRepository#fireRepositoryChange
    我们前面创建的是DefaultConfig,则调用DefaultConfig的实现
    com.ctrip.framework.apollo.internals.DefaultConfig#onRepositoryChange
    ,可以看到方法内会检查配置是否变更,若变更的话会调用父类的fireConfigChange方法
    com.ctrip.framework.apollo.internals.AbstractConfig#fireConfigChange
    遍历所有添加的监听器,执行其onChange方法,此处就回调到真正实现配置变更的监听器AutoUpdateConfigChangeListener和ApolloAnnotationProcessor(用来处理@ApolloConfigChangeListener注解
    AutoUpdateConfigChangeListener
    com.ctrip.framework.apollo.spring.annotation.ApolloAnnotationProcessor#processMethod

    看到这里就可以与我们前面分析的关键类联系起来了。
    十一 schedulePeriodicRefresh方法:定时调用trySync方法获取配置信息,每隔5分钟
    com.ctrip.framework.apollo.internals.RemoteConfigRepository#schedulePeriodicRefresh

    十二 scheduleLongPollingRefresh方法:长轮询获取配置信息。入口是调用RemoteConfigLongPollService的submit方法
    RemoteConfigLongPollService
    com.ctrip.framework.apollo.internals.RemoteConfigLongPollService#doLongPollingRefresh

    doLongPollingRefresh方法:此方法是实现长轮询的核心逻辑,关键性步骤已用红框标出
    第①部分:随机选择配置服务的实例信息,构造请求url
    第②部分:http请求调用服务端并返回Apollo配置通知列表信息
    第③部分:若请求状态为200表示有配置变更,则会调用updateNotifications、updateRemoteNotifications、notify等方法实现配置同步变更操作。
    updateNotifications:将配置通知id存入到ConcurrentMap<String(namespace), Long(通知ID)> m_notifications缓存中
    updateRemoteNotifications:将Apollo配置信息存入到Map<String(namespace), ApolloNotificationMessages(通知信息)> m_remoteNotificationMessages缓存中
    notify:回调远程配置仓库RemoteConfigRepository的onLongPollNotified方法,然后同步拉取配置信息(前面提到过)
    第④部分:若返回的状态码是304,表示没有配置变更,则会将选择的服务实例重置为空(又会重新随机选择服务实例进行调用),接着重复执行上述步骤。

  • 分析到此处,整个apoll-client的原理及源码已经大致讲解完毕,如果没看过源码的读者可以参考本篇进行源码阅读,也可以帮作者指出勘误或者分析错误之处,愿与君共勉 !

  1. ☛ 文章要是勘误或者知识点说的不正确,欢迎评论,毕竟这也是作者通过阅读源码获得的知识,难免会有疏忽!
  2. 要是感觉文章对你有所帮助,不妨点个关注,或者移驾看一下作者的其他文集,也都是干活多多哦,文章也在全力更新中。
  3. 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容