Spring Cloud 是如何实现热更新的

标签: spring cloud 更新 | 发表时间:2017-07-25 20:12 | 作者:
出处:http://www.scienjus.com/

作为一篇源码分析的文章,本文虽然介绍 Spring Cloud 的热更新机制,但是实际全文内容都不会与 Spring Cloud Config 以及 Spring Cloud Bus 有关,因为前者只是提供了一个远端的配置源,而后者也只是提供了集群环境下的事件触发机制,与核心流程均无太大关系。

ContextRefresher

顾名思义, ContextRefresher 用于刷新 Spring 上下文,在以下场景会调用其 refresh 方法。

  1. 请求 /refresh Endpoint。
  2. 集成 Spring Cloud Bus 后,收到 RefreshRemoteApplicationEvent 事件(任意集成 Bus 的应用,请求 /bus/refresh Endpoint 后都会将事件推送到整个集群)。

这个方法包含了整个刷新逻辑,也是本文分析的重点。

首先看一下这个方法的实现:

     
1
2
3
4
5
6
7
8
9
10
     
public synchronized Set<String> refresh() {
Map<String, Object> before = extract(
this.context.getEnvironment().getPropertySources());
addConfigFilesToEnvironment();
Set<String> keys = changes(before,
extract(this.context.getEnvironment().getPropertySources())).keySet();
this.context.publishEvent(new EnvironmentChangeEvent(keys));
this.scope.refreshAll();
return keys;
}

首先是第一步 extract,这个方法接收了当前环境中的所有属性源(PropertySource),并将其中的非标准属性源的所有属性汇总到一个 Map 中返回。

这里的标准属性源指的是 StandardEnvironmentStandardServletEnvironment,前者会注册系统变量(System Properties)和环境变量(System Environment),后者会注册 Servlet 环境下的 Servlet Context 和 Servlet Config 的初始参数(Init Params)和 JNDI 的属性。个人理解是因为这些属性无法改变,所以不进行刷新。

第二步 addConfigFilesToEnvironment 是核心逻辑,它创建了一个新的 Spring Boot 应用并初始化:

     
1
2
3
4
5
6
7
8
9
     
SpringApplicationBuilder builder = new SpringApplicationBuilder(Empty.class)
.bannerMode(Banner.Mode.OFF).web(false).environment(environment);
// Just the listeners that affect the environment (e.g. excluding logging
// listener because it has side effects)
builder.application()
.setListeners(
Arrays.asList(new BootstrapApplicationListener(),
new ConfigFileApplicationListener()));
capture = builder.run();

这个应用只是为了重新加载一遍属性源,所以只配置了 BootstrapApplicationListenerConfigFileApplicationListener,最后将新加载的属性源替换掉原属性源,至此属性源本身已经完成更新了。

此时属性源虽然已经更新了,但是配置项都已经注入到了对应的 Spring Bean 中,需要重新进行绑定,所以又触发了两个操作:

  1. 将刷新后发生更改的 Key 收集起来,发送一个 EnvironmentChangeEvent 事件。

  2. 调用 RefreshScope.refreshAll 方法。

EnvironmentChangeEvent

在上文中, ContextRefresher 发布了一个 EnvironmentChangeEvent 事件,接下来看看这个事件产生了哪些影响。

The application will listen for an EnvironmentChangeEvent and react to the change in a couple of standard ways (additional ApplicationListeners can be added as @Beans by the user in the normal way). When an EnvironmentChangeEvent is observed it will have a list of key values that have changed, and the application will use those to:

  1. Re-bind any @ConfigurationProperties beans in the context

  2. Set the logger levels for any properties in logging.level.*

官方文档的介绍中提到,这个事件主要会触发两个行为:

  1. 重新绑定上下文中所有使用了 @ConfigurationProperties 注解的 Spring Bean。
  2. 如果 logging.level.* 配置发生了改变,重新设置日志级别。

这两段逻辑分别可以在 ConfigurationPropertiesRebinderLoggingRebinder 中看到。

ConfigurationPropertiesRebinder

这个类乍一看代码量特别少,只需要一个 ConfigurationPropertiesBeans 和一个 ConfigurationPropertiesBindingPostProcessor,然后调用 rebind 每个 Bean 即可。但是这两个对象是从哪里来的呢?

     
1
2
3
4
5
     
public void rebind() {
for (String name : this.beans.getBeanNames()) {
rebind(name);
}
}

ConfigurationPropertiesBeans 需要一个 ConfigurationBeanFactoryMetaData, 这个类逻辑很简单,它是一个 BeanFactoryPostProcessor 的实现,将所有的 Bean 都存在了内部的一个 Map 中。

而 ConfigurationPropertiesBeans 获得这个 Map 后,会查找每一个 Bean 是否有 @ConfigurationProperties 注解,如果有的话就放到自己的 Map 中。

绕了一圈好不容易拿到所有需要重新绑定的 Bean 后,绑定的逻辑就要简单许多了:

     
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
     
public boolean rebind(String name) {
if (!this.beans.getBeanNames().contains(name)) {
return false;
}
if (this.applicationContext != null) {
try {
Object bean = this.applicationContext.getBean(name);
if (AopUtils.isCglibProxy(bean)) {
bean = getTargetObject(bean);
}
this.binder.postProcessBeforeInitialization(bean, name);
this.applicationContext.getAutowireCapableBeanFactory()
.initializeBean(bean, name);
return true;
}
catch (RuntimeException e) {
this.errors.put(name, e);
throw e;
}
}
return false;
}

其中 postProcessBeforeInitialization 方法将 Bean 重新绑定了所有属性,并做了校验等操作。

initializeBean 的实现如下:

     
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
     
protected Object initializeBean(final String beanName, final Object bean, RootBeanDefinition mbd) {
Object wrappedBean = bean;
if(mbd == null || !mbd.isSynthetic()) {
wrappedBean = this.applyBeanPostProcessorsBeforeInitialization(bean, beanName);
}
try {
this.invokeInitMethods(beanName, wrappedBean, mbd);
} catch (Throwable var6) {
throw new BeanCreationException(mbd != null?mbd.getResourceDescription():null, beanName, "Invocation of init method failed", var6);
}
if(mbd == null || !mbd.isSynthetic()) {
wrappedBean = this.applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
}
return wrappedBean;
}

其中主要做了三件事:

  1. applyBeanPostProcessorsBeforeInitialization:调用所有 BeanPostProcessorpostProcessBeforeInitialization 方法。
  2. invokeInitMethods:如果 Bean 继承了 InitializingBean,执行 afterPropertiesSet 方法,或是如果 Bean 指定了 init-method 属性,如果有则调用对应方法
  3. applyBeanPostProcessorsAfterInitialization:调用所有 BeanPostProcessorpostProcessAfterInitialization 方法。

之后 ConfigurationPropertiesRebinder 就完成整个重新绑定流程了。

LoggingRebinder

相比之下 LoggingRebinder 的逻辑要简单许多,它只是调用了 LoggingSystem 的方法重新设置了日志级别,具体逻辑就不在本文详述了。

RefreshScope

首先看看这个类的注释:

Note that all beans in this scope are only initialized when first accessed, so the scope forces lazy initialization semantics. The implementation involves creating a proxy for every bean in the scope, so there is a flag

If a bean is refreshed then the next time the bean is accessed (i.e. a method is executed) a new instance is created. All lifecycle methods are applied to the bean instances, so any destruction callbacks that were registered in the bean factory are called when it is refreshed, and then the initialization callbacks are invoked as normal when the new instance is created. A new bean instance is created from the original bean definition, so any externalized content (property placeholders or expressions in string literals) is re-evaluated when it is created.

这里提到了两个重点:

  1. 所有 @RefreshScope 的 Bean 都是延迟加载的,只有在第一次访问时才会初始化
  2. 刷新 Bean 也是同理,下次访问时会创建一个新的对象

再看一下方法实现:

     
1
2
3
4
     
public void refreshAll() {
super.destroy();
this.context.publishEvent(new RefreshScopeRefreshedEvent());
}

这个类中有一个成员变量 cache,用于缓存所有已经生成的 Bean,在调用 get 方法时尝试从缓存加载,如果没有的话就生成一个新对象放入缓存,并通过 getBean 初始化其对应的 Bean:

     
1
2
3
4
5
6
7
8
9
10
11
12
13
14
     
public Object get(String name, ObjectFactory<?> objectFactory) {
if (this.lifecycle == null) {
this.lifecycle = new StandardBeanLifecycleDecorator(this.proxyTargetClass);
}
BeanLifecycleWrapper value = this.cache.put(name,
new BeanLifecycleWrapper(name, objectFactory, this.lifecycle));
try {
return value.getBean();
}
catch (RuntimeException e) {
this.errors.put(name, e);
throw e;
}
}

所以在销毁时只需要将整个缓存清空,下次获取对象时自然就可以重新生成新的对象,也就自然绑定了新的属性:

     
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
     
public void destroy() {
List<Throwable> errors = new ArrayList<Throwable>();
Collection<BeanLifecycleWrapper> wrappers = this.cache.clear();
for (BeanLifecycleWrapper wrapper : wrappers) {
try {
wrapper.destroy();
}
catch (RuntimeException e) {
errors.add(e);
}
}
if (!errors.isEmpty()) {
throw wrapIfNecessary(errors.get(0));
}
this.errors.clear();
}

清空缓存后,下次访问对象时就会重新创建新的对象并放入缓存了。

而在清空缓存后,它还会发出一个 RefreshScopeRefreshedEvent 事件,在某些 Spring Cloud 的组件中会监听这个事件并作出一些反馈。

Zuul

Zuul 在收到这个事件后,会将自身的路由设置为 dirty 状态:

     
1
2
3
4
5
6
7
8
9
10
11
12
13
14
     
private static class ZuulRefreshListener implements ApplicationListener<ApplicationEvent> {
@Autowired
private ZuulHandlerMapping zuulHandlerMapping;
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ContextRefreshedEvent
|| event instanceof RefreshScopeRefreshedEvent
|| event instanceof RoutesRefreshedEvent) {
this.zuulHandlerMapping.setDirty(true);
}
}
}

并且当路由实现为 RefreshableRouteLocator 时,会尝试刷新路由:

     
1
2
3
4
5
6
     
public void setDirty(boolean dirty) {
this.dirty = dirty;
if (this.routeLocator instanceof RefreshableRouteLocator) {
((RefreshableRouteLocator) this.routeLocator).refresh();
}
}

当状态为 dirty 时,Zuul 会在下一次接受请求时重新注册路由,以更新配置:

     
1
2
3
4
5
6
7
8
     
if (this.dirty) {
synchronized (this) {
if (this.dirty) {
registerHandlers();
this.dirty = false;
}
}
}

Eureka

在 Eureka 收到该事件时,对于客户端和服务端都有不同的处理方式:

     
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
     
protected static class EurekaClientConfigurationRefresher {
@Autowired(required = false)
private EurekaClient eurekaClient;
@Autowired(required = false)
private EurekaAutoServiceRegistration autoRegistration;
@EventListener(RefreshScopeRefreshedEvent.class)
public void onApplicationEvent(RefreshScopeRefreshedEvent event) {
//This will force the creation of the EurkaClient bean if not already created
//to make sure the client will be reregistered after a refresh event
if(eurekaClient != null) {
eurekaClient.getApplications();
}
if (autoRegistration != null) {
// register in case meta data changed
this.autoRegistration.stop();
this.autoRegistration.start();
}
}
}

对于客户端来说,只是调用了下 eurekaClient.getApplications,理论上这个方法是没有任何效果的,但是查看上面的注释,以及联想到 RefreshScope 的延时初始化特性,这个方法调用应该只是为了强制初始化新的 EurekaClient

事实上这里很有趣的是,在 EurekaClientAutoConfiguration 中,实际为了 EurekaClient 提供了两种初始化方案,分别对应是否有 RefreshScope,所以以上的猜测应该是正确的。

而对于服务端来说, EurekaAutoServiceRegistration 会将服务端先标记为下线,在进行重新上线。

总结

至此,Spring Cloud 的热更新流程就到此结束了,从这些源码中可以总结出以下结论:

  1. 通过使用 ContextRefresher 可以进行手动的热更新,而不需要依靠 Bus 或是 Endpoint。
  2. 热更新会对两类 Bean 进行配置刷新,一类是使用了 @ConfigurationProperties 的对象,另一类是使用了 @RefreshScope 的对象。
  3. 这两种对象热更新的机制不同,前者在同一个对象中重新绑定了所有属性,后者则是利用了 RefreshScope 的缓存和延迟加载机制,生成了新的对象。
  4. 通过自行监听 EnvironmentChangeEvent 事件,也可以获得更改的配置项,以便实现自己的热更新逻辑。
  5. 在使用 Eureka 的项目中要谨慎的使用热更新,过于频繁的更新可能会使大量项目频繁的标记下线和上线,需要注意。

相关 [spring cloud 更新] 推荐:

大话 Spring Cloud

- - IT瘾-dev
研究了一段时间spring boot了准备向spirng cloud进发,公司架构和项目也全面拥抱了Spring Cloud. 在使用了一段时间后发现Spring Cloud从技术架构上降低了对大型系统构建的要求,使我们以非常低的成本(技术或者硬件)搭建一套高效、分布式、容错的平台,但Spring Cloud也不是没有缺点,小型独立的项目不适合使用.

Spring Cloud限流详解 | Spring Cloud|周立

- -
限流往往是一个绕不开的话题. 本文详细探讨在Spring Cloud中如何实现限流. Zuul上实现限流是个不错的选择,只需要编写一个过滤器就可以了,关键在于如何实现限流的算法. 常见的限流算法有漏桶算法以及令牌桶算法. https://www.cnblogs.com/LBSer/p/4083131.html,写得通俗易懂,你值得拥有,我就不拽文了.

Spring Cloud Kubernetes指南

- -
当我们构建微服务解决方案时,SpringCloud和Kubernetes都是最佳解决方案,因为它们为解决最常见的挑战提供组件. 但是,如果我们决定选择Kubernetes作为我们的解决方案的主要容器管理器和部署平台,我们仍然可以主要通过SpringCloudKubernetes项目使用SpringCloud的有趣特性.

Spring Cloud 是如何实现热更新的

- - ScienJus's Blog
作为一篇源码分析的文章,本文虽然介绍 Spring Cloud 的热更新机制,但是实际全文内容都不会与 Spring Cloud Config 以及 Spring Cloud Bus 有关,因为前者只是提供了一个远端的配置源,而后者也只是提供了集群环境下的事件触发机制,与核心流程均无太大关系. 顾名思义, ContextRefresher 用于刷新 Spring 上下文,在以下场景会调用其 refresh 方法.

Spring Cloud 快速入门

- - IT瘾-tuicool
Spring Cloud 是一套完整的微服务解决方案,基于 Spring Boot 框架,准确的说,它不是一个框架,而是一个大的容器,它将市面上较好的微服务框架集成进来,从而简化了开发者的代码量. 本课程由浅入深带领大家一步步攻克 Spring Cloud 各大模块,接着通过一个实例带领大家了解大型分布式微服务架构的搭建过程,最后深入源码加深对它的了解.

Deploy the spring cloud project using jenkins

- - Telami
先简单记录下Jenkins部署maven聚合工程要点. Root pom配置成项目根目录的pom.xml. maven命令单独install 欲构建的项目. 选项后可跟随{groupId}:{artifactId}或者所选模块的相对路径(多个模块以逗号分隔). 表示同时处理选定模块所依赖的模块. 表示同时处理依赖选定模块的模块.

Spring Cloud Gateway(限流) | Wind Mt

- -
限流可以保障我们的 API 服务对所有用户的可用性,也可以防止网络攻击. 一般开发高并发系统常见的限流有:限制总并发数(比如数据库连接池、线程池)、限制瞬时并发数(如 nginx 的 limit_conn 模块,用来限制瞬时并发连接数)、限制时间窗口内的平均速率(如 Guava 的 RateLimiter、nginx 的 limit_req 模块,限制每秒的平均速率);其他还有如限制远程接口调用速率、限制 MQ 的消费速率.

快速突击 Spring Cloud Gateway

- - 掘金后端
认识 Spring Cloud Gateway. Spring Cloud Gateway 是一款基于 Spring 5,Project Reactor 以及 Spring Boot 2 构建的 API 网关,是 Spring Cloud 微服务生态的主要组件之一. Spring Cloud Gateway 主要负责接口请求的路由分发,并且支持对请求的安全验证,流量监控和流量控制等扩展操作.

Spring Cloud Gateway2.0实践报告

- - 掘金后端
你的点赞就是对我最大的支持. 原创:小姐姐味道(微信公众号ID:xjjdog),欢迎分享,转载请保留出处. 本文将从知识拓扑讲起,谈一下api网关的功能,以及spring cloud gateway的使用方法. 一、知识拓扑 (使用和原理) 二、网关的作用 三、Predicate,路由匹配 四、Filter,过滤器编写 五、自定义过滤器 六、常见问题 复制代码.

微服务框架Spring Cloud介绍 Part2: Spring Cloud与微服务

- - skaka的博客
之前介绍过 微服务的概念与Finagle框架, 这个系列介绍Spring Cloud.. Spring Cloud还是一个相对较新的框架, 今年(2016)才推出1.0的release版本. 虽然Spring Cloud时间最短, 但是相比我之前用过的Dubbo和Finagle, Spring Cloud提供的功能最齐全..