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);
}
});
}