8000字+22张图探秘SpringCloud配置中心的核心原理
如何动态刷新Bean的属性?
我们都知道,要想实现配置属性的动态刷新,需要在类上加上一个注解
@RefreshScope
重点来了
加了@RefreshScope注解的Bean,就拿上图中的UserService举例
Spring在生成的时候会生成两个UserService的Bean:
- 第一个是UserService的代理动态代理的Bean,后面我称为第一个Bean
- 第二个就是UserService这个Bean,后面我称为第二个Bean
当你在其它类中需要注入一个UserService时,真正注入的是第一个Bean,也就是动态代理的Bean
当你使用这个注入的动态代理的Bean的时候,他会去找第二个Bean,也就是真正的UserService这个Bean,然后调用对应的方法
比如你调用注入的UserService代理对象的getUsername
方法,最终就会调用到第二个BeangetUsername
方法
获取到的username属性值自然也就是第二个Bean中的username值
那么为什么要生成两个Bean?
接着往下瞅
在SpringCloud中有这么一项规定
当配置中心客户端一旦感知到服务端的某个配置有变化的时候,需要发布一个RefreshEvent事件来告诉SpringCloud配置有变动
在SpringCloud中RefreshEventListener类会去监听这个事件
一旦监听到这个事件,SpringCloud会再次从配置中心拉取配置
这个拉取配置的核心逻辑跟启动时拉取配置的核心逻辑是一样的
也是通过 BootstrapApplicationListener 来实现的
这部分代码逻辑在ContextRefresher类中,顺着RefreshEventListener就能看到,有兴趣可以扒一扒
怕你忘了,我再把上面拉取配置的图拿过来
有了最新的配置之后,就会进行一步骚操作来移花接木”刷新“注入到对象的属性
这个骚操作就是销毁所有的前面提到的第二个Bean,但是第一个Bean,也就是代理对象保持不变
当程序运行调用代理对象的方法的时候,发现第二个Bean没有了,此时他就会去重新创建第二个Bean,也就是重新创建一个UserService对象
由于此时已经拉到最新的配置了,也就是这个被重新创建的UserService对象注入的就是最新的属性了
之后再调用的这个新创建的第二个Bean,拿到的自然就是最新的配置
所以,给你的感觉是对象的属性发生了变化,实际上是真正被调用的对象重新创建了
所以这招移花接木还是有点意思的!
小总结
其实到这就弄明白了Bean的属性动态刷新的原理
其实就是当配置中心客户端发现服务端的配置有变化,需要发送一个RefreshEvent事件来告诉SpringCloud配置有变动
SpringCloud会去监听这个事件,按照项目启动的方式重新拉取配置中心最新的属性配置
当拉取完属性配置之后,就会销毁所有的第二个Bean,也就是真正被使用的Bean
之后当第一个Bean(动态代理的Bean)需要使用这个第二个Bean时,就会重新创建这个第二个Bean
此时由于已经有最新的配置了,那么创建的这个第二个Bean就会被注入最新的属性,这样就实现了属性的”刷新“
补充个东西:@RefreshScope的秘密
上面大致说了@RefreshScope动态刷新的原理
这里我补充一下@RefreshScope代码层面的实现原理
本来这部分原理我是写在前面的,但是我发现这块比较绕,怕打断文章的节奏,所以就准备删除了
但是想想既然都写了,那么就给放到补充里面吧,看不懂也不耽误前面的理解
这个注解是个衍生注解,真正起作用的就是@Scope注解
@Scope注解并不陌生,他其实是定义Bean的作用域
比如多例(原型),就可以加上@Scope("prototype")注解
还有一些八股文常背的作用域,比如session作用域等等
而@RefreshScope也可以看做是一种Bean的作用域,名字叫做refresh
这些除了单例和多例之外的作用域的底层实现逻辑都是一样的
这些带有作用域的Bean相比于普通的单例Bean主要有以下几点不同:
- 会注册两个Bean,这个前面已经提到过
- 保存的地方不同,比如单例Bean最终会存在三级缓存中的第一级缓存中,而不同作用域的Bean是存在不同的地方的
先说会注册两个Bean,还是以前面提到的UserService举个例子,这两个Bean分别是
- 第一个Bean的Bean名称为
userService
,Bean class为ScopedProxyFactoryBean.class
,这个scope为默认,也就是单例 - 第二个Bean的Bean名称为
scopedTarget.userService
,Bean class为UserService.class
,scope为refresh(如果是session作用域就是session)
第一个Bean的class为ScopedProxyFactoryBean,是个FactoryBean的实现
这个最终会生成一个代理对象,上面的例子就是为UserService生成一个代理对象,并且由于是单例的,所以最终这个对象会被放到一级缓存中,我们使用时注入的也就是这个对象
第二个Bean的class是UserService,所以生成的就是真正的UserService对象,但是由于scope为refresh,所以不会存在第一级缓存中
这部分注册两个Bean的代码是在ScopedProxyUtils#createScopedProxy方法中,有兴趣的可以扒扒
再来讲一讲保存的地方不同
不同的作用域都需要实现一个Scope
接口来存放对应的Bean
比如refresh、session作用域都有对应的实现
也就是通过Scope
就可以管理不同作用域的Bean
所以,对于refresh这个作用域来说,他的所有的Bean都在RefreshScope中
后面说的销毁,只需要移除RefreshScope中的Bean就可以了
代码也在ContextRefresher类中
开源配置中心是如何整合SpringCloud的?
首先我们再来梳理一下拉取配置和刷新配置的核心关键点
拉取配置关键点就是项目启动的时候(也包括重新拉取配置),会去创建一个容器
这个容器只读取bootstrap配置文件和spring.factories中的键为org.springframework.cloud.bootstrap.BootstrapConfiguration
对应的配置类
之后会获取这个容器中的PropertySourceLocator,从而获取配置中心的配置
刷新配置关键点就是一旦配置中心配置变动,就需要发送RefreshEvent事件,之后一系列刷新操作都是由SpringCloud的来完成的
所以,配置中心整合到SpringCloud其实就很简单,就两点
第一点就是需要实现PropertySourceLocator,并且配置中心一些相关的Bean需要通过org.springframework.cloud.bootstrap.BootstrapConfiguration
来装配到这个容器中
第二点,当配置发生变更需要发送RefreshEvent事件,这部分配置中心一些相关的Bean配置肯定是需要通过自动装配来完成
有了这两点我们来看看Nacos作为配置中心是如何整合到SpringCloud的
我们直接看Nacos的spring.factories文件
NacosConfigBootstrapConfiguration是用来实现第一点的
除了Nacos自己的一些Bean,他还声明了一个NacosPropertySourceLocator这个Bean
这个Bean就实现了PropertySourceLocator接口
第二点的实现就是通过NacosConfigAutoConfiguration配置类来实现的
这里面有这么一个Bean
这个Bean就实现了配置变化发送事件的操作
除了Nacos,比如说Consul作为配置中心的时候也是这么一套实现逻辑
但是值的注意的是,像Apollo配置中心,他并没有适配SpringCloud这套规范
当然,如果你有兴趣,可以自己实现Apollo适配SpringCloud这套规范
ok,本文就讲到这里,如果觉得对你有点帮助,欢迎点赞、在看、收藏、转发分享给其他需要的人,灰常滴感谢!
文章转载自公众号:三友的java日记