Spring Cloud Config 動態刷新實現機制

Spring Cloud 默認實現了配置中心動態刷新的功能,在公共模塊 spring-cloud-context 包中。目前比較流行的配置中心 Spring Cloud Config 動態刷新便是依賴此模塊,而Nacos動態刷新機制是在此模塊上做了擴展,比Spring Cloud Config功能更強大豐富。
首先 Spring Cloud Config 動態刷新需要依賴 Spring Cloud Bus,而 Nacos 則是在後臺修改後直接推送到各服務。
其次,Spring Cloud Config的刷新機制針對所有修改的變量,只有有改動,後臺就會獲取。而Nacos則是支持粒度更細的方式,只有 refresh 屬性爲 true 的配置項,纔會在運行的過程中變更爲新的值。這時Nacos特有的方式。

相同點:兩種配置中心動態刷新的範圍都是以下兩種:

  • @ConfigurationProperties 註解的配置類
  • @RefreshScope 註解的bean

分別看一下這兩點的實現原理。


首先 spring cloud config 動態刷新功能通過以下變量來確定是否開啓,默認爲true。
@ConditionalOnProperty(value = “endpoints.refresh.enabled”, matchIfMissing = true)

RefreshEndpoint 端點暴露方式:

public class LifecycleMvcEndpointAutoConfiguration {

	@Bean
	@ConditionalOnBean(RefreshEndpoint.class)
	public MvcEndpoint refreshMvcEndpoint(RefreshEndpoint endpoint) {
		return new GenericPostableMvcEndpoint(endpoint);
	}
}	

// Mvc適配器
public class GenericPostableMvcEndpoint extends EndpointMvcAdapter {

	//代理類爲RefreshEndpoint 
	public GenericPostableMvcEndpoint(Endpoint<?> delegate) {
		super(delegate);
	}

	@RequestMapping(method = RequestMethod.POST)
	@ResponseBody
	@Override
	public Object invoke() {
		if (!getDelegate().isEnabled()) {
			return new ResponseEntity<>(Collections.singletonMap(
					"message", "This endpoint is disabled"), HttpStatus.NOT_FOUND);
		}
		return super.invoke();
	}
}

這裏的實現方式同 springboot actuator endpoint原理一樣,都是通過 EndpointMvcAdapter 適配器來代理實現。

RefreshEndpoint 端點:

public class RefreshEndpoint extends AbstractEndpoint<Collection<String>> {

	private ContextRefresher contextRefresher;

	public String[] refresh() {
		Set<String> keys = contextRefresher.refresh();
		return keys.toArray(new String[keys.size()]);
	}
	
	@Override
	public Collection<String> invoke() {
		return Arrays.asList(refresh());
	}
}

具體的刷新邏輯在 ContextRefresher 中。

配置ContextRefresher 刷新類:

public class ContextRefresher {
	//......
	
	private ConfigurableApplicationContext context;
	private RefreshScope scope;

	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();
		//發佈EnvironmentChangeEvent事件
		this.context.publishEvent(new EnvironmentChangeEvent(keys));
		//刷新 RefreshScope Bean
		this.scope.refreshAll();
		return keys;
	}
	//......
}

addConfigFilesToEnvironment();上述代碼通過該方法重新獲取配置:

private void addConfigFilesToEnvironment() {
		ConfigurableApplicationContext capture = null;
		try {
			StandardEnvironment environment = copyEnvironment(
					this.context.getEnvironment());
			//這裏重新創建 springboot啓動類,重新啓動時,通過配置中心會就會重新獲取配置了
			capture = new SpringApplicationBuilder(Empty.class).bannerMode(Mode.OFF)
					.web(false).environment(environment).run();
			if (environment.getPropertySources().contains(REFRESH_ARGS_PROPERTY_SOURCE)) {
				environment.getPropertySources().remove(REFRESH_ARGS_PROPERTY_SOURCE);
			}
			MutablePropertySources target = this.context.getEnvironment()
					.getPropertySources();
			String targetName = null;
		}
	}

通過SpringApplicationBuilder重新創建啓動類,啓動時就會重新拉取最新配置,然後發佈 EnvironmentChangeEvent事件,通過對應的監聽器重新加載帶有@ConfigurationProperties 的配置類和作用域爲 @RefreshScope 的bean。


@ConfigurationProperties

默認有兩個監聽器會監聽到 EnvironmentChangeEvent 事件:

  • ConfigurationPropertiesRebinder
  • LoggingRebinder

LoggingRebinder只是設置日誌級別,這裏不做展開。

來看一下ConfigurationPropertiesRebinder

public class ConfigurationPropertiesRebinder
		implements ApplicationContextAware, ApplicationListener<EnvironmentChangeEvent> {

	//用來收集所有@ConfigurationProperties 註解的bean
	private ConfigurationPropertiesBeans beans;

	private ConfigurationPropertiesBindingPostProcessor binder;

	public ConfigurationPropertiesRebinder(
			ConfigurationPropertiesBindingPostProcessor binder,
			ConfigurationPropertiesBeans beans) {
		this.binder = binder;
		this.beans = beans;
	}

	@ManagedOperation
	public void rebind() {
		this.errors.clear();
		for (String name : this.beans.getBeanNames()) {
			rebind(name);
		}
	}

	@ManagedOperation
	public boolean rebind(String name) {
		if (!this.beans.getBeanNames().contains(name)) {
			return false;
		}
		if (this.applicationContext != null) {
			try {
				//	重新加載bean
				Object bean = this.applicationContext.getBean(name);
				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;
	}

	//觸發監聽器
	@Override
	public void onApplicationEvent(EnvironmentChangeEvent event) {
		rebind();
	}
}

首先 ConfigurationPropertiesBeans肯定是提前收集好所有@ConfigurationProperties註解的bean,收集方式如下:

public class ConfigurationPropertiesBeans implements BeanPostProcessor,
ApplicationContextAware {

	private Map<String, Object> beans = new HashMap<String, Object>();
	private String refreshScope;

	@Override
	public Object postProcessBeforeInitialization(Object bean, String beanName)
			throws BeansException {
		if (isRefreshScoped(beanName)) {
			return bean;
		}
		ConfigurationProperties annotation = AnnotationUtils.findAnnotation(
				bean.getClass(), ConfigurationProperties.class);
		if (annotation != null) {
			this.beans.put(beanName, bean);
		}
		else if (this.metaData != null) {
			annotation = this.metaData.findFactoryAnnotation(beanName,
					ConfigurationProperties.class);
			if (annotation != null) {
				this.beans.put(beanName, bean);
			}
		}
		return bean;
	}
}

通過BeanPostProcessor擴展接口,然後排除掉refreshScope類型的bean,然後收集對應的屬性配置bean。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章