8000字+42张图探秘SpringCloud配置中心的核心原理

大家好,我是三友~~

这篇文章来扒一扒SpringCloud配置中心的核心原理。

不知你是否跟我一样,在刚开始使用SpringCloud配置中心的时候也有很多的疑惑:

  • SpringCloud是什么时候去拉取配置中心的?
  • 配置中心客户端的配置信息为什么要写在bootstrap文件中?
  • 对象中注入的属性是如何动态刷新的?
  • 一些开源的配置中心是如何整合SpringCloud的?
  • ...

本文就通过探讨上述问题来探秘SpringCloud配置中心核心的底层原理。

从SpringBoot的启动过程说起

在SpringBoot启动的时候会经历一系列步骤,核心就是SpringApplication的run方法的逻辑

整个过程大致可以划分为三个阶段:

ApplicationContext刷新前阶段,这个阶段主要也干三件事

  • 准备Environment(注意我这里加粗了,你懂得),也就是准备SpringBoot的整个外部化配置的对象
  • 创建一个ApplicationContext
  • 为ApplicationContext做一些准备工作

ApplicationContext刷新阶段 ,这个阶段其实就是调用ApplicationContext#refresh方法来刷新容器

刷新的整个过程可以看我之前写的万字+20张图剖析Spring启动时12个核心步骤这篇文章

ApplicationContext刷新后阶段,这个阶段其实就是收尾的阶段,这个过程其实没有什么非常核心的事

ok,在说完上面这三个阶段之后,思考一个问题

你觉得在上面的三个阶段,哪个阶段最有可能从配置中心拉取配置?

其实稍微思考一下,肯定是想到的就是刷新前阶段

因为我已经明示了,准备Environment

玩笑归玩笑,为什么是这个阶段?

很好理解,因为这个阶段是准备Environment,也就是准备外部化配置

只需要在这个阶段加载配置中心的配置,放到Environment中,后面在整个ApplicationContext刷新阶段创建Bean的时候,就可以使用到配置中心的配置了

其实不光是配置中心的配置,比如配置文件的配置,也是在这里阶段读取的

至于如何实现的,我们接着往下瞅

准备Environment的核心操作

上一节得出一个结论

准备Environment,也就是prepareEnvironment方法的实现,是拉取配置的核心

不过在说这个方法之前,先来讲一下一些前置操作

前置操作

在SpringApplication创建的时候,会去加载spring.factories中的一些对象,其中就包括:

  • org.springframework.context.ApplicationListener键对应的ApplicationListener的实现
  • org.springframework.boot.SpringApplicationRunListener键对应的SpringApplicationRunListener的实现类

SpringApplicationRunListener仅仅只有一个实现EventPublishingRunListener

构造的时候会创建一个SimpleApplicationEventMulticaster,再将加载的ApplicationListener添加进去

SimpleApplicationEventMulticaster是用来发布事件用的,不清楚的话可以看三万字盘点Spring 9大核心基础功能这篇文章

按照传统,画张图来理一下这部分前置操作

prepareEnvironment的核心逻辑

接着来讲一下prepareEnvironment方法

这个方法会首先创建一个Environment对象

之后会执行这么一行方法,传入刚刚创建的Environment对象

listeners.environmentPrepared(environment);

这个方法最终会走到这个方法

EventPublishingRunListener#environmentPrepared

这个方法最终会发布一个ApplicationEnvironmentPreparedEvent事件

而对这个事件有两个特别重要的监听器:

  • ConfigFileApplicationListener
  • BootstrapApplicationListener

这些监听器都是通过前置操作从spring.factories配置文件中加载的

ConfigFileApplicationListener,用来处理配置文件的,他会解析配置文件的配置,放到Environment中

BootstrapApplicationListener这个跟本文探讨的主题相关了,它是用来专门来跟配置中心交互的

到这,我们就找到了SpringCloud配置中心配置拉取的整个入口逻辑

不过在分析BootstrapApplicationListener是如何从配置中心拉取配置的之前,先来张图总结一下这部分prepareEnvironment的操作

SpringCloud是如何巧妙地拉取配置的?

在BootstrapApplicationListener中,他首先也会创建一个SpringApplication去执行

其实本质上就是创建一个Spring容器,也就是ApplicationContext

这个容器非常重要,这个容器是专门用来跟配置中心交互的

这个容器在创建的时候会给它两个比较重要的配置

第一个就是设置这个容器所用的配置文件的名称

默认就是bootstrap

这就解释了为什么配置中心的配置信息需要写在bootstrap配置文件中

第二个就是会加入一个配置类

BootstrapImportSelectorConfiguration

这个配置类又会通过@Import注解导入另一个配置类

BootstrapImportSelector

BootstrapImportSelector实现了(间接)ImportSelector接口

那么这个容器在启动的时候,就会调用BootstrapImportSelector的selectImports方法的实现获取到一些配置类

而BootstrapImportSelector的selectImports实现从截图中也就可以看出

他会加载所有的spring.factories中的键为org.springframework.cloud.bootstrap.BootstrapConfiguration的配置类

其实这里@BootstrapConfiguration的作用其实跟@EnableAutoConfiguration的作用是差不多的,都是用来导入配置类的

所以,总的来说,这个用来跟配置中心交互的Spring容器最最主要就是干两件事:

  • 加载bootstrap配置文件
  • 加载所有的spring.factories中的键为org.springframework.cloud.bootstrap.BootstrapConfiguration对应的配置类

而在spring-cloud-context包下,@BootstrapConfiguration会导入一个很重要的配置类

PropertySourceBootstrapConfiguration

这个配置类中会注入这么一个集合对象

PropertySourceLocator

这个接口非常非常重要,先来看看注释

Strategy for locating (possibly remote) property sources for the Environment. Implementations should not fail unless they intend to prevent the application from starting.

我用我的四级英语功力给大家翻译一下

以一种策略的方式为Environment定位(可能是远程)属性配置(PropertySource)。实现不应该失败,除非打算阻止应用程序启动。

从这个翻译后的意思就是说,这个接口是用来定位,也就是说获取属性配置的

并且可能是远程告诉我们一个很重要的信息,那就是获取的配置信息不仅仅可以存在本地,而且还可以存在远程。

远程?作者这里就差直接告诉你可以从配置中心获取了。。

所以这个接口的作用就是用配置中心获取配置的!

那么自然而然不同的配置中心要想整合到SpringCloud就得实现这个接口

当注入完PropertySourceLocator集合之后,在某个阶段会调用所有的PropertySourceLocator,获取配置中心中的配置

之后在把这些配置放到Environment中

这样在ApplicationContext的刷新阶段就可以使用到配置中心的那些配置了

小总结

到这我们就弄明白了在项目启动中加载配置中心的配置了

其实就是项目在启动时会额外创建一个跟配置中心相关的Spring容器

这个容器会去加载bootstrap配置文件和所有的spring.factories中的键为org.springframework.cloud.bootstrap.BootstrapConfiguration对应的配置类

之后会去调用这个容器中所有的PropertySourceLocator对象,从配置中心获取配置

再放到Environment中就完成了启动时从配置中心获取配置的方式

最后,来张全家福概括一下前面整体的步骤

如何动态刷新Bean的属性?

我们都知道,要想实现配置属性的动态刷新,需要在类上加上一个注解

@RefreshScope

重点来了

加了@RefreshScope注解的Bean,就拿上图中的UserService举例

Spring在生成的时候会生成两个UserService的Bean:

  • 第一个是UserService的代理动态代理的Bean,后面我称为第一个Bean
  • 第二个就是UserService这个Bean,后面我称为第二个Bean

当你在其它类中需要注入一个UserService时,真正注入的是第一个Bean,也就是动态代理的Bean

当你使用这个注入的动态代理的Bean的时候,他会去找第二个Bean,也就是真正的UserService这个Bean,然后调用对应的方法

比如你调用注入的UserService代理对象的getUsername方法,最终就会调用到第二个Bean getUsername方法

获取到的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,本文就讲到这里,如果觉得对你有点帮助,欢迎点赞、在看、收藏、转发分享给其他需要的人,灰常滴感谢!

往期热门文章推荐

如何去阅读源码,我总结了18条心法

如何写出漂亮代码,我总结了45个小技巧

三万字盘点Spring/Boot的那些常用扩展点

三万字盘点Spring 9大核心基础功能

万字+20张图剖析Spring启动时12个核心步骤

1.5万字+30张图盘点索引常见的11个知识点

两万字盘点那些被玩烂了的设计模式

相关推荐
积水成江1 小时前
Vite+Vue3+SpringBoot项目如何打包部署
java·前端·vue.js·windows·spring boot·后端·nginx
CocoaAndYy2 小时前
ThreadLocal、InheritableThreadLocal、TransmittableThreadLocal原理及Demo
java·jvm·算法
原机小子3 小时前
SpringBoot在线教育系统:从零到一的构建过程
数据库·spring boot·后端
2401_857439693 小时前
SpringBoot在线教育平台:设计与实现的深度解析
java·spring boot·后端
总是学不会.3 小时前
SpringBoot项目:前后端打包与部署(使用 Maven)
java·服务器·前端·后端·maven
IT学长编程4 小时前
计算机毕业设计 视频点播系统的设计与实现 Java实战项目 附源码+文档+视频讲解
java·spring boot·毕业设计·课程设计·毕业论文·计算机毕业设计选题·视频点播系统
一 乐5 小时前
英语词汇小程序小程序|英语词汇小程序系统|基于java的四六级词汇小程序设计与实现(源码+数据库+文档)
java·数据库·小程序·源码·notepad++·英语词汇
曳渔5 小时前
Java-数据结构-反射、枚举 |ू・ω・` )
java·开发语言·数据结构·算法
laocooon5238578865 小时前
java 模拟多人聊天室,服务器与客户机
java·开发语言
风槐啊5 小时前
六、Java 基础语法(下)
android·java·开发语言