Bootstrap

Apollo 1.9.2 部署手册实现本地零配置

1:阿里云部署Apollo,默认注册的Eurake是内网IP地址,需要修改AdminService和ConfigService的application.yml配置信息

eureka:
  instance:
    hostname: ${hostname:localhost}
    preferIpAddress: true
    status-page-url-path: /info
    health-check-url-path: /health
    ip-address: 139.196.208.53

2:Apollo的quick start项目,默认注册内网IP,需要修改demo.sh如下配置

#先修改数据库
# apollo config db info
apollo_config_db_url="jdbc:mysql://139.196.208.53:3306/ApolloConfigDB?characterEncoding=utf8&serverTimezone=Asia/Shanghai"
apollo_config_db_username=root
apollo_config_db_password=Zs11195310


# apollo portal db info
apollo_portal_db_url="jdbc:mysql://139.196.208.53:3306/ApolloPortalDB?characterEncoding=utf8&serverTimezone=Asia/Shanghai"
apollo_portal_db_username=root
apollo_portal_db_password=Zs11195310



# 需要修改地址为外网IP地址
config_server_url=http://139.196.208.53:8080
admin_server_url=http://139.196.208.53:8090
eureka_service_url=$config_server_url/eureka/
portal_url=http://139.196.208.53:8070


#另外添加eureka.instance.ip指定外网IP地址
if [ "$1" = "start" ] ; then
  echo "==== starting service ===="
  echo "Service logging file is $SERVICE_LOG"
  export APP_NAME="apollo-service"
  export JAVA_OPTS="$SERVER_JAVA_OPTS -Dlogging.file.name=./apollo-service.log 
    -Dspring.datasource.url=$apollo_config_db_url 
    -Dspring.datasource.username=$apollo_config_db_username 
    -Dspring.datasource.password=$apollo_config_db_password 
    -Deureka.instance.ip-address=139.196.208.53"

3:修改Apollo源码实现本地零配置
3.1:修改读取环境变量env为spring.profiles.active

# DefaultServerProvider   initEnvType
#先修改读取ENV为读取spring.profiles.active
//m_env = System.getProperty("env");
//修改读取env为spring.profiles.active
m_env = System.getProperty("spring.profiles.active");

3.2:修改默认本地缓存路径

#ConfigUtil
/**
* 修改默认缓存路径
* @return
*/
public String getDefaultLocalCacheDir() {
  String cacheRoot = this.isOSWindows() ? "C:\\app\\data\\%s" : "/app/data/%s";
  return String.format(cacheRoot, getAppId());
}

3.3:修改apollo.bootstrap.enabled和apollo.bootstrap.eagerLoad.enabled未设置默认为true

#ApolloApplicationContextInitializer   postProcessEnvironment

/**
* 默认设置将Apollo配置加载提到初始化日志系统之前
*/
Boolean eagerLoadEnabled = configurableEnvironment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_EAGER_LOAD_ENABLED, Boolean.class, true);

/**
* 默认设置apollo.bootstrap.enabled为true
*/
Boolean bootstrapEnabled = configurableEnvironment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED, Boolean.class, true);


#ApolloApplicationContextInitializer  initialize

/**
* 未设置默认修改为true
*/
if (!environment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED, Boolean.class, true)) {
  logger.debug("Apollo bootstrap config is not enabled for context {}, see property: ${{}}", context, PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED);
  return;
}

5:apollo-core模块的resources目录下添加配置文件apollo-env.properties

local.meta=http://localhost:8080
dev.meta=http://139.196.208.53:8080
fat.meta=http://dcc.pre.cloud.org
uat.meta=http://dcc.gr.cloud.org
lpt.meta=http://dcc.yc.cloud.org
pro.meta=http://dcc.cloud.org

6:修改MetaDomainConsts代码

public class MetaDomainConsts {


    private static final Map<Env, String> domains;
    private static final String DEFAULT_META_URL = "http://apollo.meta";

    /**
     * Return one meta server address. If multiple meta server addresses are configured, will select
     * one.
     */
    public static String getDomain(Env env) {
        return String.valueOf(domains.get(env));
    }

    static {
        domains = new HashMap<>();
        Properties prop = new Properties();
        prop = ResourceUtils.readConfigFile("apollo-env.properties", prop);
        final Properties env = System.getProperties();
        domains.put(Env.LOCAL, env.getProperty("local_meta", prop.getProperty("local.meta", DEFAULT_META_URL)));
        domains.put(Env.DEV, env.getProperty("dev_meta", prop.getProperty("dev.meta", DEFAULT_META_URL)));
        domains.put(Env.FAT, env.getProperty("fat_meta", prop.getProperty("fat.meta", DEFAULT_META_URL)));
        domains.put(Env.UAT, env.getProperty("uat_meta", prop.getProperty("uat.meta", DEFAULT_META_URL)));
        domains.put(Env.LPT, env.getProperty("lpt_meta", prop.getProperty("lpt.meta", DEFAULT_META_URL)));
        domains.put(Env.PRO, env.getProperty("pro_meta", prop.getProperty("pro.meta", DEFAULT_META_URL)));
    }

}

7:注意默认情况下@ConfigurationProperties注解标注的配置类是不会实时更新的,官方原话
需要注意的是,@ConfigurationProperties如果需要在Apollo配置变化时自动更新注入的值,需要配合使用EnvironmentChangeEvent或RefreshScope。
相关代码实现,可以参考apollo-use-cases项目中的ZuulPropertiesRefresher.java和apollo-demo项目中的SampleRedisConfig.java以及SpringBootApolloRefreshConfig.java

7.1:在apollo-client com.ctrip.framework.apollo.spring.config 新建ApolloRefreshConfig

package com.ctrip.framework.apollo.spring.config;


import com.ctrip.framework.apollo.build.ApolloInjector;
import com.ctrip.framework.apollo.internals.AbstractConfig;
import com.ctrip.framework.apollo.model.ConfigChange;
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
import com.ctrip.framework.apollo.util.ConfigUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;


/**
* @description:ConfigurationProperties配置实现自动更新
* ConfigurationProperties如果需要在Apollo配置变化时自动更新注入的值,需要配合使用EnvironmentChangeEvent或RefreshScope
* @author: zhou shuai
* @date: 2022/4/15 12:47
* @version: v1
*/
public class ApolloRefreshConfig implements ApplicationContextAware {


    private static final Logger logger = LoggerFactory.getLogger(ApolloRefreshConfig.class);


    private ApplicationContext applicationContext;
    private ConfigUtil m_configUtil = ApolloInjector.getInstance(ConfigUtil.class);


    public ApolloRefreshConfig() {
    }

    @ApolloConfigChangeListener
    public void onChange(ConfigChangeEvent changeEvent) {
        if (this.m_configUtil.isAutoUpdateInjectedSpringPropertiesEnabled()) {
            changeEvent.changedKeys().stream().map(changeEvent::getChange).forEach(change -> {
              logger.info("found config change - namespace: {},  key: {}, oldValue: {}, newValue: {}, changeType: {}",
              change.getNamespace(), change.getPropertyName(), change.getOldValue(),
              change.getNewValue(), change.getChangeType());
            });
            //更新相应的bean的属性值,主要是存在@ConfigurationProperties注解的bean
            this.applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));
        }
    }

    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

}

7.2:另外需求在apollo-client pom添加以下依赖

<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-context</artifactId>
   <version>RELEASE</version>
</dependency>

7.3:在com.ctrip.framework.apollo.spring.boot包下的ApolloAutoConfiguration 添加该bean

package com.ctrip.framework.apollo.spring.boot;


import com.ctrip.framework.apollo.spring.config.ApolloRefreshConfig;
import com.ctrip.framework.apollo.spring.config.ConfigPropertySourcesProcessor;
import com.ctrip.framework.apollo.spring.config.PropertySourcesConstants;
import com.ctrip.framework.apollo.spring.config.PropertySourcesProcessor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


@Configuration
@ConditionalOnProperty(
        value = PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED,
        matchIfMissing = true)
@ConditionalOnMissingBean(PropertySourcesProcessor.class)
public class ApolloAutoConfiguration {


  @Bean
  public ConfigPropertySourcesProcessor configPropertySourcesProcessor() {
    return new ConfigPropertySourcesProcessor();
  }

  @Bean
  public ApolloRefreshConfig apolloRefreshConfig() {
    return new ApolloRefreshConfig();
  }
}

8:自动获取appId所有的namespaces

@RestController
@RequestMapping("/namespaces")
public class NamespaceController {


    @Autowired
    private NamespaceService namespaceService;


    @GetMapping(value = "/{appId}/{clusterName}")
    public List<NamespaceDTO> find(@PathVariable("appId") String appId,
                                   @PathVariable("clusterName") String clusterName) {
        List<Namespace> groups = namespaceService.findNamespaces(appId, clusterName);
        if(CollectionUtils.isEmpty(groups)){
            groups = namespaceService.findNamespaces(appId, ConfigConsts.CLUSTER_NAME_DEFAULT);
        }
        return BeanUtils.batchTransform(NamespaceDTO.class, groups);
    }

}

9:修改apollo-client包下com.ctrip.framework.apollo.spring.boot ApolloApplicationContextInitializer代码

protected void initialize(ConfigurableEnvironment environment) {


    if (environment.getPropertySources().contains(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
        //already initialized, replay the logs that were printed before the logging system was initialized
        DeferredLogger.replayTo();
        return;
    }


    String namespaces = environment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_NAMESPACES, ConfigConsts.NAMESPACE_APPLICATION);
    logger.debug("Apollo bootstrap namespaces: {}", namespaces);
    //List<String> namespaceList = NAMESPACE_SPLITTER.splitToList(namespaces);
    final Set<String> namespaceList = NAMESPACE_SPLITTER.splitToList(namespaces).stream().collect(Collectors.toSet());

    try {
        List<NamespaceDTO> namespaceDTOs = this.remoteNamespaceRepository.getNamespacesByAppIdAndClusterName(this.m_configUtil.getAppId(), this.m_configUtil.getCluster());
        namespaceDTOs.forEach((namespaceDTO) -> {
            namespaceList.add(namespaceDTO.getNamespaceName());
        });
    } catch (Exception e) {
        logger.error("get namespaces by appId from config service error:", e);
    }


    CompositePropertySource composite;
    final ConfigUtil configUtil = ApolloInjector.getInstance(ConfigUtil.class);
    if (configUtil.isPropertyNamesCacheEnabled()) {
        composite = new CachedCompositePropertySource(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME);
    } else {
        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);RemoteNamespaceRepository
}

10:在apollo-client 包com.ctrip.framework.apollo.internals下新增RemoteNamespaceRepository

package com.ctrip.framework.apollo.internals;


import com.ctrip.framework.apollo.build.ApolloInjector;
import com.ctrip.framework.apollo.core.dto.NamespaceDTO;
import com.ctrip.framework.apollo.core.dto.ServiceDTO;
import com.ctrip.framework.apollo.core.utils.StringUtils;
import com.ctrip.framework.apollo.exceptions.ApolloConfigException;
import com.ctrip.framework.apollo.util.http.HttpClient;
import com.ctrip.framework.apollo.util.http.HttpRequest;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.escape.Escaper;
import com.google.common.net.UrlEscapers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


import java.util.*;
import java.util.stream.Collectors;


/**
* @description: http调用获取appId-cluster下的 namespaces
* @author: zhou shuai
* @date: 2022/4/16 19:12
* @version: v1
*/
public class RemoteNamespaceRepository {


    private static final Logger logger = LoggerFactory.getLogger(RemoteNamespaceRepository.class);
    private ConfigServiceLocator m_serviceLocator = ApolloInjector.getInstance(ConfigServiceLocator.class);
    private HttpClient m_httpClient = ApolloInjector.getInstance(HttpClient.class);
    private static final Escaper pathEscaper = UrlEscapers.urlPathSegmentEscaper();
    private static final Joiner.MapJoiner MAP_JOINER = Joiner.on("&").withKeyValueSeparator("=");

    public List<NamespaceDTO> getNamespacesByAppIdAndClusterName(final String appId, final String clusterName) {
        RemoteNamespaceRepository.logger.info("load namespaces from config service...");
        final String path = "namespaces/%s/%s";
        List<String> pathParams =
                Lists.newArrayList(pathEscaper.escape(appId), pathEscaper.escape(clusterName));
        String pathExpanded = String.format(path, pathParams.toArray());
        Map<String, String> queryParams = Maps.newHashMap();
        if (!queryParams.isEmpty()) {
            pathExpanded += "?" + MAP_JOINER.join(queryParams);
        }
        Map<Integer, String> namespacesUrls = getNamespacesUrl(pathExpanded);
        int maxRetries = namespacesUrls.size();
        for (int i = 0; i < maxRetries; i++) {
            String namespacesUrl = namespacesUrls.get(i);
            try {
                if (!StringUtils.isBlank(namespacesUrl)) {
                    return Arrays.asList(m_httpClient.doGet(new HttpRequest(namespacesUrl), NamespaceDTO[].class).getBody());
                }
            } catch (Exception e) {
                logger.warn("load namespaces from config service error,url:{}", namespacesUrl);
            }
        }
        return new ArrayList<>();
    }


    private Map<Integer, String> getNamespacesUrl(String pathExpanded) {
        List<String> namespacesUrls = this.getConfigServices().stream().map((serviceDTO) -> {
            String homepageUrl = serviceDTO.getHomepageUrl();
            if (!homepageUrl.endsWith("/")) {
                homepageUrl += "/";
            }
            return homepageUrl + pathExpanded;
        }).collect(Collectors.toList());
        Map<Integer, String> namespaceUrlMap = new HashMap();
        for(int i = 0; i < namespacesUrls.size(); ++i) {
            namespaceUrlMap.put(i, namespacesUrls.get(i));
        }
        return namespaceUrlMap;
    }


    private List<ServiceDTO> getConfigServices() {
        final List<ServiceDTO> services = this.m_serviceLocator.getConfigServices();
        if (services.size() == 0) {
            throw new ApolloConfigException("No available config service");
        }
        return services; ApolloConfigChangeListener  ConfigurationProperties
    }


}

11:添加ApolloConfigChangeListener注解的namespace,实现其他namespace的ConfigurationProperties动态更新

#ApolloAnnotationProcessor   processApolloConfigChangeListener

private void processApolloConfigChangeListener(final Object bean, final Method method) {
    ApolloConfigChangeListener annotation = AnnotationUtils
            .findAnnotation(method, ApolloConfigChangeListener.class);
    if (annotation == null) {
        return;
    }
    Class<?>[] parameterTypes = method.getParameterTypes();
    Preconditions.checkArgument(parameterTypes.length == 1,
            "Invalid number of parameters: %s for method: %s, should be 1", parameterTypes.length,
            method);
    Preconditions.checkArgument(ConfigChangeEvent.class.isAssignableFrom(parameterTypes[0]),
            "Invalid parameter type: %s for method: %s, should be ConfigChangeEvent", parameterTypes[0],
            method);


    ReflectionUtils.makeAccessible(method);
    //获取ApolloConfigChangeListener注解的namespace
    Set<String> namespaceList = Arrays.stream(annotation.value()).collect(Collectors.toSet());


    try {
        List<NamespaceDTO> namespaceDTOs = this.remoteNamespaceRepository.getNamespacesByAppIdAndClusterName(this.m_configUtil.getAppId(), this.m_configUtil.getCluster());
        namespaceDTOs.forEach((namespaceDTO) -> {
            namespaceList.add(namespaceDTO.getNamespaceName());
        });
    } catch (Exception e) {
        logger.error("get namespaces by appId from config service error:", e);
    }


    String[] annotatedInterestedKeys = annotation.interestedKeys();
    String[] annotatedInterestedKeyPrefixes = annotation.interestedKeyPrefixes();
    ConfigChangeListener configChangeListener = new ConfigChangeListener() {
        @Override
        public void onChange(ConfigChangeEvent changeEvent) {
            ReflectionUtils.invokeMethod(method, bean, changeEvent);
        }
    };


    Set<String> interestedKeys =
            annotatedInterestedKeys.length > 0 ? Sets.newHashSet(annotatedInterestedKeys) : null;
    Set<String> interestedKeyPrefixes =
            annotatedInterestedKeyPrefixes.length > 0 ? Sets.newHashSet(annotatedInterestedKeyPrefixes)
                    : null;


    namespaceList.forEach(namespace -> {
        final String resolvedNamespace = this.environment.resolveRequiredPlaceholders(namespace);
        Config config = ConfigService.getConfig(resolvedNamespace);

        if (interestedKeys == null && interestedKeyPrefixes == null) {
            config.addChangeListener(configChangeListener);
        } else {
            config.addChangeListener(configChangeListener, interestedKeys, interestedKeyPrefixes);
        }
    });
}
;