前言

我们这边有一个新项目,部门统一定好了技术框架的选型。用SpringCloud 全家桶作为微服务调用框架、SpringCloud Gateway作为服务网关、 Apollo做配置中心、Redis 做缓存、Sharding-jdbc做分表实现。

然后项目所有的配置项都会放在Apollo中存储,如果数据库链接相关信息、zk地址、Redis 链接信息。项目过程中Apollo客户端就会从配置中心拉取所有配置信息,然后在初始化对应的中间件。这个启动成功,一切都运行正常。

但是TeamLeader提出了一个要求,就是如果在本地开发的话,希望项目启动优先读取本地配置,只有当本地没有配置的话才会使用Apollo配置中心。目前来说就是在本地环境调试的时候,希望使用本地的zk作为注册中心,而不是Apollo里配置的dev环境的zk地址。

尝试

有了这个需求之后,就开始考虑如何支持这个功能。网上先搜索了一番,也没有发现比较好的实现方案。似乎事情变得开始不简单起来了,这就意味着只能自己搞定了,而且也没什么资料可以参考。于是就从Apollo的源码入手,准备研究一下启动过程中Apollo到底干了什么。

经过一番走马观花式的浏览,发现了apollo-client 包里/META-INF/spring.factories,文件中内容如下。

  1. org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  2. com.ctrip.framework.apollo.spring.boot.ApolloAutoConfiguration
  3. org.springframework.context.ApplicationContextInitializer=\
  4. com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer
  5. org.springframework.boot.env.EnvironmentPostProcessor=\
  6. com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer

根据直觉,猜想Apollo从远程拉取配置之后肯定会将这些值设置到Spring容器中的环境变量里。那么很明显,这个过程肯定和ApolloApplicationContextInitializer脱不了关系。

点进去看一下代码,发现了两个方法initialize、postProcessEnvironment。这两个方法最终都指向了initialize私有方法。

  1. /**
  2. * Initialize Apollo Configurations Just after environment is ready.
  3. *
  4. * @param environment
  5. */
  6. protected void initialize(ConfigurableEnvironment environment) {
  7. if (environment.getPropertySources().contains(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
  8. //already initialized
  9. return;
  10. }
  11. String namespaces = environment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_NAMESPACES, ConfigConsts.NAMESPACE_APPLICATION);
  12. logger.debug("Apollo bootstrap namespaces: {}", namespaces);
  13. List<String> namespaceList = NAMESPACE_SPLITTER.splitToList(namespaces);
  14. CompositePropertySource composite = new CompositePropertySource(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME);
  15. for (String namespace : namespaceList) {
  16. Config config = ConfigService.getConfig(namespace);
  17. composite.addPropertySource(configPropertySourceFactory.getConfigPropertySource(namespace, config));
  18. }
  19. environment.getPropertySources().addFirst(composite);
  20. }

这里注释也写的很清楚了,大意是说在Spring的环境准备好之后来就初始化Apollo的配置。看到这里我自认为问题已经解决了,就是我自己也写一个自己的ContextInitializer,保证在ApolloApplicationContextInitializer之后执行。把本地环境的配置放到Apollo配置之前就行。

  1. package com.shopee.banking.ekyc.spring.boot;
  2. import org.apache.commons.lang3.StringUtils;
  3. import org.springframework.context.ApplicationContextInitializer;
  4. import org.springframework.context.ConfigurableApplicationContext;
  5. import org.springframework.core.Ordered;
  6. import org.springframework.core.env.ConfigurableEnvironment;
  7. import org.springframework.core.env.PropertySource;
  8. /**
  9. * @description: 本地环境配置初始化
  10. * @className: com.shopee.banking.ekyc.spring.boot.LocalApplicationContextInitializer
  11. * @copyRight: www.shopee.com by SZDC-BankingGroup
  12. * @author: xiaoqiangzhang
  13. * @createDate: 2020/7/7
  14. */
  15. public class LocalApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
  16. /**
  17. * 初始化顺序(最后)
  18. */
  19. private final int DEFAULT_ORDER = Ordered.LOWEST_PRECEDENCE - 1;
  20. /**
  21. * Apollo property source name
  22. */
  23. private final String APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME = "ApolloBootstrapPropertySources";
  24. /**
  25. * local property source name
  26. */
  27. private final String LOCAL_PROPERTY_SOURCE_NAME = "applicationConfig: [classpath:/application-local.yml]";
  28. /**
  29. * 是否使用本地配置
  30. */
  31. private final String LOCAL_CONFIG_USED = "local.config.used";
  32. /**
  33. * local env
  34. */
  35. private final String LOCAL_ENV = "LOCAL";
  36. @Override
  37. public void initialize(ConfigurableApplicationContext applicationContext) {
  38. ConfigurableEnvironment environment = applicationContext.getEnvironment();
  39. String activeProfile = environment.getActiveProfiles()[0];
  40. //not local env
  41. if (StringUtils.isBlank(activeProfile) || !LOCAL_ENV.equalsIgnoreCase(activeProfile.trim())) {
  42. return;
  43. }
  44. Boolean localConfigUsed = environment.getProperty(LOCAL_CONFIG_USED, Boolean.class);
  45. boolean apollo = environment.getPropertySources().contains(APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME);
  46. boolean activePropertySource = environment.getPropertySources().contains(LOCAL_PROPERTY_SOURCE_NAME);
  47. if (localConfigUsed != null && localConfigUsed && apollo && activePropertySource) {
  48. PropertySource localPropertySource = environment.getPropertySources().get(LOCAL_PROPERTY_SOURCE_NAME);
  49. environment.getPropertySources().addFirst(localPropertySource);
  50. }
  51. }
  52. @Override
  53. public int getOrder() {
  54. return DEFAULT_ORDER;
  55. }
  56. }

本地配置大概长这样,我在本地环境配置的zk地址为: localhost:2181,本地的xxl-job调度中心地址为: http://127.0.0.1:8080/xxl-job-admin , 启动的时候想用本地的地址来初始化zk client、xxl-job client。
image.png

设置启动参数,-Dspring.profiles.active=local,点击运行。满怀欣喜的以为问题搞定了,但是现实跟我开了个笑话,通过启动日志观察到zk client初始化的地址是取自Apollo配置中心。开始以为自己眼花了,于是又多试了两次,没想到结果依然是从配置中心拿到的zk地址。

似乎陷入到困境,决定打下断点调试一下spring启动过程中环境变量到底是怎么变化的。第一次断点打在SpringApplication类的prepareContext方法上,等执行完这个方法后看一些context的environment中名称为ApolloBootstrapPropertySources的PropertySource排在第一位。
image.png
接下来继续在refreshContext打一个断点,跟进去执行完后。

曙光

最后