本文以携程的Apollo和阿里的Nacos为例。
pom中引入一下依赖:
com.ctrip.framework.apollo apollo-client 2.0.1 com.alibaba.cloud spring-cloud-starter-alibaba-nacos-config 2021.1
不管是Apollo还是Nacos,实现从远程加载配置都是通过ConfigurableEnvironment
和PropertySource
完成的,步骤如下:
PropertySource
ConfigurableEnvironment
获取聚合类 MutablePropertySources propertySources = ConfigurableEnvironment#getPropertySources();
PropertySource
添加到从ConfigurableEnvironment
获取的聚合类MutablePropertySources#add...(PropertySource> propertySource)
至于这个过程是怎么触发和运行的,要看具体实现。
org.springframework.cloud.bootstrap.config
包下),nacos实现该规范即可,即实现spring cloud 的PropertySourceLocator
接口。关注PropertySourcesProcessor
,该类为一个BeanFactoryPostProcessor
,同时为了获取ConfigurableEnvironment
,该类实现了EnvironmentAware
回调接口。该类何时被加入spring容器?是通过@EnableApolloConfig
的@Import
注解的类ApolloConfigRegistrar
来加入,常规套路。
public class PropertySourcesProcessor implements BeanFactoryPostProcessor, EnvironmentAware,ApplicationEventPublisherAware, PriorityOrdered {// aware回调接口设置private ConfigurableEnvironment environment;@Overridepublic void setEnvironment(Environment environment) {//it is safe enough to cast as all known environment is derived from ConfigurableEnvironmentthis.environment = (ConfigurableEnvironment) environment;}@Overridepublic void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {// 获取配置this.configUtil = ApolloInjector.getInstance(ConfigUtil.class);// 从远程获取PropertySourceinitializePropertySources();// 为每个ConfigPropertySource注册ConfigChangeEvent监听器// 监听器监听到ConfigChangeEvent后publish一个ApolloConfigChangeEvent// 等于将apollo自定义的ConfigChangeEvent事件机制转化为了spring的ApolloConfigChangeEvent事件initializeAutoUpdatePropertiesFeature(beanFactory);}private void initializePropertySources() {// 聚合类,该类也是一个PropertySource,代理了一堆PropertySource// 该类中有一个 Set> 字段CompositePropertySource composite = new ...;...// 从 远程 或 本地缓存 获取配置Config config = ConfigService.getConfig(namespace);// 适配Config到PropertySource,并加入聚合类 composite.addPropertySource(configPropertySourceFactory.getConfigPropertySource(namespace, config));// 添加到ConfigurableEnvironmentenvironment.getPropertySources().addFirst(composite);}private void initializeAutoUpdatePropertiesFeature(ConfigurableListableBeanFactory beanFactory) {if (!AUTO_UPDATE_INITIALIZED_BEAN_FACTORIES.add(beanFactory)) {return;}// 定义监听器,监听器监听到ConfigChangeEvent后发布ApolloConfigChangeEventConfigChangeListener configChangeEventPublisher = changeEvent ->applicationEventPublisher.publishEvent(new ApolloConfigChangeEvent(changeEvent));// 注册监听器到每个PropertySourceList configPropertySources = configPropertySourceFactory.getAllConfigPropertySources();for (ConfigPropertySource configPropertySource : configPropertySources) {configPropertySource.addChangeListener(configChangeEventPublisher);}}...
}
从上面可知初始化时会从ConfigService远程拉取配置,并保存到内部缓存。而后续远程配置中心配置发生变化时本地会拉去最新配置并发布事件,PropertySource根据事件进行更新。
无论是开始从远程拉取配置初始化,还是后续远程配置更新,最终都是通过RemoteConfigRepository
以http形式定时获取配置:
public class RemoteConfigRepository extends AbstractConfigRepository implements ConfigRepository{public RemoteConfigRepository(String namespace) {...// 定时拉取this.schedulePeriodicRefresh();// 长轮询this.scheduleLongPollingRefresh();...}private void schedulePeriodicRefresh() {// 定时线程池m_executorService.scheduleAtFixedRate(new Runnable() {@Overridepublic void run() {// 调用父抽象类trySync()// trySync()调用模版方法sync()trySync();}}, m_configUtil.getRefreshInterval(), m_configUtil.getRefreshInterval(),m_configUtil.getRefreshIntervalTimeUnit());}@Overrideprotected synchronized void sync() {// 事务Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "syncRemoteConfig");try {ApolloConfig previous = m_configCache.get();// http远程拉取配置ApolloConfig current = loadApolloConfig();// reference equals means HTTP 304if (previous != current) {logger.debug("Remote Config refreshed!");// 设置缓存m_configCache.set(current);// 发布事件,该方法在父抽象类中this.fireRepositoryChange(m_namespace, this.getConfig());}if (current != null) {Tracer.logEvent(String.format("Apollo.Client.Configs.%s", current.getNamespaceName()),current.getReleaseKey());}transaction.setStatus(Transaction.SUCCESS);} catch (Throwable ex) {transaction.setStatus(ex);throw ex;} finally {transaction.complete();}...}
可以看到,在构造方法中,就执行了 3 个本地方法,其中就包括定时刷新和长轮询刷新。这两个功能在 apollo 的 github 文档中也有介绍:
客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送。
客户端还会定时从Apollo配置中心服务端拉取应用的最新配置。
这是一个fallback机制,为了防止推送机制失效导致配置不更新。
客户端定时拉取会上报本地版本,所以一般情况下,对于定时拉取的操作,服务端都会返回304 - Not Modified。
定时频率默认为每5分钟拉取一次,客户端也可以通过在运行时指定System Property: apollo.refreshInterval来覆盖,单位为分钟。
所以,长连接是更新配置的主要手段,然后用定时任务辅助长连接,防止长连接失败。
nacos实现了spring cloud config规范,规范代码的maven坐标如下:
org.springframework.cloud spring-cloud-context ... compile
这里介绍规范内容,nacos的实现略。
PropertySource
用于存储k-v键值对,远程或本地的配置最终都转化为PropertySource
,放入ConfigurableEnvironment
中,通常EnumerablePropertySource
中会代理一个PropertySource
的list。
规范接口主要为PropertySourceLocator
接口,该接口用于定位PropertySource
,注释如下:
Strategy for locating (possibly remote) property sources for the Environment. Implementations should not fail unless they intend to prevent the application from starting.
public interface PropertySourceLocator {// 实现类实现该方法PropertySource> locate(Environment environment);default Collection> locateCollection(Environment environment) {return locateCollection(this, environment);}static Collection> locateCollection(PropertySourceLocator locator, Environment environment) {// 调用实现类PropertySource> propertySource = locator.locate(environment);if (propertySource == null) {return Collections.emptyList();}// 如果该PropertySource是代理了list的CompositePropertySource,提取全部if (CompositePropertySource.class.isInstance(propertySource)) {Collection> sources = ((CompositePropertySource) propertySource).getPropertySources();List> filteredSources = new ArrayList<>();for (PropertySource> p : sources) {if (p != null) {filteredSources.add(p);}}return filteredSources;}else {return Arrays.asList(propertySource);}}}
调用PropertySourceLocator
接口将PropertySource
加入ConfigurableEnvironment
中。
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(PropertySourceBootstrapProperties.class)
public class PropertySourceBootstrapConfigurationimplements ApplicationContextInitializer, Ordered {@Autowired(required = false)private List propertySourceLocators = new ArrayList<>();public void setPropertySourceLocators(Collection propertySourceLocators) {this.propertySourceLocators = new ArrayList<>(propertySourceLocators);}@Overridepublic void initialize(ConfigurableApplicationContext applicationContext) {List> composite = new ArrayList<>();// 排序AnnotationAwareOrderComparator.sort(this.propertySourceLocators);boolean empty = true;// applicationContext由回调接口提供ConfigurableEnvironment environment = applicationContext.getEnvironment();for (PropertySourceLocator locator : this.propertySourceLocators) {// 调用PropertySourceLocatorCollection> source = locator.locateCollection(environment);...for (PropertySource> p : source) {// 是否代理了PropertySource的list做分类if (p instanceof EnumerablePropertySource) {EnumerablePropertySource> enumerable = (EnumerablePropertySource>) p;sourceList.add(new BootstrapPropertySource<>(enumerable));}else {sourceList.add(new SimpleBootstrapPropertySource(p));}}composite.addAll(sourceList);empty = false;}if (!empty) {// 获取 ConfigurableEnvironment中的MutablePropertySourcesMutablePropertySources propertySources = environment.getPropertySources();...// 执行插入到ConfigurableEnvironment的MutablePropertySourcesinsertPropertySources(propertySources, composite);...}}
}
可以看到从远程获取配置都是通过向ConfigurableEnvironment
插入从远程获取的数据转化的PropertySource
。而从远程获取就涉及到长轮询、本地缓存等内容,设计都比较一致。