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

简介: 不知你是否跟我一样,在刚开始使用SpringCloud配置中心的时候也有很多的疑惑:- SpringCloud是什么时候去拉取配置中心的?- 配置中心客户端的配置信息为什么要写在bootstrap文件中?- 对象中注入的属性是如何动态刷新的?- 一些开源的配置中心是如何整合SpringCloud的?- ...本文就通过探讨上述问题来探秘SpringCloud配置中心核心的底层原理。

大家好,我是三友~~

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

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

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

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

微信公众号:三友的java日记

从SpringBoot的启动过程说起

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

image.png

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

image.png

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

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

image.png

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

image.png

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

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

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

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

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

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

image.png

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

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

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

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

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

准备Environment的核心操作

上一节得出一个结论

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

image.png

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

前置操作

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

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

image.png

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

image.png

SpringApplicationRunListener仅仅只有一个实现EventPublishingRunListener

image.png

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

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

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

image.png

prepareEnvironment的核心逻辑

接着来讲一下prepareEnvironment方法

image.png

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

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

listeners.environmentPrepared(environment);

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

EventPublishingRunListener#environmentPrepared

image.png

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

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

  • ConfigFileApplicationListener
  • BootstrapApplicationListener

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

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

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

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

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

image.png

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

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

image.png

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

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

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

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

image.png

默认就是bootstrap

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

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

BootstrapImportSelectorConfiguration

image.png

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

BootstrapImportSelector

image.png

BootstrapImportSelector实现了(间接)ImportSelector接口

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

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

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

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

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

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

image.png

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

image.png

PropertySourceBootstrapConfiguration

PropertySourceBootstrapConfiguration

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

PropertySourceLocator

image.png

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

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,获取配置中心中的配置

image.png

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

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

小总结

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

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

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

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

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

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

image.png

如何动态刷新Bean的属性?

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

@RefreshScope

image.png

重点来了

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

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

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

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

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

image.png

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

获取到的username属性值自然也就是第二个Bean中的username值

那么为什么要生成两个Bean?

接着往下瞅

在SpringCloud中有这么一项规定

当配置中心客户端一旦感知到服务端的某个配置有变化的时候,需要发布一个RefreshEvent事件来告诉SpringCloud配置有变动

image.png

在SpringCloud中RefreshEventListener类会去监听这个事件

image.png

一旦监听到这个事件,SpringCloud会再次从配置中心拉取配置

这个拉取配置的核心逻辑跟启动时拉取配置的核心逻辑是一样的

也是通过 BootstrapApplicationListener 来实现的

image.png

这部分代码逻辑在ContextRefresher类中,顺着RefreshEventListener就能看到,有兴趣可以扒一扒

怕你忘了,我再把上面拉取配置的图拿过来

image.png

有了最新的配置之后,就会进行一步骚操作来移花接木”刷新“注入到对象的属性

这个骚操作就是销毁所有的前面提到的第二个Bean,但是第一个Bean,也就是代理对象保持不变

image.png

当程序运行调用代理对象的方法的时候,发现第二个Bean没有了,此时他就会去重新创建第二个Bean,也就是重新创建一个UserService对象

由于此时已经拉到最新的配置了,也就是这个被重新创建的UserService对象注入的就是最新的属性

image.png

之后再调用的这个新创建的第二个Bean,拿到的自然就是最新的配置

所以,给你的感觉是对象的属性发生了变化,实际上是真正被调用的对象重新创建了

所以这招移花接木还是有点意思的!

小总结

其实到这就弄明白了Bean的属性动态刷新的原理

其实就是当配置中心客户端发现服务端的配置有变化,需要发送一个RefreshEvent事件来告诉SpringCloud配置有变动

SpringCloud会去监听这个事件,按照项目启动的方式重新拉取配置中心最新的属性配置

当拉取完属性配置之后,就会销毁所有的第二个Bean,也就是真正被使用的Bean

之后当第一个Bean(动态代理的Bean)需要使用这个第二个Bean时,就会重新创建这个第二个Bean

此时由于已经有最新的配置了,那么创建的这个第二个Bean就会被注入最新的属性,这样就实现了属性的”刷新“

image.png

补充个东西:@RefreshScope的秘密

上面大致说了@RefreshScope动态刷新的原理

这里我补充一下@RefreshScope代码层面的实现原理

本来这部分原理我是写在前面的,但是我发现这块比较绕,怕打断文章的节奏,所以就准备删除了

但是想想既然都写了,那么就给放到补充里面吧,看不懂也不耽误前面的理解

image.png

这个注解是个衍生注解,真正起作用的就是@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的实现

image.png

这个最终会生成一个代理对象,上面的例子就是为UserService生成一个代理对象,并且由于是单例的,所以最终这个对象会被放到一级缓存中,我们使用时注入的也就是这个对象

第二个Bean的class是UserService,所以生成的就是真正的UserService对象,但是由于scope为refresh,所以不会存在第一级缓存中

这部分注册两个Bean的代码是在ScopedProxyUtils#createScopedProxy方法中,有兴趣的可以扒扒

再来讲一讲保存的地方不同

不同的作用域都需要实现一个Scope接口来存放对应的Bean

image.png

比如refresh、session作用域都有对应的实现

image.png

也就是通过Scope就可以管理不同作用域的Bean

所以,对于refresh这个作用域来说,他的所有的Bean都在RefreshScope中

后面说的销毁,只需要移除RefreshScope中的Bean就可以了

image.png

代码也在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文件

image.png

NacosConfigBootstrapConfiguration是用来实现第一点的

image.png

除了Nacos自己的一些Bean,他还声明了一个NacosPropertySourceLocator这个Bean

image.png

这个Bean就实现了PropertySourceLocator接口

第二点的实现就是通过NacosConfigAutoConfiguration配置类来实现的

这里面有这么一个Bean

image.png

这个Bean就实现了配置变化发送事件的操作

image.png

除了Nacos,比如说Consul作为配置中心的时候也是这么一套实现逻辑

但是值的注意的是,像Apollo配置中心,他并没有适配SpringCloud这套规范

当然,如果你有兴趣,可以自己实现Apollo适配SpringCloud这套规范

ok,本文就讲到这里,如果觉得对你有点帮助,欢迎点赞、在看、收藏、转发分享给其他需要的人,灰常滴感谢!

往期热门文章推荐

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

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

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

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

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

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

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

扫码或者搜索关注公众号 三友的java日记 ,及时干货不错过,公众号致力于通过画图加上通俗易懂的语言讲解技术,让技术更加容易学习,回复 面试 即可获得一套面试真题。

相关文章
|
1月前
|
安全 Java Spring
Spring之Aop的底层原理
Spring之Aop的底层原理
|
2月前
|
设计模式 前端开发 Java
【深入浅出Spring原理及实战】「夯实基础系列」360全方位渗透和探究SpringMVC的核心原理和运作机制(总体框架原理篇)
【深入浅出Spring原理及实战】「夯实基础系列」360全方位渗透和探究SpringMVC的核心原理和运作机制(总体框架原理篇)
32 0
|
2月前
|
XML Java Shell
【深入浅出Spring原理及实战】「夯实基础系列」360全方位渗透和探究Spring的核心注解开发和实现指南(Spring5的常见的注解)
【深入浅出Spring原理及实战】「夯实基础系列」360全方位渗透和探究Spring的核心注解开发和实现指南(Spring5的常见的注解)
20 1
|
2月前
|
安全 Java 数据安全/隐私保护
【深入浅出Spring原理及实战】「EL表达式开发系列」深入解析SpringEL表达式理论详解与实际应用
【深入浅出Spring原理及实战】「EL表达式开发系列」深入解析SpringEL表达式理论详解与实际应用
73 1
|
2月前
|
XML 存储 缓存
【深入浅出Spring原理及实战】「缓存Cache开发系列」带你深入分析Spring所提供的缓存Cache管理器的实战开发指南(修正篇)
【深入浅出Spring原理及实战】「缓存Cache开发系列」带你深入分析Spring所提供的缓存Cache管理器的实战开发指南(修正篇)
29 0
|
4天前
|
Java 开发者 微服务
Spring Cloud原理详解
【5月更文挑战第4天】Spring Cloud是Spring生态系统中的微服务框架,包含配置管理、服务发现、断路器、API网关等工具,简化分布式系统开发。核心组件如Eureka(服务发现)、Config Server(配置中心)、Ribbon(负载均衡)、Hystrix(断路器)、Zuul(API网关)等。本文讨论了Spring Cloud的基本概念、核心组件、常见问题及解决策略,并提供代码示例,帮助开发者更好地理解和实践微服务架构。此外,还涵盖了服务通信方式、安全性、性能优化、自动化部署、服务网格和无服务器架构的融合等话题,揭示了微服务架构的未来趋势。
28 6
|
8天前
|
负载均衡 Java 开发者
Spring Cloud:一文读懂其原理与架构
Spring Cloud 是一套微服务解决方案,它整合了Netflix公司的多个开源框架,简化了分布式系统开发。Spring Cloud 提供了服务注册与发现、配置中心、消息总线、负载均衡、熔断机制等工具,让开发者可以快速地构建一些常见的微服务架构。
|
13天前
|
负载均衡 算法
SpringCloud&Ribbon负载均衡原理与实践
SpringCloud&Ribbon负载均衡原理与实践
19 3
|
14天前
|
安全 Java API
Spring工厂API与原理
Spring工厂API与原理
35 10
|
14天前
|
Java Nacos 开发者
Java从入门到精通:4.2.1学习新技术与框架——以Spring Boot和Spring Cloud Alibaba为例
Java从入门到精通:4.2.1学习新技术与框架——以Spring Boot和Spring Cloud Alibaba为例