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。