Apollo 公共 Namespace 使用

背景

现在使用 Apollo 配置中心框架的公司越来越多了,也希望写这篇文章对刚入手 Apollo 的同学有所帮助,对系统做出更多更好用的功能。

问题举例

  • 私有 Namespace 和 公共 Namespace 区别?
  • 如何更好的使用公共 Namespace?

所需知识

Apollo Java客户端使用指南

问题解决

私有 Namespace 和 公共 Namespace 区别

  • 私有 Namespace 配置信息无法提供给其他项目(Apollo 中项目)共用,若数据库连接变更时,需每个项目修改对应数据库配置。
  • 公共 Namespace 配置信息可提供给其他项目(Apollo 中项目)共用,其他项目可通过 Apollo 提供的 关联公共 Namespace 功能进行关联,即可使用公共 Namespace 配置信息,也可以覆盖公共 Namespace 配置信息,使用自定义配置。若数据库连接变更时,只需修改公共 Namespace 中的数据库配置。
    关联 Namespace.png
  • 可根据功能划分公共 Namespace,项目按需 关联 Namespace
    划分公共 Namespace.png

更好的使用公共 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.propertiesbootstrap.yml,获取不到这两个配置文件时会初始化一些默认数据再执行一次 SpringApplication.run() 方法,导致 ApolloContextInitializer 会被执行两次,可通过 spring.cloud.bootstrap.enabled=false 禁用 BootstrapApplicationListener 执行逻辑。
  • 若针对 Spring 框架集成 Apollo 想使用通用的公共 Namespace 不知如何改造的可联系。
对代码不明白地方可联系讨论或对写的不好的地方望不吝指教。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342