前言
我们这边有一个新项目,部门统一定好了技术框架的选型。用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,文件中内容如下。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\com.ctrip.framework.apollo.spring.boot.ApolloAutoConfigurationorg.springframework.context.ApplicationContextInitializer=\com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializerorg.springframework.boot.env.EnvironmentPostProcessor=\com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer
根据直觉,猜想Apollo从远程拉取配置之后肯定会将这些值设置到Spring容器中的环境变量里。那么很明显,这个过程肯定和ApolloApplicationContextInitializer脱不了关系。
点进去看一下代码,发现了两个方法initialize、postProcessEnvironment。这两个方法最终都指向了initialize私有方法。
/*** Initialize Apollo Configurations Just after environment is ready.** @param environment*/protected void initialize(ConfigurableEnvironment environment) {if (environment.getPropertySources().contains(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME)) {//already initializedreturn;}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) {Config config = ConfigService.getConfig(namespace);composite.addPropertySource(configPropertySourceFactory.getConfigPropertySource(namespace, config));}environment.getPropertySources().addFirst(composite);}
这里注释也写的很清楚了,大意是说在Spring的环境准备好之后来就初始化Apollo的配置。看到这里我自认为问题已经解决了,就是我自己也写一个自己的ContextInitializer,保证在ApolloApplicationContextInitializer之后执行。把本地环境的配置放到Apollo配置之前就行。
package com.shopee.banking.ekyc.spring.boot;import org.apache.commons.lang3.StringUtils;import org.springframework.context.ApplicationContextInitializer;import org.springframework.context.ConfigurableApplicationContext;import org.springframework.core.Ordered;import org.springframework.core.env.ConfigurableEnvironment;import org.springframework.core.env.PropertySource;/*** @description: 本地环境配置初始化* @className: com.shopee.banking.ekyc.spring.boot.LocalApplicationContextInitializer* @copyRight: www.shopee.com by SZDC-BankingGroup* @author: xiaoqiangzhang* @createDate: 2020/7/7*/public class LocalApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {/*** 初始化顺序(最后)*/private final int DEFAULT_ORDER = Ordered.LOWEST_PRECEDENCE - 1;/*** Apollo property source name*/private final String APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME = "ApolloBootstrapPropertySources";/*** local property source name*/private final String LOCAL_PROPERTY_SOURCE_NAME = "applicationConfig: [classpath:/application-local.yml]";/*** 是否使用本地配置*/private final String LOCAL_CONFIG_USED = "local.config.used";/*** local env*/private final String LOCAL_ENV = "LOCAL";@Overridepublic void initialize(ConfigurableApplicationContext applicationContext) {ConfigurableEnvironment environment = applicationContext.getEnvironment();String activeProfile = environment.getActiveProfiles()[0];//not local envif (StringUtils.isBlank(activeProfile) || !LOCAL_ENV.equalsIgnoreCase(activeProfile.trim())) {return;}Boolean localConfigUsed = environment.getProperty(LOCAL_CONFIG_USED, Boolean.class);boolean apollo = environment.getPropertySources().contains(APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME);boolean activePropertySource = environment.getPropertySources().contains(LOCAL_PROPERTY_SOURCE_NAME);if (localConfigUsed != null && localConfigUsed && apollo && activePropertySource) {PropertySource localPropertySource = environment.getPropertySources().get(LOCAL_PROPERTY_SOURCE_NAME);environment.getPropertySources().addFirst(localPropertySource);}}@Overridepublic int getOrder() {return DEFAULT_ORDER;}}
本地配置大概长这样,我在本地环境配置的zk地址为: localhost:2181,本地的xxl-job调度中心地址为: http://127.0.0.1:8080/xxl-job-admin , 启动的时候想用本地的地址来初始化zk client、xxl-job client。
设置启动参数,-Dspring.profiles.active=local,点击运行。满怀欣喜的以为问题搞定了,但是现实跟我开了个笑话,通过启动日志观察到zk client初始化的地址是取自Apollo配置中心。开始以为自己眼花了,于是又多试了两次,没想到结果依然是从配置中心拿到的zk地址。
似乎陷入到困境,决定打下断点调试一下spring启动过程中环境变量到底是怎么变化的。第一次断点打在SpringApplication类的prepareContext方法上,等执行完这个方法后看一些context的environment中名称为ApolloBootstrapPropertySources的PropertySource排在第一位。
接下来继续在refreshContext打一个断点,跟进去执行完后。
