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.
2.2、接口的注册
在配置中心服务端会创建很多接口,比如
- 配置信息的加解密接口
- 查看 配置中心服务 拉取到的配置信息接口 :我们可以直接在调用查看配置信息, 配置中心客户端在启动的时候,也会调用这个接口从配置中心服务端获取对应的配置信息。
SPI从spring.factories导入的org.springframework.cloud.config.server.config.ConfigServerAutoConfiguration类。
会导入 ConfigServerEncryptionConfiguration,ConfigServerMvcConfiguration这两个类,接口的创建就在这两个类中。
2.3.1、获取配置信息的接口注册
ConfigServerMvcConfiguration类里会用@Bean 注册 EnvironmentController接口
EnvironmentController 就是用来请求获取 配置信息的Controller类,类上有@RestController+ @RequestMapping注解。
里面有各种形式的接口来获取配置信息。
2.3.2、加解密接口的注册
ConfigServerEncryptionConfiguration 会用@Bean 注册 EncryptionController接口
EncryptionController就是一个Controller类 ,有@RestController + @RequestMapping 注解,里面会配置 加解密接口。
2.3.2.1、加密接口
2.3.2.2、解密接口
2.3、获取配置信息接口 源码
获取配置信息的接口请求路径有很多种形式,我们挑/{label}/{name}-{profiles}.properties 这种,对应的是EnvironmentController的 这个接口。 看看源码的执行流程。
调用这个接口 , 获取 master分支, micro-order服务,dev环境的配置文件.
http://localhost:9101/master/micro-order-dev.properties
断点进到了这个接口, 走labelled方法获取环境对象
调用EnvironmentEncryptorEnvironmentRepository对象的findOne方法
调用 SearchPathCompositeEnvironmentRepository类的findOne方法。
由于SearchPathCompositeEnvironmentRepository类没有实现findOne方法,会调到它的父类AbstractScmEnvironmentRepository的findOne方法。
进入getLocations 方法
进入 MultipleJGitEnvironmentRepository类的 getLocations()。
MultipleJGitEnvironmentRepository 的父类 是JGitEnvironmentRepository 类,调父类的getLocations方法。
如果请求接口传入 git分支参数, 那么获取的lebel就是请求传的,否则默认获取的是master分支的。
然后调refresh方法,获取这个分支下的所有配置。
createGitClient() ,会先检查git本地仓库是否存在,不存在就会创建git本地仓库。
接着判断 git本地仓库 文件夹是否存在, 判断的文件夹路径 由getWorkingDirectory()返回,我这mac电脑的路径是
/var/folders/tb/w7qkq1w54w3g25_3_jp0w6500000gn/T/config-repo-8553408175024803312, 的子路径下是否有 .git/index.lock文件,是否存在git的锁,有的话就删除。
windows的本地仓库路径 好像是什么 C://work/config/tmp啥的。
接下来判断 本地仓库/var/folders/tb/w7qkq1w54w3g25_3_jp0w6500000gn/T/config-repo-8553408175024803312 下是否有.git文件存在。
如果存在就直接返回这个路径下的git操作对象, 不存在就会 新建一个这个路径,然后在下面生成.git文件
我们先把这个文件夹删除删除掉。让他重新创建一个。
这个就会创建一个这样的目录,并且生成.git文件。并且 设置远程仓库地址
并且会执行git clone 指令,把整个 仓库克隆到本地仓库。
这个时候,这个路径下有 整个配置文件的config文件夹了。
/var/folders/tb/w7qkq1w54w3g25_3_jp0w6500000gn/T/config-repo-8553408175024803312/config :
拉取完配置文件之后,判断是否需要更新,如果需要的话,就会先执行 git fetch指令,
git checkout 切换分支 到 我们接口请求对应的分支下。
然后执行 merge指令, 把文件更新到最新的状态。
这个时候,服务端 已经把所有最新的配置文件 拉取到本地仓库了。
然后返回getLocations 返回 本地仓库的地址,以及服务名,环境,分支,版本号。
然后会把对应服务的配置文件 放到 enviroment对象 里的PropertySources 数组中,返回enviroment对象。
再把Environment对象 , 本地仓库路径 替换成git仓库地址,变成这样。然后返回
最后,从environment对象里获取 micro-order-dev.properties里的内容,把map变成 字符串形式,返回出去。
2.4、服务端 本地配置仓库 初始化
当然,配置中心服务端 不会只等到 被请求接口了才会 去git拉取 配置文件到本地仓库,而是在启动完成之后会由线程池 定时触发 健康检测。在健康检测里,就会从git 更新 配置文件到本地仓库。
走的也是 请求git,创建本地仓库,git clone, 然后merge更新。
3、配置中心客户端
配置中心客户端 会在启动的时候请求 配置中心服务端 获取 配置信息, 然后塞到自己的environment 对象的propertySource属性中。
3.1、源码入口
同样是spi。
Maven: org.springframework.cloud:spring-cloud-config-client:2.0.0.RELEASE2包下的META-INF/spring.factories。
3.1.1、加载 key为 BootstrapConfiguration的类
只不过这里配置的key是BootstrapConfiguration类型的,springboot启动的时候默认加载的是key为EnableAutoConfiguration的类。
key为BootstrapConfiguration的类 是由监听器加载的。
有个 BootstrapApplicationListener 监听器类,实现了ApplicationListener接口,当Spring容器发布 ApplicationEnvironmentPreparedEvent 事件时,会调用实现的onApplicationEvent方法。
这里就会加载META-INF/Spring.factories 下配置的key为 BootstrapConfiguration类型的类。
关于事件是啥时候发布的,可以看一下调用链,也是在Springboot的run方法里调用的。
3.1.2、ConfigServicePropertySourceLocator实例和ConfigClientProperties实例的注册
META-INF/spring.factories文件里配置了 key为BootstrapConfiguration 的有ConfigServiceBootstrapConfiguration类
ConfigServiceBootstrapConfiguration类会用@Bean注册ConfigServicePropertySourceLocator类 ,并且还会在方法列表注入配置中心客户端的配置对象ConfigClientProperties对象
ConfigServicePropertySourceLocator实现PropertySourceLocator接口,实现locate方法。
ConfigClientProperties对象加载 spring.cloud.config为前缀的配置信息
3.1.3、ConfigServicePropertySourceLocator类locate()的调用时机
之后Springboot在run方法里面的prepareContext方法会调用所有 ApplicationContextInitializer接口实例的 initialize方法。
有一个ApplicationContextInitializer接口实现类 PropertySourceBootstrapConfiguration,会注入PropertySourceLocator接口类型的实例,也就是ConfigServicePropertySourceLocator实例。
然后会在 initialize方法里调用所有 PropertySourceLocator接口类型 的实例的locate方法。也就会调用到 ConfigServicePropertySourceLocator的locate方法。
3.1.4、ConfigServicePropertySourceLocator的locate()去配置中心服务端 拉取配置信息
3.1.4.1、去配置中心服务端 拉取配置信息
ConfigServicePropertySourceLocator的locate方法,去配置中心服务端 拉取配置信息。
先创建一个restTemplate对象,getRemoteEnvironment()
在getRemoteEnvironment()方法里,先拼接请求的接口路径
是 /{name}/{profile}/{label}
替换变量就是 /micro-user/dev/master 接口
1、配置中心服务端uri的获取
在我们的配置里,一般都是配serviceId的。
spring.cloud.config.discovery.enabled=true
spring.cloud.config.discovery.serviceId=config-server
所以需要根据服务名称 从 注册中心 获取到 配置中心服务端的 ip+端口。
Springcloud eureka客户端从注册中心 拉取完服务列表里,会发布HeartbeatEvent事件。
SpringCloud之Eureka客户端源码解析 这篇有介绍,这里就简单带过了。
这里发布的是HeartbeatEvent事件。
有一个监听 HeartbeatEvent事件的 ApplicationListenerMethodAdapter类,在响应事件时,会反射调用org.springframework.cloud.config.client.DiscoveryClientConfigServiceBootstrapConfiguration类的 heartbeat方法
DiscoveryClientConfigServiceBootstrapConfiguration 会注入ConfigClientProperties 配置中心客户端配置类ConfigClientProperties对象
在DiscoveryClientConfigServiceBootstrapConfiguration的heartbeat方法,会调用refresh()
refresh()则会从配置中心客户端配置类ConfigClientProperties对象 里拿到我们配置的配置中心服务端的服务名称, 然后从 拉取到的服务列表里,获取 这个服务名称下对应的服务实例。
拿到所有服务实例之后, 遍历获取 ip+端口,放到一个url的列表里,最后设置到 配置中心客户端配置类ConfigClientProperties对象 的uri 属性中。
所以,后面 配置中心 客户端 直接就可以从 配置中心客户端配置类ConfigClientProperties对象里的 uri数组里,获取到 ip+端口, 发起请求就行了。
2、发起请求
**遍历 配置中心客户端配置类ConfigClientProperties对象里的 uri数组, 对 每个 配置中心服务端的 ip+端口进行 接口请求。如果有一个配置中心服务端成功返回了 它的配置信息,就返回。 **
先 把用户名,密码 用base64 加密 方法请求头Authorization里,
最后向配置中心服务端http://192.168.31.211:9101/{name}/{profile}/{label} 发GET请求,获取对应的配置信息
返回的reponse里的Body是一个Environment对象,里面的propertiesSource 列表就是 这个服务dev环境从master分支 对应 的 配置信息。
最后return,跳出对 配置中心服务端的 uri数组的循环。
3.1.4.2、设置到本地的 Environment对象的PropertySource中
获取到Environment对象之后,加到name = configService 的CompositePropertySource对象中,
最终返回的CompositePropertySource对象是这样的,里面有一个propertySources列表
就是从配置中心服务端拉到的 配置信息
然后把新返回的 CompositePropertySource对象 再添加到name = bootstrapProperties的CompositePropertySource对象的propertySource列表中,
最终添加到 插入到 配置中心客户端的 environment对象中
最后environment的propertySource就有 从配置中心服务端拉取的配置信息了。
后面其他的bean实例化,就可以 从 environment里面 获取 从配置中心服务端拉取的配置信息,注入到需要使用的地方了。
4、配置信息的刷新
如果我们在git里的配置信息的配置信息发生改变,SpringCloud提供了 配置中心服务端 刷新配置信息的功能。
有以下两种方式 :
- 手动调用每台服务 actuator组件提供的 /actuator/refresh 接口。
- 引入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方法
当我们调用 /actuator/refresh接口,就会进到这里。进入contextRefresher的refresh()
先把当前的Environment对象的PropertySources列表保存。然后调用addConfigFilesToEnvironment方法。
addConfigFilesToEnvironment方法里 会 重新 创建一个SpringApplication类,
- 指定environment为当前Spring上下文environment对象的备份。PropertySources的引用是相同的。
- 要加载的source类 是 Empty类, 是个空的类。
- 并且指定 WebApplicationType.NONE,不启动spring里的web容器相关的东西。
- 并且手动加入 BootstrapApplicationListener监听器类,注意,这个类 是会 加载META-INF/spring.factories 文件里配置的 key为 BootstrapConfiguration的类的。
之后调用SpringApplication的run方法。
解读一下这是要干嘛。其实就是 启动了一个特别轻量级的springboot容器。什么功能也不启用,web相关的东西也不启用。 然后run的时候,就会加载 SPI里配置的相关的类。其他什么bean都不注册,因为他传入的class是个空的类。
新的Spring容器里要实例化的bean也就只有这么几个spi导入进来的
加载spi之后,那么肯定就会加载到 配置中心客户端 的 ConfigServicePropertySourceLocator类, 然后又可以借用 ApplicationContextInitializer接口实例 去调用PropertySourceLocator接口的实现类onfigServicePropertySourceLocator类的 locate()方法,重新从配置中心服务端 拉取配置。
拉取完配置之后,传入的environment对象的propertySources对象 就又有 从配置中心服务端 拉取的配置了。
新创建的spring容器 的 environment对象:
最后返回这个Spring上下文对象。
返回之后,就是 与之前的 Environment对象里PropertySources集合进行比较交换。把更改后的配置信息 更新到 当前spring上下文的Environment对象里。
由于刚才新创建并启动的Spring容器传入的 Environment对象里 的 propertySources 对象 的引用 就是 当前spring上下文的Environment对象里的。所以就直接用 当前spring上下文的Environment对象里的propertySources对象进行比较。
然后返回 有改动的 配置文件的key的 集合。
到这里, Environment对象里 的 propertySources 已经被替换成最新拉取的配置了。
4.2、刷新作用域为@RefreshScope 的bean
更新完 Environment对象里 的 propertySources之后,下面还有一行很重要的代码。
刷新 作用域为 @RefreshScope的所有bean。
刷新其实就是把他们全部都销毁了。
直接把@RefreshScope作用域里存放bean的 缓存清了。
作用域Scope接口
作用域其实就是Scope接口的实现类 来实现 管理bean的方式。
Spring在获取bean的时候,会调用 Scope接口的实现类的get方法,来获取bean。并传入ObjectFactory对象。
Spring创建bean:
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方法,
会往BeanLifecycleWrapperCache进行缓存。
然后返回BeanLifecycleWrapper.get方法,判断bean是不是实例化,没有实例化就调用ObjectFactory的getObject方法, 实例化了就直接返回。
bean的销毁
所以现在我们调用 /actuator/refresh接口刷新配置时,就把 RefreshScope作用域里的缓存全部都清了, 下次这些bean 被用到时,进RefreshScope的get方法 ,缓存里没有, 那么就会重新实例化这些bean。重新这些实例化bean,那么这些bean里用到 @value的地方 就会从 最新的environment对象里获取最新的配置,注入进去, 以达到 刷新配置的效果。