Apollo配置中心与本地配置优先级
背景
在项目重构时,删除若干个application-{env}.yml
文件,仅保留一个application.yml
文件,该文件中保留的配置项都是几乎不会变更的配置,至于需要跟随不同环境而变更的配置项都放置在Apollo配置中心。
然后本地application.yml
文件里面有一个静态配置,调用外部接口的URL:https://graph.facebook.com/v9.0/aaaaa
,有一天没有得到事前通知(至少研发同事都没人注意到),这个v9.0
版本的API被废弃(deprecated),进而引发严重的生产问题。
方案:
- 修改本地配置,然后重新部署应用;
- 添加Apollo配置。
故而引出问题:本地配置和Apollo配置的优先级关系是怎样的?方案2的预设前提是:Apollo的优先级高于本地配置应用,并且可以自动下发推送到客户端,即无需重新部署应用。
调研
Apollo使用的Spring @Value注解为字段注入值,那么Apollo与yml同时存在相同配置时,以谁为准?Apollo官网有此解释,在 3.1 和Spring集成的原理,结论是优先读取Apollo的配置,Apollo中没有的读取其他的。官网解释如下:
Apollo除了支持API方式获取配置,也支持和Spring/Spring Boot集成,集成原理简述如下。
Spring从3.1版本开始增加ConfigurableEnvironment和PropertySource:
ConfigurableEnvironment
Spring的ApplicationContext会包含一个Environment(实现ConfigurableEnvironment接口)
ConfigurableEnvironment自身包含了很多个PropertySource
PropertySource
属性源
可以理解为很多个Key - Value的属性配置
在运行时的结构形如:
Application Context
Environment
PropertySources
PropertySource1
PropertySource2
PropertySource之间是有优先级顺序的,如果有一个Key在多个property source中都存在,在前面的property source优先。
基于此原理后,Apollo和Spring/SB集成的方案就是:在应用启动阶段,Apollo从远端获取配置,然后组装成PropertySource并插入到第一个即可,即 Remote Property Source
package com.ctrip.framework.apollo.spring.config;
import com.ctrip.framework.apollo.build.ApolloInjector;
import com.ctrip.framework.apollo.spring.property.AutoUpdateConfigChangeListener;
import com.ctrip.framework.apollo.spring.util.SpringInjector;
import com.ctrip.framework.apollo.util.ConfigUtil;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Multimap;
import com.ctrip.framework.apollo.Config;
import com.ctrip.framework.apollo.ConfigService;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.Ordered;
import org.springframework.core.PriorityOrdered;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
/**
* Apollo Property Sources processor for Spring Annotation Based Application. <br /> <br />
*
* The reason why PropertySourcesProcessor implements {@link BeanFactoryPostProcessor} instead of
* {@link org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor} is that lower versions of
* Spring (e.g. 3.1.1) doesn't support registering BeanDefinitionRegistryPostProcessor in ImportBeanDefinitionRegistrar
* - {@link com.ctrip.framework.apollo.spring.annotation.ApolloConfigRegistrar}
*
* @author Jason Song(song_s@ctrip.com)
*/
public class PropertySourcesProcessor implements BeanFactoryPostProcessor, EnvironmentAware, PriorityOrdered {
private static final Multimap<Integer, String> NAMESPACE_NAMES = LinkedHashMultimap.create();
private static final Set<BeanFactory> AUTO_UPDATE_INITIALIZED_BEAN_FACTORIES = Sets.newConcurrentHashSet();
private final ConfigPropertySourceFactory configPropertySourceFactory = SpringInjector.getInstance(ConfigPropertySourceFactory.class);
private ConfigUtil configUtil;
private ConfigurableEnvironment environment;
public static boolean addNamespaces(Collection<String> namespaces, int order) {
return NAMESPACE_NAMES.putAll(order, namespaces);
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
this.configUtil = ApolloInjector.getInstance(ConfigUtil.class);
initializePropertySources();
initializeAutoUpdatePropertiesFeature(beanFactory);
}
private void initializePropertySources() {
if (environment.getPropertySources().contains(PropertySourcesConstants.APOLLO_PROPERTY_SOURCE_NAME)) {
//already initialized
return;
}
CompositePropertySource composite;
if (configUtil.isPropertyNamesCacheEnabled()) {
composite = new CachedCompositePropertySource(PropertySourcesConstants.APOLLO_PROPERTY_SOURCE_NAME);
} else {
composite = new CompositePropertySource(PropertySourcesConstants.APOLLO_PROPERTY_SOURCE_NAME);
}
//sort by order asc
ImmutableSortedSet<Integer> orders = ImmutableSortedSet.copyOf(NAMESPACE_NAMES.keySet());
Iterator<Integer> iterator = orders.iterator();
while (iterator.hasNext()) {
int order = iterator.next();
for (String namespace : NAMESPACE_NAMES.get(order)) {
Config config = ConfigService.getConfig(namespace);
composite.addPropertySource(configPropertySourceFactory.getConfigPropertySource(namespace, config));
}
}
// clean up
NAMESPACE_NAMES.clear();
// add after the bootstrap property source or to the first
if (environment.getPropertySources().contains(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
// ensure ApolloBootstrapPropertySources is still the first
ensureBootstrapPropertyPrecedence(environment);
environment.getPropertySources().addAfter(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME, composite);
} else {
environment.getPropertySources().addFirst(composite);
}
}
private void ensureBootstrapPropertyPrecedence(ConfigurableEnvironment environment) {
MutablePropertySources propertySources = environment.getPropertySources();
PropertySource<?> bootstrapPropertySource = propertySources.get(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME);
// not exists or already in the first place
if (bootstrapPropertySource == null || propertySources.precedenceOf(bootstrapPropertySource) == 0) {
return;
}
propertySources.remove(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME);
propertySources.addFirst(bootstrapPropertySource);
}
private void initializeAutoUpdatePropertiesFeature(ConfigurableListableBeanFactory beanFactory) {
if (!configUtil.isAutoUpdateInjectedSpringPropertiesEnabled() || !AUTO_UPDATE_INITIALIZED_BEAN_FACTORIES.add(beanFactory)) {
return;
}
AutoUpdateConfigChangeListener autoUpdateConfigChangeListener = new AutoUpdateConfigChangeListener(environment, beanFactory);
List<ConfigPropertySource> configPropertySources = configPropertySourceFactory.getAllConfigPropertySources();
for (ConfigPropertySource configPropertySource : configPropertySources) {
configPropertySource.addChangeListener(autoUpdateConfigChangeListener);
}
}
@Override
public void setEnvironment(Environment environment) {
//it is safe enough to cast as all known environment is derived from ConfigurableEnvironment
this.environment = (ConfigurableEnvironment) environment;
}
@Override
public int getOrder() {
//make it as early as possible
return Ordered.HIGHEST_PRECEDENCE;
}
// for test only
static void reset() {
NAMESPACE_NAMES.clear();
AUTO_UPDATE_INITIALIZED_BEAN_FACTORIES.clear();
}
}
验证
本地application.yml
添加一个配置test: aaaaa
,Controller打印配置值(不够严谨:没有加时间戳):
@RestController
public class HealthController {
@Value("${test}")
private String test;
@RequestMapping(value = "/hs")
public String hs() {
System.out.println("test" + test);
return "ok";
}
}
启动应用程序,postman请求接口http://localhost:8080/hs
,控制台输出:testaaaaa
。
增加Apollo配置项:
发布配置项,配置生效时间有延迟,等一分钟,再次请求接口,控制台打印输出依然是:testaaaaa
。
至于未打印时间的不够严谨的问题,给出postman截图:
所以,应用需要重启。
结论:应用发布后,Apollo新增的配置,无论这个配置是否在本地配置文件存在与否,都不能覆盖。想要Apollo的配置生效,必须要重启(重新部署)应用,因为配置是启动时加载的。
注:本文使用的Apollo为公司内部二次开发版,基于Apollo 1.4版本,开源最新版本为1.9。
热更新
public String currentUser() {
log.info("levelOne" + levelOne);
}
2022-03-09 15:50:29.413 [INFO][http-nio-8080-exec-4]:c.x.c.common.services.impl.UserServiceImpl [currentUser:274] levelOne10
另外,再给出配置发布历史,发布前是10,发布后是15。
2022-03-09 15:52:58.564 [INFO][http-nio-8080-exec-1]:c.x.c.common.services.impl.UserServiceImpl [currentUser:274] levelOne10
结论:公司内部维护的Apollo版本,真是一个笑话!!!!!!!!
如何实现热更新
答案:使用@ApolloConfig
。
实例:
@ApolloConfig
private Config config;
int one = config.getIntProperty("category.level.one", 11);
log.info("one: " + one);
变更前的日志:
2022-03-10 14:01:38.373 [INFO][http-nio-8080-exec-4]:c.x.c.common.services.impl.UserServiceImpl [currentUser:280] one: 10
配置变更:
变更前的日志:
2022-03-10 14:03:11.463 [INFO][http-nio-8080-exec-8]:c.x.c.common.services.impl.UserServiceImpl [currentUser:280] one: 12
其他
另外还有一个感觉很恶心的使用问题,配置项必须不能为空:
参考
Apollo配置中心设计
apollo配置中心与yml中存在相同配置
https://github.com/apolloconfig/apollo/issues/1800