踩坑Apollo配置namespace加载顺序优先级问题,更具体点应该表述为【踩坑Spring框架中针对多配置属性源取值的逻辑】。

背景

SprintBoot项目中使用了携程开源的Apollo组件完成分布式配置功能。其中有一个common包负责公用一些基础配置,工程特有的配置通过namespace进行区分。

配置项长这样:

apollo.bootstrap.enabled = true
apollo.bootstrap.namespaces = common,application

问题表现: 在管理界面修改namespace为application的某一个项时,发现配置不生效。

定位

去找了下中文文档,针对Java客户端使用,集成到Spring的描述中针对此项有具体解释:注入多个namespace,并且指定顺序

此处规则很明确:在前面的namespace优先级最高,谁在前面哪个值生效。

源码解析

Apollo中通过ApolloApplicationContextInitializer这个类implements了Spring框架中的ApplicationContextInitializer类,内部实现了initialize(ConfigurableApplicationContext context)方法。这里负责把各命名空间中的配置项加载到java对象(PropertySources)中,交给spring管理起来。

我们详细看看initialize(ConfigurableEnvironment environment)方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
   * 环境变量初始化动作
   *
   * @param environment
   */
  protected void initialize(ConfigurableEnvironment environment) {

    if (environment.getPropertySources().contains(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
      // 这里判断apollo启动配置中是否已注入 ApolloPropertySources,fail-fast逻辑
      return;
    }
    // 从 apollo.bootstrap.namespaces 中取配置的命名空间,默认使用 application。此处其实已经表明下游自定义的项应该配在application空间中,但是要保证namespace顺序application在前面。
    String namespaces = environment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_NAMESPACES, ConfigConsts.NAMESPACE_APPLICATION);
    logger.debug("Apollo bootstrap namespaces: {}", namespaces);
    List<String> namespaceList = NAMESPACE_SPLITTER.splitToList(namespaces);

    CompositePropertySource composite = new CompositePropertySource(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME);
    for (String namespace : namespaceList) {
      // 每个命名空间会对应到管理界面中配置的数据,java中使用 Config 表示这些配置
      Config config = ConfigService.getConfig(namespace);
      // 通过命名空间与config对象拿到PropertySource(可遍历的),添加到apollo启动配置对象中。
      composite.addPropertySource(configPropertySourceFactory.getConfigPropertySource(namespace, config));
    }
    // 将组装好的组合配置源扔进spring的环境对象中,置于首位
    environment.getPropertySources().addFirst(composite);
  }

我们需要关注下CompositePropertySource.addPropertySource(PropertySource<?> propertySource)方法:

1
2
3
4
5
public void addPropertySource(PropertySource<?> propertySource) {
    this.propertySources.add(propertySource);
}
// 而这里的propertySources可以观察到是一个链表结构,保证了有序。
private final Set<PropertySource<?>> propertySources = new LinkedHashSet<>();

propertySources在spring中表示多配置属性源,而这个initialize(ConfigurableEnvironment environment)本质上就是把我们在Apollo界面中配置的数据加载到spring中了。到这里,数据都加载了进来。

多个命名空间的配置配置源封装到了名为ApolloBootstrapPropertySourcesCompositePropertySource中。根据上面注释说明,propertySources添加子项的时候通过LinkedHashSet保证了有序。

而获取某个key对应属性值的方法是:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Override
	@Nullable
	public Object getProperty(String name) {
		for (PropertySource<?> propertySource : this.propertySources) {
            // 有序的集合中进行迭代,找到就返回,因此先添加进来的 PropertySource 会作为最终的值
			Object candidate = propertySource.getProperty(name);
			if (candidate != null) {
				return candidate;
			}
		}
		return null;
	}

小结

所以我们当时那个工程中关于apollo.bootstrap.namespaces的配置应该改为application,common

而这个实现的逻辑位于Spring中对于环境配置源的CompositePropertySource中,其中通过链表添加多个命名空间对应的PropertySource对象,获取同个key值时,取最先加入的命名空间对应的配置。