轉(zhuǎn)自:http://www.scienjus.com/spring-cloud-refresh/
作為一篇源碼分析的文章之景,本文雖然介紹 Spring Cloud 的熱更新機(jī)制,但是實(shí)際全文內(nèi)容都不會(huì)與 Spring Cloud Config 以及 Spring Cloud Bus 有關(guān)脯丝,因?yàn)榍罢咧皇翘峁┝艘粋€(gè)遠(yuǎn)端的配置源壶唤,而后者也只是提供了集群環(huán)境下的事件觸發(fā)機(jī)制绞吁,與核心流程均無(wú)太大關(guān)系朦前。
ContextRefresher
顧名思義厘唾,ContextRefresher 用于刷新 Spring 上下文褥符,在以下場(chǎng)景會(huì)調(diào)用其 refresh 方法。
- 請(qǐng)求 /refresh Endpoint抚垃。
- 集成 Spring Cloud Bus 后喷楣,收到 RefreshRemoteApplicationEvent 事件(任意集成 Bus 的應(yīng)用,請(qǐng)求 /bus/refresh Endpoint 后都會(huì)將事件推送到整個(gè)集群)鹤树。
這個(gè)方法包含了整個(gè)刷新邏輯铣焊,也是本文分析的重點(diǎn)。
首先看一下這個(gè)方法的實(shí)現(xiàn):
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罕伯,這個(gè)方法接收了當(dāng)前環(huán)境中的所有屬性源(PropertySource)曲伊,并將其中的非標(biāo)準(zhǔn)屬性源的所有屬性匯總到一個(gè) Map 中返回。
這里的標(biāo)準(zhǔn)屬性源指的是 StandardEnvironment 和 StandardServletEnvironment追他,前者會(huì)注冊(cè)系統(tǒng)變量(System Properties)和環(huán)境變量(System Environment)坟募,后者會(huì)注冊(cè) Servlet 環(huán)境下的 Servlet Context 和 Servlet Config 的初始參數(shù)(Init Params)和 JNDI 的屬性。個(gè)人理解是因?yàn)檫@些屬性無(wú)法改變湿酸,所以不進(jìn)行刷新婿屹。
第二步 addConfigFilesToEnvironment 是核心邏輯,它創(chuàng)建了一個(gè)新的 Spring Boot 應(yīng)用并初始化:
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();
這個(gè)應(yīng)用只是為了重新加載一遍屬性源推溃,所以只配置了 BootstrapApplicationListener 和 ConfigFileApplicationListener昂利,最后將新加載的屬性源替換掉原屬性源,至此屬性源本身已經(jīng)完成更新了。
此時(shí)屬性源雖然已經(jīng)更新了蜂奸,但是配置項(xiàng)都已經(jīng)注入到了對(duì)應(yīng)的 Spring Bean 中犁苏,需要重新進(jìn)行綁定,所以又觸發(fā)了兩個(gè)操作:
- 將刷新后發(fā)生更改的 Key 收集起來(lái)扩所,發(fā)送一個(gè) EnvironmentChangeEvent 事件围详。
- 調(diào)用 RefreshScope.refreshAll 方法。
EnvironmentChangeEvent
在上文中祖屏,ContextRefresher 發(fā)布了一個(gè) EnvironmentChangeEvent 事件助赞,接下來(lái)看看這個(gè)事件產(chǎn)生了哪些影響。
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:
- Re-bind any @ConfigurationProperties beans in the context
- Set the logger levels for any properties in logging.level.*
官方文檔的介紹中提到袁勺,這個(gè)事件主要會(huì)觸發(fā)兩個(gè)行為:
- 重新綁定上下文中所有使用了 @ConfigurationProperties 注解的 Spring Bean雹食。
- 如果 logging.level.* 配置發(fā)生了改變,重新設(shè)置日志級(jí)別期丰。
這兩段邏輯分別可以在 ConfigurationPropertiesRebinder 和 LoggingRebinder 中看到群叶。
ConfigurationPropertiesRebinder
這個(gè)類(lèi)乍一看代碼量特別少,只需要一個(gè) ConfigurationPropertiesBeans 和一個(gè)ConfigurationPropertiesBindingPostProcessor钝荡,然后調(diào)用 rebind 每個(gè) Bean 即可街立。但是這兩個(gè)對(duì)象是從哪里來(lái)的呢?
public void rebind() {
for (String name : this.beans.getBeanNames()) {
rebind(name);
}
}
ConfigurationPropertiesBeans 需要一個(gè) ConfigurationBeanFactoryMetaData埠通, 這個(gè)類(lèi)邏輯很簡(jiǎn)單赎离,它是一個(gè) BeanFactoryPostProcessor 的實(shí)現(xiàn),將所有的 Bean 都存在了內(nèi)部的一個(gè) Map 中植阴。
而 ConfigurationPropertiesBeans 獲得這個(gè) Map 后蟹瘾,會(huì)查找每一個(gè) Bean 是否有 @ConfigurationProperties 注解,如果有的話就放到自己的 Map 中掠手。
繞了一圈好不容易拿到所有需要重新綁定的 Bean 后憾朴,綁定的邏輯就要簡(jiǎn)單許多了:
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 重新綁定了所有屬性,并做了校驗(yàn)等操作喷鸽。
而 initializeBean 的實(shí)現(xiàn)如下:
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;
}
其中主要做了三件事:
- applyBeanPostProcessorsBeforeInitialization:調(diào)用所有 BeanPostProcessor 的 postProcessBeforeInitialization 方法众雷。
- invokeInitMethods:如果 Bean 繼承了 InitializingBean,執(zhí)行 afterPropertiesSet 方法做祝,或是如果 Bean 指定了 init-method 屬性砾省,如果有則調(diào)用對(duì)應(yīng)方法
- applyBeanPostProcessorsAfterInitialization:調(diào)用所有 BeanPostProcessor 的 postProcessAfterInitialization 方法。
之后 ConfigurationPropertiesRebinder 就完成整個(gè)重新綁定流程了混槐。
LoggingRebinder
相比之下 LoggingRebinder 的邏輯要簡(jiǎn)單許多编兄,它只是調(diào)用了 LoggingSystem 的方法重新設(shè)置了日志級(jí)別,具體邏輯就不在本文詳述了声登。
RefreshScope
首先看看這個(gè)類(lèi)的注釋?zhuān)?/p>
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.
這里提到了兩個(gè)重點(diǎn):
- 所有 @RefreshScope 的 Bean 都是延遲加載的狠鸳,只有在第一次訪問(wèn)時(shí)才會(huì)初始化
- 刷新 Bean 也是同理揣苏,下次訪問(wèn)時(shí)會(huì)創(chuàng)建一個(gè)新的對(duì)象
再看一下方法實(shí)現(xiàn):
public void refreshAll() {
super.destroy();
this.context.publishEvent(new RefreshScopeRefreshedEvent());
}
這個(gè)類(lèi)中有一個(gè)成員變量 cache,用于緩存所有已經(jīng)生成的 Bean件舵,在調(diào)用 get 方法時(shí)嘗試從緩存加載卸察,如果沒(méi)有的話就生成一個(gè)新對(duì)象放入緩存,并通過(guò) getBean 初始化其對(duì)應(yīng)的 Bean:
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;
}
}
所以在銷(xiāo)毀時(shí)只需要將整個(gè)緩存清空铅祸,下次獲取對(duì)象時(shí)自然就可以重新生成新的對(duì)象坑质,也就自然綁定了新的屬性:
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();
}
清空緩存后,下次訪問(wèn)對(duì)象時(shí)就會(huì)重新創(chuàng)建新的對(duì)象并放入緩存了临梗。
而在清空緩存后涡扼,它還會(huì)發(fā)出一個(gè) RefreshScopeRefreshedEvent 事件,在某些 Spring Cloud 的組件中會(huì)監(jiān)聽(tīng)這個(gè)事件并作出一些反饋夜焦。
Zuul
Zuul 在收到這個(gè)事件后壳澳,會(huì)將自身的路由設(shè)置為 dirty 狀態(tài):
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);
}
}
}
并且當(dāng)路由實(shí)現(xiàn)為 RefreshableRouteLocator 時(shí),會(huì)嘗試刷新路由:
public void setDirty(boolean dirty) {
this.dirty = dirty;
if (this.routeLocator instanceof RefreshableRouteLocator) {
((RefreshableRouteLocator) this.routeLocator).refresh();
}
}
當(dāng)狀態(tài)為 dirty 時(shí)茫经,Zuul 會(huì)在下一次接受請(qǐng)求時(shí)重新注冊(cè)路由,以更新配置:
if (this.dirty) {
synchronized (this) {
if (this.dirty) {
registerHandlers();
this.dirty = false;
}
}
}
Eureka
在 Eureka 收到該事件時(shí)萎津,對(duì)于客戶端和服務(wù)端都有不同的處理方式:
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();
}
}
}
對(duì)于客戶端來(lái)說(shuō)卸伞,只是調(diào)用了下 eurekaClient.getApplications,理論上這個(gè)方法是沒(méi)有任何效果的锉屈,但是查看上面的注釋?zhuān)约奥?lián)想到 RefreshScope 的延時(shí)初始化特性荤傲,這個(gè)方法調(diào)用應(yīng)該只是為了強(qiáng)制初始化新的 EurekaClient。
事實(shí)上這里很有趣的是颈渊,在 EurekaClientAutoConfiguration 中遂黍,實(shí)際為了 EurekaClient 提供了兩種初始化方案,分別對(duì)應(yīng)是否有 RefreshScope俊嗽,所以以上的猜測(cè)應(yīng)該是正確的雾家。
而對(duì)于服務(wù)端來(lái)說(shuō),EurekaAutoServiceRegistration 會(huì)將服務(wù)端先標(biāo)記為下線绍豁,在進(jìn)行重新上線芯咧。
總結(jié)
至此,Spring Cloud 的熱更新流程就到此結(jié)束了竹揍,從這些源碼中可以總結(jié)出以下結(jié)論:
- 通過(guò)使用 ContextRefresher 可以進(jìn)行手動(dòng)的熱更新敬飒,而不需要依靠 Bus 或是 Endpoint。
- 熱更新會(huì)對(duì)兩類(lèi) Bean 進(jìn)行配置刷新芬位,一類(lèi)是使用了 @ConfigurationProperties 的對(duì)象无拗,另一類(lèi)是使用了 @RefreshScope 的對(duì)象。
- 這兩種對(duì)象熱更新的機(jī)制不同昧碉,前者在同一個(gè)對(duì)象中重新綁定了所有屬性英染,后者則是利用了 RefreshScope 的緩存和延遲加載機(jī)制阴孟,生成了新的對(duì)象。
- 通過(guò)自行監(jiān)聽(tīng) EnvironmentChangeEvent 事件税迷,也可以獲得更改的配置項(xiàng)永丝,以便實(shí)現(xiàn)自己的熱更新邏輯。
- 在使用 Eureka 的項(xiàng)目中要謹(jǐn)慎的使用熱更新箭养,過(guò)于頻繁的更新可能會(huì)使大量項(xiàng)目頻繁的標(biāo)記下線和上線慕嚷,需要注意。