21. SpringCloud之配置中心ConfigServer源码 & 配置刷新源码解读

image.png

1、前言

分布式配置中心ConfigServer的使用方式 :

SpringCloud之ConfigServer配置中心以及Bus消息总线

2、配置中心服务端

2.1、源码入口

@EnableConfigServer 这个注解仅仅起到一个标识的作用,没有引入任何东西

那么就 还是spi的方式

找到Maven: org.springframework.cloud:spring-cloud-config-server:2.0.0.RELEASE2包下的META-INF/Spring.factories.

image.png

2.2、接口的注册

在配置中心服务端会创建很多接口,比如

  • 配置信息的加解密接口
  • 查看 配置中心服务 拉取到的配置信息接口 :我们可以直接在调用查看配置信息, 配置中心客户端在启动的时候,也会调用这个接口从配置中心服务端获取对应的配置信息。

SPI从spring.factories导入的org.springframework.cloud.config.server.config.ConfigServerAutoConfiguration类。

image.png

会导入 ConfigServerEncryptionConfiguration,ConfigServerMvcConfiguration这两个类,接口的创建就在这两个类中。

2.3.1、获取配置信息的接口注册

ConfigServerMvcConfiguration类里会用@Bean 注册 EnvironmentController接口

image.png

EnvironmentController 就是用来请求获取 配置信息的Controller类,类上有@RestController+ @RequestMapping注解。

image.png

里面有各种形式的接口来获取配置信息。

image.png
image.png

2.3.2、加解密接口的注册

ConfigServerEncryptionConfiguration 会用@Bean 注册 EncryptionController接口

image.png

EncryptionController就是一个Controller类 ,有@RestController + @RequestMapping 注解,里面会配置 加解密接口。

2.3.2.1、加密接口

image.png

2.3.2.2、解密接口

image.png

2.3、获取配置信息接口 源码

获取配置信息的接口请求路径有很多种形式,我们挑/{label}/{name}-{profiles}.properties 这种,对应的是EnvironmentController的 这个接口。 看看源码的执行流程。

image.png

调用这个接口 , 获取 master分支, micro-order服务,dev环境的配置文件.

http://localhost:9101/master/micro-order-dev.properties

断点进到了这个接口, 走labelled方法获取环境对象

image.png

调用EnvironmentEncryptorEnvironmentRepository对象的findOne方法

image.png

调用 SearchPathCompositeEnvironmentRepository类的findOne方法。

由于SearchPathCompositeEnvironmentRepository类没有实现findOne方法,会调到它的父类AbstractScmEnvironmentRepository的findOne方法。

image.png

进入getLocations 方法

image.png

进入 MultipleJGitEnvironmentRepository类的 getLocations()。

MultipleJGitEnvironmentRepository 的父类 是JGitEnvironmentRepository 类,调父类的getLocations方法。

image.png

如果请求接口传入 git分支参数, 那么获取的lebel就是请求传的,否则默认获取的是master分支的。

然后调refresh方法,获取这个分支下的所有配置。

image.png

createGitClient() ,会先检查git本地仓库是否存在,不存在就会创建git本地仓库。

image.png

接着判断 git本地仓库 文件夹是否存在, 判断的文件夹路径 由getWorkingDirectory()返回,我这mac电脑的路径是

/var/folders/tb/w7qkq1w54w3g25_3_jp0w6500000gn/T/config-repo-8553408175024803312, 的子路径下是否有 .git/index.lock文件,是否存在git的锁,有的话就删除。

windows的本地仓库路径 好像是什么 C://work/config/tmp啥的

image.png

接下来判断 本地仓库/var/folders/tb/w7qkq1w54w3g25_3_jp0w6500000gn/T/config-repo-8553408175024803312 下是否有.git文件存在。

如果存在就直接返回这个路径下的git操作对象, 不存在就会 新建一个这个路径,然后在下面生成.git文件

image.png

我们先把这个文件夹删除删除掉。让他重新创建一个。

image.png

这个就会创建一个这样的目录,并且生成.git文件。并且 设置远程仓库地址

image.png

并且会执行git clone 指令,把整个 仓库克隆到本地仓库。

image.png

这个时候,这个路径下有 整个配置文件的config文件夹了。

/var/folders/tb/w7qkq1w54w3g25_3_jp0w6500000gn/T/config-repo-8553408175024803312/config :

image.png

拉取完配置文件之后,判断是否需要更新,如果需要的话,就会先执行 git fetch指令,

git checkout 切换分支 到 我们接口请求对应的分支下。

然后执行 merge指令, 把文件更新到最新的状态。

image.png

这个时候,服务端 已经把所有最新的配置文件 拉取到本地仓库了。

然后返回getLocations 返回 本地仓库的地址,以及服务名,环境,分支,版本号。

image.png

然后会把对应服务的配置文件 放到 enviroment对象 里的PropertySources 数组中,返回enviroment对象。

image.png

再把Environment对象 , 本地仓库路径 替换成git仓库地址,变成这样。然后返回

image.png

最后,从environment对象里获取 micro-order-dev.properties里的内容,把map变成 字符串形式,返回出去。

image.png

2.4、服务端 本地配置仓库 初始化

当然,配置中心服务端 不会只等到 被请求接口了才会 去git拉取 配置文件到本地仓库,而是在启动完成之后会由线程池 定时触发 健康检测。在健康检测里,就会从git 更新 配置文件到本地仓库。

走的也是 请求git,创建本地仓库,git clone, 然后merge更新。

image.png

3、配置中心客户端

配置中心客户端 会在启动的时候请求 配置中心服务端 获取 配置信息, 然后塞到自己的environment 对象的propertySource属性中。

3.1、源码入口

同样是spi。

Maven: org.springframework.cloud:spring-cloud-config-client:2.0.0.RELEASE2包下的META-INF/spring.factories。

image.png

3.1.1、加载 key为 BootstrapConfiguration的类

只不过这里配置的key是BootstrapConfiguration类型的,springboot启动的时候默认加载的是key为EnableAutoConfiguration的类。

key为BootstrapConfiguration的类 是由监听器加载的。

有个 BootstrapApplicationListener 监听器类,实现了ApplicationListener接口,当Spring容器发布 ApplicationEnvironmentPreparedEvent 事件时,会调用实现的onApplicationEvent方法。

image.png

这里就会加载META-INF/Spring.factories 下配置的key为 BootstrapConfiguration类型的类。

image.png

关于事件是啥时候发布的,可以看一下调用链,也是在Springboot的run方法里调用的。

image.png

3.1.2、ConfigServicePropertySourceLocator实例和ConfigClientProperties实例的注册

META-INF/spring.factories文件里配置了 key为BootstrapConfiguration 的有ConfigServiceBootstrapConfiguration类

image.png

ConfigServiceBootstrapConfiguration类会用@Bean注册ConfigServicePropertySourceLocator类 ,并且还会在方法列表注入配置中心客户端的配置对象ConfigClientProperties对象

ConfigServicePropertySourceLocator实现PropertySourceLocator接口,实现locate方法。

image.png

ConfigClientProperties对象加载 spring.cloud.config为前缀的配置信息

image.png

3.1.3、ConfigServicePropertySourceLocator类locate()的调用时机

之后Springboot在run方法里面的prepareContext方法会调用所有 ApplicationContextInitializer接口实例的 initialize方法。

image.png

有一个ApplicationContextInitializer接口实现类 PropertySourceBootstrapConfiguration,会注入PropertySourceLocator接口类型的实例,也就是ConfigServicePropertySourceLocator实例。

image.png

然后会在 initialize方法里调用所有 PropertySourceLocator接口类型 的实例的locate方法。也就会调用到 ConfigServicePropertySourceLocator的locate方法。

image.png

3.1.4、ConfigServicePropertySourceLocator的locate()去配置中心服务端 拉取配置信息

3.1.4.1、去配置中心服务端 拉取配置信息

ConfigServicePropertySourceLocator的locate方法,去配置中心服务端 拉取配置信息。

先创建一个restTemplate对象,getRemoteEnvironment()

image.png

在getRemoteEnvironment()方法里,先拼接请求的接口路径

是 /{name}/{profile}/{label}

替换变量就是 /micro-user/dev/master 接口

image.png
1、配置中心服务端uri的获取

在我们的配置里,一般都是配serviceId的。

spring.cloud.config.discovery.enabled=true
spring.cloud.config.discovery.serviceId=config-server

所以需要根据服务名称 从 注册中心 获取到 配置中心服务端的 ip+端口。

Springcloud eureka客户端从注册中心 拉取完服务列表里,会发布HeartbeatEvent事件。

SpringCloud之Eureka客户端源码解析 这篇有介绍,这里就简单带过了。

image.png

这里发布的是HeartbeatEvent事件。

image.png

有一个监听 HeartbeatEvent事件的 ApplicationListenerMethodAdapter类,在响应事件时,会反射调用org.springframework.cloud.config.client.DiscoveryClientConfigServiceBootstrapConfiguration类的 heartbeat方法

image.png

DiscoveryClientConfigServiceBootstrapConfiguration 会注入ConfigClientProperties 配置中心客户端配置类ConfigClientProperties对象

image.png

在DiscoveryClientConfigServiceBootstrapConfiguration的heartbeat方法,会调用refresh()

image.png

refresh()则会从配置中心客户端配置类ConfigClientProperties对象 里拿到我们配置的配置中心服务端的服务名称, 然后从 拉取到的服务列表里,获取 这个服务名称下对应的服务实例。

image.png

拿到所有服务实例之后, 遍历获取 ip+端口,放到一个url的列表里,最后设置到 配置中心客户端配置类ConfigClientProperties对象 的uri 属性中。

image.png

所以,后面 配置中心 客户端 直接就可以从 配置中心客户端配置类ConfigClientProperties对象里的 uri数组里,获取到 ip+端口, 发起请求就行了。

2、发起请求

**遍历 配置中心客户端配置类ConfigClientProperties对象里的 uri数组, 对 每个 配置中心服务端的 ip+端口进行 接口请求。如果有一个配置中心服务端成功返回了 它的配置信息,就返回。 **

先 把用户名,密码 用base64 加密 方法请求头Authorization里,

最后向配置中心服务端http://192.168.31.211:9101/{name}/{profile}/{label} 发GET请求,获取对应的配置信息

image.png

返回的reponse里的Body是一个Environment对象,里面的propertiesSource 列表就是 这个服务dev环境从master分支 对应 的 配置信息。

image.png

最后return,跳出对 配置中心服务端的 uri数组的循环。

3.1.4.2、设置到本地的 Environment对象的PropertySource中

获取到Environment对象之后,加到name = configService 的CompositePropertySource对象中,

image.png

最终返回的CompositePropertySource对象是这样的,里面有一个propertySources列表

就是从配置中心服务端拉到的 配置信息

image.png

然后把新返回的 CompositePropertySource对象 再添加到name = bootstrapProperties的CompositePropertySource对象的propertySource列表中,

最终添加到 插入到 配置中心客户端的 environment对象中

image.png

最后environment的propertySource就有 从配置中心服务端拉取的配置信息了。

image.png

后面其他的bean实例化,就可以 从 environment里面 获取 从配置中心服务端拉取的配置信息,注入到需要使用的地方了。

4、配置信息的刷新

如果我们在git里的配置信息的配置信息发生改变,SpringCloud提供了 配置中心服务端 刷新配置信息的功能。

有以下两种方式 :

  1. 手动调用每台服务 actuator组件提供的 /actuator/refresh 接口。
  2. 引入SpringCloudBus, 当配置发生改变时,调用 /actuator/bus-refresh接口,通过消息中间件 以广播的形式 通知 所有 服务 刷新配置信息。 或者 结合github -webhook,自动调用 /actuator/bus-refresh接口。

SpringCloudBus 与 手动调用 每台服务的 /actuator/refresh接口 所做的刷新配置的工作 是一样的,只不过 不用 一台一台的 去调 /actuator/refresh 接口,而是通过广播。

所以我们这里只介绍 /actuator/refresh 接口这种 是怎么刷新 配置的。

关于服务刷新配置的 大致原理 我在SpringCloud之ConfigServer配置中心以及Bus消息总线里有介绍过。大致有两点 :

  • 替换当前spring上下文的environment对象的propertySources对象里的配置信息为最新从 配置中心服务端拉去的配置。
  • 为了bean里 有@Value之类的引用重新 赋值 ,刷新作用域为@RefreshScope 的bean,让这些bean重新 初始化,重新从 environment对象里获取配置, 将最新值注入到 @Value的属性中。

4.1、重新拉取配置信息, 替换 environment对象的propertySources

首先找到 /actuator/refresh接口所在的源码位置。

RefreshEndpoint类的refresh方法

image.png

当我们调用 /actuator/refresh接口,就会进到这里。进入contextRefresher的refresh()

image.png

先把当前的Environment对象的PropertySources列表保存。然后调用addConfigFilesToEnvironment方法。

image.png

addConfigFilesToEnvironment方法里 会 重新 创建一个SpringApplication类,

  • 指定environment为当前Spring上下文environment对象的备份。PropertySources的引用是相同的。
  • 要加载的source类 是 Empty类, 是个空的类。
  • 并且指定 WebApplicationType.NONE,不启动spring里的web容器相关的东西。
  • 并且手动加入 BootstrapApplicationListener监听器类,注意,这个类 是会 加载META-INF/spring.factories 文件里配置的 key为 BootstrapConfiguration的类的。

之后调用SpringApplication的run方法。

image.png

解读一下这是要干嘛。其实就是 启动了一个特别轻量级的springboot容器。什么功能也不启用,web相关的东西也不启用。 然后run的时候,就会加载 SPI里配置的相关的类。其他什么bean都不注册,因为他传入的class是个空的类。

新的Spring容器里要实例化的bean也就只有这么几个spi导入进来的

image.png

加载spi之后,那么肯定就会加载到 配置中心客户端 的 ConfigServicePropertySourceLocator类, 然后又可以借用 ApplicationContextInitializer接口实例 去调用PropertySourceLocator接口的实现类onfigServicePropertySourceLocator类的 locate()方法,重新从配置中心服务端 拉取配置。

拉取完配置之后,传入的environment对象的propertySources对象 就又有 从配置中心服务端 拉取的配置了。

新创建的spring容器 的 environment对象:

image.png

最后返回这个Spring上下文对象。

返回之后,就是 与之前的 Environment对象里PropertySources集合进行比较交换。把更改后的配置信息 更新到 当前spring上下文的Environment对象里。

由于刚才新创建并启动的Spring容器传入的 Environment对象里 的 propertySources 对象 的引用 就是 当前spring上下文的Environment对象里的。所以就直接用 当前spring上下文的Environment对象里的propertySources对象进行比较。

image.png

然后返回 有改动的 配置文件的key的 集合。

到这里, Environment对象里 的 propertySources 已经被替换成最新拉取的配置了。

4.2、刷新作用域为@RefreshScope 的bean

更新完 Environment对象里 的 propertySources之后,下面还有一行很重要的代码。

刷新 作用域为 @RefreshScope的所有bean。

image.png

刷新其实就是把他们全部都销毁了。

image.png

直接把@RefreshScope作用域里存放bean的 缓存清了。

image.png

作用域Scope接口

作用域其实就是Scope接口的实现类 来实现 管理bean的方式。

Spring在获取bean的时候,会调用 Scope接口的实现类的get方法,来获取bean。并传入ObjectFactory对象。

image.png

Spring创建bean:

image.png

ObjectFactory的getObject方法是会去创建bean的。Scope接口的实现类的get方法需要自己来判断 bean是不是存在,不存在 就调用 ObjectFactory的getObject方法去创建bean, 然后返回这个bean。 存在就直接 返回。

像spring里的单例作用域,get方法里如果不存在,创建完bean之后,就会存起来,下次get就从缓存里拿,不会再创建新的实例,以保证从spring容器里获取的该bean永远返回的是同一个实例。

而spring里的多例作用域, get方法里如果不存在,创建完bean之后,它不会缓存起来,下次来还当没有,总是创建最新的,所以 从spring容器里获取的该beanu永远是新创建的实例。

RefreshScope 如何管理bean?

RefreshScope是借用内部缓存来 管理bean的。

get方法

走到父类GenericScope的get方法,

image.png

会往BeanLifecycleWrapperCache进行缓存。

image.png

然后返回BeanLifecycleWrapper.get方法,判断bean是不是实例化,没有实例化就调用ObjectFactory的getObject方法, 实例化了就直接返回。

image.png

bean的销毁

所以现在我们调用 /actuator/refresh接口刷新配置时,就把 RefreshScope作用域里的缓存全部都清了, 下次这些bean 被用到时,进RefreshScope的get方法 ,缓存里没有, 那么就会重新实例化这些bean。重新这些实例化bean,那么这些bean里用到 @value的地方 就会从 最新的environment对象里获取最新的配置,注入进去, 以达到 刷新配置的效果。

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