Apollo配置中心与本地配置优先级

背景

在项目重构时,删除若干个application-{env}.yml文件,仅保留一个application.yml文件,该文件中保留的配置项都是几乎不会变更的配置,至于需要跟随不同环境而变更的配置项都放置在Apollo配置中心。

然后本地application.yml文件里面有一个静态配置,调用外部接口的URL:https://graph.facebook.com/v9.0/aaaaa,有一天没有得到事前通知(至少研发同事都没人注意到),这个v9.0版本的API被废弃(deprecated),进而引发严重的生产问题。
方案:

  1. 修改本地配置,然后重新部署应用;
  2. 添加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

posted @ 2021-08-28 15:53  johnny233  阅读(628)  评论(0编辑  收藏  举报  来源