Spring Cloud Config 規範

阿里巴巴中介軟體發表於2018-12-12

Spring Cloud Config 規範

首先Spring Cloud 是基於 Spring 來擴充套件的,Spring 本身就提供當建立一個Bean時可從Environment 中將一些屬性值通過@Value的形式注入到業務程式碼中的能力。那Spring Cloud Config 要解決的問題就是:

  1. 如何將配置載入到 Environment 。
  2. 配置變更時,如何控制 Bean 是否需要 create,重新觸發一次 Bean 的初始化,才能將 @Value 註解指定的欄位從 Environment 中重新注入。
  3. 配置變更時,如何控制新的配置會更新到 Environment 中,才能保證配置變更時可注入最新的值。

要解決以上三個問題:Spring Cloud Config 規範中剛好定義了核心的三個介面:

  1. PropertySourceLocator:抽象出這個介面,就是讓使用者可定製化的將一些配置載入到 Environment。這部分的配置獲取遵循了 Spring Cloud Config 的理念,即希望能從外部儲存介質中來 loacte。
  2. RefreshScope: Spring Cloud 定義這個註解,是擴充套件了 Spring 原有的 Scope 型別。用來標識當前這個 Bean 是一個refresh 型別的 Scope。其主要作用就是可以控制 Bean 的整個生命週期。
  3. ContextRefresher:抽象出這個 Class,是讓使用者自己按需來重新整理上下文(比如當有配置重新整理時,希望可以重新整理上下文,將最新的配置更新到 Environment,重新建立 Bean 時,就可以從 Environment 中注入最新的配置)。

Spring Cloud Config 原理

Spring Cloud Config 的啟動過程

1、如何將配置載入到Environment:PropertySourceLocator

在整個 Spring Boot 啟動的生命週期過程中,有一個階段是 prepare environment。在這個階段,會publish 一個 ApplicationEnvironmentPreparedEvent,通知所有對這個事件感興趣的 Listener,提供對 Environment 做更多的定製化的操作。Spring Cloud 定義了一個BootstrapApplicationListener,在 BootstrapApplicationListener 的處理過程中有一步非常關鍵的操作如下所示:

private ConfigurableApplicationContext bootstrapServiceContext(
			ConfigurableEnvironment environment, final SpringApplication application,
			String configName) {
		//省略
		ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
		// Use names and ensure unique to protect against duplicates
		List<String> names = new ArrayList<>(SpringFactoriesLoader
				.loadFactoryNames(BootstrapConfiguration.class, classLoader));
		//省略
	}
複製程式碼

這是 Spring 的工廠載入機制,可通過在 META-INF/spring.factories 檔案中配置一些程式中預定義的一些擴充套件點。比如 Spring Cloud 這裡的實現,可以看到 BootstrapConfiguration 不是一個具體的介面,而是一個註解。通過這種方式配置的擴充套件點好處是不侷限於某一種介面的實現,而是同一類別的實現。可以檢視 spring-cloud-context 包中的 spring.factories 檔案關於BootstrapConfiguration的配置,有一個比較核心入口的配置就是:

org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration
複製程式碼

可以發現 PropertySourceBootstrapConfiguration 實現了 ApplicationContextInitializer 介面,其目的就是在應用程式上下文初始化的時候做一些額外的操作。在 Bootstrap 階段,會通過 Spring Ioc 的整個生命週期來初始化所有通過key為_org.springframework.cloud.bootstrap.BootstrapConfiguration_ 在 spring.factories 中配置的 Bean。Spring Cloud Alibaba Nacos Config 的實現就是通過該key來自定義一些在Bootstrap 階段需要初始化的一些Bean。在該模組的 spring.factories 配置檔案中可以看到如下配置:

org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.alibaba.nacos.NacosConfigBootstrapConfiguration
複製程式碼

在 Bootstrap 階段初始化的過程中,會獲取所有 ApplicationContextInitializer 型別的 Bean,並設定回SpringApplication主流程當中。如下 BootstrapApplicationListener 類中的部分程式碼所示:

private void apply(ConfigurableApplicationContext context,
		SpringApplication application, ConfigurableEnvironment environment) {
	@SuppressWarnings("rawtypes")
	//這裡的 context 是一個 bootstrap 級別的 ApplicationContext,這裡已經含有了在 bootstrap階段所有需要初始化的 Bean。
	//因此可以獲取 ApplicationContextInitializer.class 型別的所有例項
	List<ApplicationContextInitializer> initializers = getOrderedBeansOfType(context,
			ApplicationContextInitializer.class);
	//設定回 SpringApplication 主流程當中
	application.addInitializers(initializers 
			.toArray(new ApplicationContextInitializer[initializers.size()]));
	
	//省略...
}
複製程式碼

這樣一來,就可以通過在 SpringApplication 的主流程中來回撥這些ApplicationContextInitializer 的例項,做一些初始化的操作。如下 SpringApplication 類中的部分程式碼所示:

private void prepareContext(ConfigurableApplicationContext context,
		ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
		ApplicationArguments applicationArguments, Banner printedBanner) {
	context.setEnvironment(environment);
	postProcessApplicationContext(context);
	//回撥在BootstrapApplicationListener中設定的ApplicationContextInitializer例項
	applyInitializers(context);
	listeners.contextPrepared(context);
	//省略...
}

protected void applyInitializers(ConfigurableApplicationContext context) {
	for (ApplicationContextInitializer initializer : getInitializers()) {
		Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(
				initializer.getClass(), ApplicationContextInitializer.class);
		Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
		initializer.initialize(context);
	}
}
複製程式碼

在 applyInitializers 方法中,會觸發 PropertySourceBootstrapConfiguration 中的 initialize 方法。如下所示:

@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
	CompositePropertySource composite = new CompositePropertySource(
			BOOTSTRAP_PROPERTY_SOURCE_NAME);
	AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
	boolean empty = true;
	ConfigurableEnvironment environment = applicationContext.getEnvironment();
	for (PropertySourceLocator locator : this.propertySourceLocators) {
		PropertySource<?> source = null;
		//回撥所有實現PropertySourceLocator介面例項的locate方法,
		source = locator.locate(environment);
		if (source == null) {
			continue;
		}
		
		composite.addPropertySource(source);
		empty = false;
	}
	if (!empty) {
	//從當前Enviroment中獲取 propertySources
		MutablePropertySources propertySources = environment.getPropertySources();
		//省略...
		//將composite中的PropertySource新增到當前應用上下文的propertySources中
		insertPropertySources(propertySources, composite);
		//省略...
	}
複製程式碼

在這個方法中會回撥所有實現 PropertySourceLocator 介面例項的locate方法, locate 方法返回一個 PropertySource 的例項,統一add到CompositePropertySource例項中。如果 composite 中有新加的PropertySource,最後將composite中的PropertySource新增到當前應用上下文的propertySources中。Spring Cloud Alibaba Nacos Config 在 Bootstrap 階段通過Java配置的方式初始化了一個 NacosPropertySourceLocator 型別的Bean。從而在 locate 方法中將存放在Nacos中的配置資訊讀取出來,將讀取結果存放到 PropertySource 的例項中返回。具體如何從Nacos中讀取配置資訊可參考 NacosPropertySourceLocator 類的實現。

Spring Cloud Config 正是提供了PropertySourceLocator介面,來提供應用外部化配置可動態載入的能力。Spring Ioc 容器在初始化 Bean 的時候,如果發現 Bean 的欄位上含有 @Value 的註解,就會從 Enviroment 中的PropertySources 來獲取其值,完成屬性的注入。

Spring Cloud Config 外部化配置可動態重新整理

感知到外部化配置的變更這部分程式碼的操作是需要使用者來完成的。Spring Cloud Config 只提供了具備外部化配置可動態重新整理的能力,並不具備自動感知外部化配置發生變更的能力。比如如果你的配置是基於Mysql來實現的,那麼在程式碼裡面肯定要有能力感知到配置發生變化了,然後再顯示的呼叫 ContextRefresher 的 refresh方法,從而完成外部化配置的動態重新整理(只會重新整理使用RefreshScope註解的Bean)。

例如在 Spring Cloud Alibaba Nacos Config 的實現過程中,Nacos 提供了對dataid 變更的Listener 回撥。在對每個dataid 註冊好了相應的Listener之後,如果Nacos內部通過長輪詢的方式感知到資料的變更,就會回撥相應的Listener,在 Listener 的實現過程中,就是通過呼叫 ContextRefresher 的 refresh方法完成配置的動態重新整理。具體可參考 NacosContextRefresher 類的實現。

Sring Cloud Config的動態配置重新整理原理圖如下所示:

undefined

ContextRefresher的refresh的方法主要做了兩件事:

  1. 觸發PropertySourceLocator的locator方法,需要載入最新的值,並替換 Environment 中舊值
  2. Bean中的引用配置值需要重新注入一遍。重新注入的流程是在Bean初始化時做的操作,那也就是需要將refresh scope中的Bean 快取失效,當再次從refresh scope中獲取這個Bean時,發現取不到,就會重新觸發一次Bean的初始化過程。

這兩個操作所對應的程式碼如下所示:

public synchronized Set<String> refresh() {
		Map<String, Object> before = extract(
				this.context.getEnvironment().getPropertySources());
		//1、載入最新的值,並替換Envrioment中舊值
		addConfigFilesToEnvironment();
		Set<String> keys = changes(before,
				extract(this.context.getEnvironment().getPropertySources())).keySet();
		this.context.publishEvent(new EnvironmentChangeEvent(context, keys));
		//2、將refresh scope中的Bean 快取失效: 清空
		this.scope.refreshAll();
		return keys;
	}
複製程式碼

addConfigFilesToEnvironment 方法中發生替換的程式碼如下所示:

ConfigurableApplicationContext addConfigFilesToEnvironment() {
		ConfigurableApplicationContext capture = null;
		try {
			//省略...
			//1、這裡會重新觸發PropertySourceLoactor的locate的方法,獲取最新的外部化配置
			capture = (SpringApplicationBuilder)builder.run();
			
			MutablePropertySources target = this.context.getEnvironment()
					.getPropertySources();
			String targetName = null;
			for (PropertySource<?> source : environment.getPropertySources()) {
				String name = source.getName();
				//省略..
				
				//只有不是標準的 Source 才可替換
				if (!this.standardSources.contains(name)) {
					if (target.contains(name)) {
						//開始用新的PropertySource替換舊值
						target.replace(name, source);
					}
					//
				}
			}
		}
		//
		return capture;
	}
複製程式碼

this.scope.refreshAll() 清空快取的操作程式碼如下所示:

@Override
	public void destroy() {
		List<Throwable> errors = new ArrayList<Throwable>();
		//清空Refresh Scope 中的快取
		Collection<BeanLifecycleWrapper> wrappers = this.cache.clear();
		//省略...
	}

複製程式碼

為了驗證每次配置重新整理時,Bean 是新建立的,特意寫了一個Demo 驗證了下,如下所示:

Acm Properties: beijing-region
//重新整理前
Object Instance is :com.alibaba.demo.normal.ConfigProperties@1be9634
2018-11-01 19:16:32.535  INFO 27254 --- [gPullingdefault] startup date [Thu Nov 01 19:16:32 CST 2018]; root of context hierarchy
Acm Properties: qingdao-region
//重新整理後
Object Instance is :com.alibaba.demo.normal.ConfigProperties@2c6965e0

複製程式碼

Spring Cloud Config 擴充套件Scope的核心類:RefreshScope

可以看到上面的程式碼中有 this.scope.refreshAll(),其中的scope就是RefreshScope。是用來存放scope型別為refresh型別的Bean(即使用RefreshScope註解標識的Bean),也就是說當一個Bean既不是singleton也不是prototype時,就會從自定義的Scope中去獲取(Spring 允許自定義Scope),然後呼叫Scope的get方法來獲取一個例項,Spring Cloud 正是擴充套件了Scope,從而控制了整個 Bean 的生命週期。當配置需要動態重新整理的時候, 呼叫this.scope.refreshAll()這個方法,就會將整個RefreshScope的快取清空,完成配置可動態重新整理的可能。

更多關於Scope的分析請參考 這裡

後續

關於ContextRefresh 和 RefreshScope的初始化配置是在RefreshAutoConfiguration類中完成的。而RefreshAutoConfiguration類初始化的入口是在spring-cloud-context中的META-INF/spring.factories中配置的。從而完成整個和動態重新整理相關的Bean的初始化操作。


**歡迎關注“[阿里巴巴中介軟體官方微博](https://weibo.com/p/1006066822341783/home?from=page_100606&mod=TAB&is_all=1#place)” ※一個集乾貨與前衛的技術號**

Spring Cloud Config 規範

**歡迎關注“阿里巴巴中介軟體”官方公眾號,與技術同行**

Spring Cloud Config 規範

相關文章