轉自:http://www.scienjus.com/spring-cloud-refresh/
作為一篇源碼分析的文章,本文雖然介紹 Spring Cloud 的熱更新機制,但是實際全文內容都不會與 Spring Cloud Config 以及 Spring Cloud Bus 有關,因為前者只是提供了一個遠端的配置源,而后者也只是提供了集群環境下的事件觸發機制,與核心流程均無太大關系。
ContextRefresher
顧名思義,ContextRefresher 用于刷新 Spring 上下文,在以下場景會調用其 refresh 方法。
- 請求 /refresh Endpoint。
- 集成 Spring Cloud Bus 后,收到 RefreshRemoteApplicationEvent 事件(任意集成 Bus 的應用,請求 /bus/refresh Endpoint 后都會將事件推送到整個集群)。
這個方法包含了整個刷新邏輯,也是本文分析的重點。
首先看一下這個方法的實現:
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 中返回。
這里的標準屬性源指的是 StandardEnvironment 和 StandardServletEnvironment,前者會注冊系統變量(System Properties)和環境變量(System Environment),后者會注冊 Servlet 環境下的 Servlet Context 和 Servlet Config 的初始參數(Init Params)和 JNDI 的屬性。個人理解是因為這些屬性無法改變,所以不進行刷新。
第二步 addConfigFilesToEnvironment 是核心邏輯,它創建了一個新的 Spring Boot 應用并初始化:
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();
這個應用只是為了重新加載一遍屬性源,所以只配置了 BootstrapApplicationListener 和 ConfigFileApplicationListener,最后將新加載的屬性源替換掉原屬性源,至此屬性源本身已經完成更新了。
此時屬性源雖然已經更新了,但是配置項都已經注入到了對應的 Spring Bean 中,需要重新進行綁定,所以又觸發了兩個操作:
- 將刷新后發生更改的 Key 收集起來,發送一個 EnvironmentChangeEvent 事件。
- 調用 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:
- Re-bind any @ConfigurationProperties beans in the context
- Set the logger levels for any properties in logging.level.*
官方文檔的介紹中提到,這個事件主要會觸發兩個行為:
- 重新綁定上下文中所有使用了 @ConfigurationProperties 注解的 Spring Bean。
- 如果 logging.level.* 配置發生了改變,重新設置日志級別。
這兩段邏輯分別可以在 ConfigurationPropertiesRebinder 和 LoggingRebinder 中看到。
ConfigurationPropertiesRebinder
這個類乍一看代碼量特別少,只需要一個 ConfigurationPropertiesBeans 和一個ConfigurationPropertiesBindingPostProcessor,然后調用 rebind 每個 Bean 即可。但是這兩個對象是從哪里來的呢?
public void rebind() {
for (String name : this.beans.getBeanNames()) {
rebind(name);
}
}
ConfigurationPropertiesBeans 需要一個 ConfigurationBeanFactoryMetaData, 這個類邏輯很簡單,它是一個 BeanFactoryPostProcessor 的實現,將所有的 Bean 都存在了內部的一個 Map 中。
而 ConfigurationPropertiesBeans 獲得這個 Map 后,會查找每一個 Bean 是否有 @ConfigurationProperties 注解,如果有的話就放到自己的 Map 中。
繞了一圈好不容易拿到所有需要重新綁定的 Bean 后,綁定的邏輯就要簡單許多了:
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 的實現如下:
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:調用所有 BeanPostProcessor 的 postProcessBeforeInitialization 方法。
- invokeInitMethods:如果 Bean 繼承了 InitializingBean,執行 afterPropertiesSet 方法,或是如果 Bean 指定了 init-method 屬性,如果有則調用對應方法
- applyBeanPostProcessorsAfterInitialization:調用所有 BeanPostProcessor 的 postProcessAfterInitialization 方法。
之后 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.
這里提到了兩個重點:
- 所有 @RefreshScope 的 Bean 都是延遲加載的,只有在第一次訪問時才會初始化
- 刷新 Bean 也是同理,下次訪問時會創建一個新的對象
再看一下方法實現:
public void refreshAll() {
super.destroy();
this.context.publishEvent(new RefreshScopeRefreshedEvent());
}
這個類中有一個成員變量 cache,用于緩存所有已經生成的 Bean,在調用 get 方法時嘗試從緩存加載,如果沒有的話就生成一個新對象放入緩存,并通過 getBean 初始化其對應的 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;
}
}
所以在銷毀時只需要將整個緩存清空,下次獲取對象時自然就可以重新生成新的對象,也就自然綁定了新的屬性:
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 狀態:
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 時,會嘗試刷新路由:
public void setDirty(boolean dirty) {
this.dirty = dirty;
if (this.routeLocator instanceof RefreshableRouteLocator) {
((RefreshableRouteLocator) this.routeLocator).refresh();
}
}
當狀態為 dirty 時,Zuul 會在下一次接受請求時重新注冊路由,以更新配置:
if (this.dirty) {
synchronized (this) {
if (this.dirty) {
registerHandlers();
this.dirty = false;
}
}
}
Eureka
在 Eureka 收到該事件時,對于客戶端和服務端都有不同的處理方式:
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 的熱更新流程就到此結束了,從這些源碼中可以總結出以下結論:
- 通過使用 ContextRefresher 可以進行手動的熱更新,而不需要依靠 Bus 或是 Endpoint。
- 熱更新會對兩類 Bean 進行配置刷新,一類是使用了 @ConfigurationProperties 的對象,另一類是使用了 @RefreshScope 的對象。
- 這兩種對象熱更新的機制不同,前者在同一個對象中重新綁定了所有屬性,后者則是利用了 RefreshScope 的緩存和延遲加載機制,生成了新的對象。
- 通過自行監聽 EnvironmentChangeEvent 事件,也可以獲得更改的配置項,以便實現自己的熱更新邏輯。
- 在使用 Eureka 的項目中要謹慎的使用熱更新,過于頻繁的更新可能會使大量項目頻繁的標記下線和上線,需要注意。