背景
现在使用 Apollo 配置中心框架的公司越来越多了,也希望写这篇文章对刚入手 Apollo 的同学有所帮助,对系统做出更多更好用的功能。
问题举例
- 私有 Namespace 和 公共 Namespace 区别?
- 如何更好的使用公共 Namespace?
所需知识
问题解决
私有 Namespace 和 公共 Namespace 区别
- 私有 Namespace 配置信息无法提供给其他项目(Apollo 中项目)共用,若数据库连接变更时,需每个项目修改对应数据库配置。
- 公共 Namespace 配置信息可提供给其他项目(Apollo 中项目)共用,其他项目可通过 Apollo 提供的
关联公共 Namespace
功能进行关联,即可使用公共 Namespace 配置信息,也可以覆盖公共 Namespace 配置信息,使用自定义配置。若数据库连接变更时,只需修改公共 Namespace 中的数据库配置。
- 可根据功能划分公共 Namespace,项目按需
关联 Namespace
。
更好的使用公共 Namespace
由于 Apollo 默认只是把 application 加入 Spring PropertySources 中,会导致 Spring 初始化 mysql,redis 等组件时,无法获取到公共 Namespace 中的配置信息,所以 Apollo 提供了配置方式 apollo.bootstrap.enabled=true
apollo.bootstrap.namespaces=redis,mysql(多个使用逗号隔开)
,把公共 Namespace 加入 Spring PropertySources 中,但这种方式有个弊端就是公共 Namespace 需人工配置加载,若再新增一个公共 Namespace,还需修改 apollo.bootstrap.namespaces
(对加载顺序不了解,可看 ApolloApplicationContextInitializer),因此,我自己实现获取公共 Namespace 加入 Spring PropertySources 的过程并支持公共 Namespace 配置的自动更新,无需配置 @EnableApolloConfig
,还支持 @ConfigurationProperties
配置类自动更新。
import cn.hutool.core.text.StrFormatter;
import com.ctrip.framework.apollo.Config;
import com.ctrip.framework.apollo.ConfigChangeListener;
import com.ctrip.framework.apollo.ConfigService;
import com.ctrip.framework.apollo.core.ConfigConsts;
import com.ctrip.framework.apollo.core.dto.ApolloConfig;
import com.ctrip.framework.apollo.core.dto.ServiceDTO;
import com.ctrip.framework.apollo.core.utils.StringUtils;
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import com.ctrip.framework.apollo.spring.config.ConfigPropertySourceFactory;
import com.ctrip.framework.apollo.spring.config.PropertySourcesConstants;
import com.ctrip.framework.apollo.spring.util.SpringInjector;
import com.ctrip.framework.apollo.util.http.HttpRequest;
import com.ctrip.framework.apollo.util.http.HttpResponse;
import com.ctrip.framework.apollo.util.http.HttpUtil;
import com.ctrip.framework.foundation.internals.provider.DefaultApplicationProvider;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.gson.reflect.TypeToken;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.io.UrlResource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import java.io.IOException;
import java.net.URL;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 需在resources目录下新增META-INF/spring.factories文件,文件内容为:
* org.springframework.boot.env.EnvironmentPostProcessor=\
* 包名.ApolloContextInitializer
* org.springframework.context.ApplicationContextInitializer=\
* 包名.ApolloContextInitializer
*
* 对 EnvironmentPostProcessor 和 ApplicationContextInitializer 不清楚的,可自行百度了解
*/
@Slf4j
public class ApolloContextInitializer implements EnvironmentPostProcessor, ApplicationContextInitializer<ConfigurableApplicationContext> {
/**
* 配置是否执行
*/
private final static boolean APOLLO_ENABLE = Boolean.valueOf(System.getProperty(ApolloConfigConsts.APOLLO_ENABLE, Boolean.TRUE.toString()));
private final static Set<Config> CONFIGS = Sets.newHashSet();
static {
if (APOLLO_ENABLE) {
// 检查重复配置 spring.factories
ApolloContextInitializer.checkDuplicate(EnvironmentPostProcessor.class);
}
}
private static void checkDuplicate(Class<EnvironmentPostProcessor> factoryClass) {
try {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(SpringFactoriesLoader.FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(SpringFactoriesLoader.FACTORIES_RESOURCE_LOCATION));
List<String> apolloContextInitializerList = Lists.newArrayListWithCapacity(5);
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
String urlStr = url.toString();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<Object, Object> entry : properties.entrySet()) {
String factoryClassName = ((String) entry.getKey()).trim();
for (String factoryName : org.springframework.util.StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
factoryName = factoryName.trim();
if (factoryClassName.equals(factoryClass.getName()) && factoryName.equals(ApolloContextInitializer.class.getName())) {
apolloContextInitializerList.add(StrFormatter.format("{}#{}", urlStr, factoryName));
}
}
}
}
Assert.isTrue(apolloContextInitializerList.size() <= 1,
StrFormatter.format("项目中发现有{}个{}都配置了{},重复配置文件路径=>{},请检查lib目录下是否存在重复配置",
apolloContextInitializerList.size(), SpringFactoriesLoader.FACTORIES_RESOURCE_LOCATION,
ApolloContextInitializer.class.getName(), apolloContextInitializerList.stream().collect(Collectors.joining(",")))
);
} catch (IOException e) {
throw new IllegalArgumentException(StrFormatter.format("加载{}文件异常,请检查是否存在该配置文件", SpringFactoriesLoader.FACTORIES_RESOURCE_LOCATION), e);
}
}
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
initialize(environment);
}
private void initialize(ConfigurableEnvironment environment) {
if (!APOLLO_ENABLE) {
return;
}
List<ApolloConfig> apolloConfigList = allApolloConfig();
Assert.notEmpty(apolloConfigList, StrFormatter.format("该项目没有Apollo配置项,如无需使用Apollo,请在VM参数中配置-D{}=false", ApolloConfigConsts.APOLLO_ENABLE));
CompositePropertySource composite = new CompositePropertySource(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME);
ConfigPropertySourceFactory configPropertySourceFactory = SpringInjector.getInstance(ConfigPropertySourceFactory.class);
for (ApolloConfig apolloConfig : apolloConfigList) {
Config config = ConfigService.getConfig(apolloConfig.getNamespaceName());
composite.addPropertySource(configPropertySourceFactory.getConfigPropertySource(apolloConfig.getNamespaceName(), config));
CONFIGS.add(config);
}
environment.getPropertySources().addFirst(composite);
}
private List<ApolloConfig> allApolloConfig() {
DefaultApplicationProvider defaultApplicationProvider = getApplicationProvider();
String configServiceUri = getConfigServiceUri(defaultApplicationProvider);
ServiceDTO adminService = getAdminService(configServiceUri);
return allApolloConfig(defaultApplicationProvider, adminService);
}
private List<ApolloConfig> allApolloConfig(DefaultApplicationProvider defaultApplicationProvider, ServiceDTO adminService) {
String clustersName = System.getProperty(ConfigConsts.APOLLO_CLUSTER_KEY, ConfigConsts.CLUSTER_NAME_DEFAULT);
String namespacesUrl = StrFormatter.format(ApolloConfigConsts.NAMESPACES_URL_PATTERN, adminService.getHomepageUrl(), defaultApplicationProvider.getAppId(), clustersName);
try {
HttpUtil httpUtil = new HttpUtil();
HttpRequest request = new HttpRequest(namespacesUrl);
request.setConnectTimeout(ApolloConfigConsts.TIMEOUT);
request.setReadTimeout(ApolloConfigConsts.TIMEOUT);
HttpResponse<List<ApolloConfig>> response = httpUtil.doGet(request, new TypeToken<List<ApolloConfig>>() {}.getType());
return response.getBody();
} catch (Exception e) {
throw new IllegalStateException(StrFormatter.format("获取Apollo信息异常,请求url=>{},请检查配置", namespacesUrl), e);
}
}
private DefaultApplicationProvider getApplicationProvider() {
DefaultApplicationProvider defaultApplicationProvider = new DefaultApplicationProvider();
defaultApplicationProvider.initialize();
return defaultApplicationProvider;
}
private ServiceDTO getAdminService(String metaServiceUri) {
String adminServiceUrl = StrFormatter.format(ApolloConfigConsts.ADMIN_SERVICE_URL_PATTERN, metaServiceUri);
try {
HttpUtil httpUtil = new HttpUtil();
HttpRequest request = new HttpRequest(adminServiceUrl);
request.setConnectTimeout(ApolloConfigConsts.TIMEOUT);
request.setReadTimeout(ApolloConfigConsts.TIMEOUT);
HttpResponse<List<ServiceDTO>> response = httpUtil.doGet(request, new TypeToken<List<ServiceDTO>>() {}.getType());
List<ServiceDTO> adminServiceList = response.getBody();
Assert.notEmpty(adminServiceList, StrFormatter.format("无Apollo AdminService服务实例,请检查服务,如无需使用Apollo,请在VM参数中配置-D{}=false", ApolloConfigConsts.APOLLO_ENABLE));
return adminServiceList.get(0);
} catch (Exception e) {
throw new IllegalStateException(StrFormatter.format("获取Apollo AdminService服务实例信息异常,请求url=>{},请检查配置,如无需使用Apollo,请在VM参数中配置-D{}=false", adminServiceUrl, ApolloConfigConsts.APOLLO_ENABLE), e);
}
}
private String getConfigServiceUri(DefaultApplicationProvider defaultApplicationProvider) {
String configServiceUri = System.getProperty(ConfigConsts.APOLLO_META_KEY);
if (StringUtils.isBlank(configServiceUri)) {
configServiceUri = defaultApplicationProvider.getProperty(ConfigConsts.APOLLO_META_KEY, StringUtils.EMPTY);
}
Assert.hasText(configServiceUri, StrFormatter.format("Apollo ConfigService uri未配置,如无需使用Apollo,请在VM参数中配置-D{}=false", ApolloConfigConsts.APOLLO_ENABLE));
configServiceUri = configServiceUri.indexOf(",") > 0 ? configServiceUri.split(",")[0] : configServiceUri;
return configServiceUri;
}
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
if (CollectionUtils.isEmpty(CONFIGS)) {
return;
}
// 创建 Apollo 监听器
ConfigRefreshListener configRefreshListener = new ConfigRefreshListener(applicationContext);
// 所有 Namespace Config 对象添加监听器
CONFIGS.stream().forEach(config -> config.addChangeListener(configRefreshListener));
}
}
@AllArgsConstructor
class ConfigRefreshListener implements ConfigChangeListener {
private ApplicationContext applicationContext;
@Override
public void onChange(ConfigChangeEvent changeEvent) {
// 使用 Spring Cloud EnvironmentChangeEvent 刷新配置
applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));
}
}
public interface ApolloConfigConsts {
int TIMEOUT = 3000;
String APOLLO_ENABLE = "apollo.enable";
String ADMIN_SERVICE_URL_PATTERN = "{}services/admin";
String NAMESPACES_URL_PATTERN = "{}apps/{}/clusters/{}/namespaces";
}
PS:
- 由于引进了 spring-cloud-context 包,导致服务启动时会执行 Spring Cloud 监听器
org.springframework.cloud.bootstrap.BootstrapApplicationListener
,该监听器执行时会获取bootstrap.properties
或bootstrap.yml
,获取不到这两个配置文件时会初始化一些默认数据再执行一次SpringApplication.run()
方法,导致ApolloContextInitializer
会被执行两次,可通过spring.cloud.bootstrap.enabled=false
禁用BootstrapApplicationListener
执行逻辑。 - 若针对 Spring 框架集成 Apollo 想使用通用的公共
Namespace
不知如何改造的可联系。