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 是如何实现热更新的

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

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

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

Spring Cloud各组件调优参数

- - Spring Cloud|周立
Spring Cloud整合了各种组件,每个组件往往还有各种参数. 本文来详细探讨Spring Cloud各组件的调优参数. 欢迎联系我的QQ: 511932633 或微信: jumping_me ,补充或者勘误,一起总结出最全、最实用的调优参数. hystrix.threadpool.default.maxQueueSize: -1 # 如该值为-1,那么使用的是SynchronousQueue,否则使用的是LinkedBlockingQueue.

周立/spring-cloud-yes - 码云 Gitee.com

- -
基于Spring Cloud的快速开发脚手架&最佳实践总结. Config Server与X. 如图,微服务集成Config Client,从而与Config Server进行通信,Config Server响应Config Client的请求,去Git仓库(当然也可以是SVN/Vauld/本地存储)获取配置文件,并返回给Config Client.

Dubbo将积极适配Spring Cloud生态,Spring Cloud体系或将成为微服务的不二选择!

- - 程序猿DD
2016年,我在博客中发表过一篇 《微服务架构的基础框架选择:Spring Cloud还是Dubbo. 在这篇文章中,我主要对比了Spring Cloud与Dubbo所具备的能力,并阐述了个人推崇Spring Cloud的原因. 但是,最近各大技术社区出现了不少类似的文章,观点比较激进,对于Spring Cloud的褒扬远胜于Dubbo,但是这些评价很多都忽略了Spring Cloud与Dubbo在设计视角上的不同.

doc/keycloak-learn/Spring Cloud Keycloak搭建手把手操作指南.md · 周立/spring-cloud-yes - 码云 Gitee.com

- -
http://www.keycloak.org/downloads.html,按需进行下载. 笔者下载的是“Standalone server distribution”. 安装Keycloak非常简单,步骤如下:. KEYCLOAK_PATH/bin,其中KEYCLOAK_PATH是您Keycloak的根目录.

Spring Cloud Netflix构建微服务入门实践

- - 简单之美
在使用Spring Cloud Netflix构建微服务之前,我们先了解一下Spring Cloud集成的Netflix OSS的基础组件Eureka,对于Netflix的其他微服务组件,像Hystrix、Zuul、Ribbon等等本文暂不涉及,感兴趣可以参考官网文档. 这里,我们用最基础的Eureka来构建一个最基础的微服务应用,来演示如何构建微服务,了解微服务的基本特点.

为Spring Cloud Ribbon配置请求重试(Camden.SR2+)

- - 程序猿DD
下面的例子,实现了对服务名为 hello-service的 /hello接口的调用. 由于 RestTemplate被 @LoadBalanced修饰,所以它具备客户端负载均衡的能力,当请求真正发起的时候,url中的服务名会根据负载均衡策略从服务清单中挑选出一个实例来进行访问. 大多数情况下,上面的实现没有任何问题,但是总有一些意外发生,比如:有一个实例发生了故障而该情况还没有被服务治理机制及时的发现和摘除,这时候客户端访问该节点的时候自然会失败.