美文网首页
Apollo 公共 Namespace 使用

Apollo 公共 Namespace 使用

作者: _晓__ | 来源:发表于2019-05-29 14:19 被阅读0次

    背景

    现在使用 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 不知如何改造的可联系。
    对代码不明白地方可联系讨论或对写的不好的地方望不吝指教。

    相关文章

      网友评论

          本文标题:Apollo 公共 Namespace 使用

          本文链接:https://www.haomeiwen.com/subject/uyjwoqtx.html