前言:
本文主要是分享Apollo Client客户端使用过程中,遇到的问题、解决问题及分析代码逻辑的过程。其中一个重要问题就是关于apollo.bootstrap.enabled = true的使用及注意事项。
一、准备工作
1.1环境要求
本文是基于Apollo v1.1.1版本,springboot项目客户端引入的是:
com.ctrip.framework.apollo
apollo-client
1.1.0
com.ctrip.framework.apollo
apollo-core
1.1.0
1.2必选设置
Apollo客户端依赖于AppId(项目ID),Apollo Meta Server(配置中心Eureka地址)
1.2.1 AppId
项目application.properties文件内容:
app.id=demo-test
apollo.bootstrap.enabled = true
注:app.id是用来标识应用身份的唯一id,格式为string。
apollo.bootstrap.enabled官方解释为注入默认application namespace的配置示例
1.2.2 Apollo Meta Server
apollo.meta(配置中心Eureka地址) 配置如下:
Apollo默认会读取系统上/opt/settings/server.properties(linux)或
C:\ opt \settings\server.properties(windows)文件(手动新建目录与文件)
apollo.meta=http://localhost:8089
1.3配置中心添加配置
1.4 springboot项目客户端代码
启动类:
package com.test.apollodemo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.stereotype.Component;
import com.test.apollodemo.configbean.MysqlConfig2;
import com.test.apollodemo.configbean.MysqlConfigBean;
@Component
@ComponentScan("com.test")
@SpringBootApplication
public class SpringBootConsoleApplication implements CommandLineRunner {
@Autowired
MysqlConfig2 mysqlConfig2;
public static void main(String[] args) throws Exception {
SpringApplication.run(SpringBootConsoleApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
while(true) {
System.out.println("+++++++++++++++++++++++++");
System.out.println(mysqlConfig2.getMysqlConfigBean().getUrl());
System.out.println(mysqlConfig2.getMysqlConfigBean().getDes());
Thread.sleep(3000);
}
}
}
实体类:
package com.test.apollodemo.configbean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.spring.annotation.EnableApolloConfig;
@Configuration
@EnableApolloConfig("mysqlcfg")
public class MysqlConfig2 {
@Bean
public MysqlConfigBean getMysqlConfigBean() {
return new MysqlConfigBean();
}
public class MysqlConfigBean{
@Value("${url:}")
private String url;
@Value("${username:}")
private String username;
@Value("${password:}")
private String password;
@Value("${des:我是默认值}")
private String des;
public String getDes() {
return des;
}
public void setDes(String des) {
this.des = des;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
/**
* @ApolloConfigChangeListener用来自动注册ConfigChangeListener
*/
@ApolloConfigChangeListener("mysqlcfg")
private void someOnChange(ConfigChangeEvent changeEvent) {
for(String changeKey:changeEvent.changedKeys()) {
ConfigChange change = changeEvent.getChange(changeKey);
System.out.println(String.format("%%%%%%%%%%Found datasource change - key: %s, oldValue: %s, newValue: %s, changeType: %s", change.getPropertyName(), change.getOldValue(), change.getNewValue(), change.getChangeType()));
}
}
}
1.5springboot项目执行结果
1.6 java项目案例
1.6.1代码案例
package com.demo.apollo;
import java.io.IOException;
import com.ctrip.framework.apollo.Config;
import com.ctrip.framework.apollo.ConfigChangeListener;
import com.ctrip.framework.apollo.ConfigService;
import com.ctrip.framework.apollo.model.ConfigChange;
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
public class Apollo {
public static void main(String[] args) throws InterruptedException, IOException {
//src目录下新建:META-INF\app.properties,
//app.id配置如下:app.id=demo-test3//项目ID(唯一)
Config config = ConfigService.getConfig("mysqlcfg");
String someKey = "url";
String someDefaultValue = "我是默认值";
String value = config.getProperty(someKey, someDefaultValue);
System.out.println(value);
config.addChangeListener(new ConfigChangeListener() {
@Override
public void onChange(ConfigChangeEvent changeEvent) {
// TODO Auto-generated method stub
System.out.println("Changes for namespace " + changeEvent.getNamespace());
for (String key : changeEvent.changedKeys()) {
ConfigChange change = changeEvent.getChange(key);
System.out.println(String.format("Found change - key: %s, oldValue: %s, newValue: %s, changeType: %s", change.getPropertyName(), change.getOldValue(), change.getNewValue(), change.getChangeType()));
}
}
});
while(true) {
System.out.println(config.getProperty(someKey, someDefaultValue));
Thread.sleep(3000);
}
}
}
1.6.2依赖引入
Apollo客户端核心jar包:
apollo-client-1.1.0.jar
apollo-core-1.1.0.jar
只导入核心Jar包报错如下:
Exception in thread "main" java.lang.NoClassDefFoundError: Could not initialize class com.ctrip.framework.apollo.tracer.Tracer
at com.ctrip.framework.apollo.build.ApolloInjector.getInstance(ApolloInjector.java:37)
at com.ctrip.framework.apollo.ConfigService.getManager(ConfigService.java:25)
at com.ctrip.framework.apollo.ConfigService.getConfig(ConfigService.java:61)
核心jar包依赖包:
aopalliance-1.0.jar
gson-2.8.5.jar
guava-26.0-jre.jar
guice-4.2.1.jar
javax.inject-1.jar
log4j-api-2.10.0.jar
log4j-core-2.10.0.jar
log4j-slf4j-impl-2.10.0.jar
slf4j-api-1.7.25.jar
1.6.3日志
引入log4j2.xml日志文件:
%d{yyyy-MM-dd HH:mm:ss.SSS} |-%-5level [%T-%thread] %c [%L] -| %msg%n
二、引入问题
本来认为执行结果是:
+++++++++++++++++++++++++++++++++++++++++
localhost
application-des
为什么会出现这个情况?(注意:mysqlcfg不管是关联还是私用,des的值本来应该为mysqlcfg-des)
下面将分析出现这个结果的原因及解决办法。
三、代码分析
3.1 apollo-client源码分析
3.3.1第一步-切入点
首先分析,客户端肯定从服务端,获取了application 和mysqlcfg的配置项,所以从 客户端请求url组装进行分析:
类RemoteConfigLongPollService:
String assembleLongPollRefreshUrl(String uri, String appId, String cluster, String dataCenter, Map
Long> notificationsMap) {
Map queryParams = Maps.newHashMap();
queryParams.put("appId",
queryParamEscaper.escape(appId));
queryParams.put("cluster",
queryParamEscaper.escape(cluster));
//下面这段就是告诉服务器,我要获取application和mysqlcfg的配置项
queryParams
.put("notifications",queryParamEscaper.escape(assembleNotifications(notificationsMap)));
if
(!Strings.isNullOrEmpty(dataCenter)) {
queryParams.put("dataCenter",
queryParamEscaper.escape(dataCenter));
}
String localIp
= m_configUtil.getLocalIp();
if
(!Strings.isNullOrEmpty(localIp)) {
queryParams.put("ip", queryParamEscaper.escape(localIp));
}
String params
= MAP_JOINER.join(queryParams);
if
(!uri.endsWith("/")) {
uri
+= "/";
}
return
uri
+ "notifications/v2?" + params;
}
方法doLongPollingRefresh会调用:
url =assembleLongPollRefreshUrl(lastServiceDto.getHomepageUrl(), appId, cluster,
dataCenter,
m_notifications);
doLongPollingRefresh到startLongPolling到submit
3.3.2第二步-反向追溯
1.类
RemoteConfigRepository:
private
void
scheduleLongPollingRefresh() {
remoteConfigLongPollService.submit(m_namespace,
this);
}
然后:
public
RemoteConfigRepository(String namespace) {
m_namespace
= namespace;
m_configCache
= new
AtomicReference<>();
m_configUtil
= ApolloInjector.getInstance(ConfigUtil.class);
m_httpUtil
= ApolloInjector.getInstance(HttpUtil.class);
m_serviceLocator
= ApolloInjector.getInstance(ConfigServiceLocator.class);
remoteConfigLongPollService = ApolloInjector.getInstance(RemoteConfigLongPollService.class);
m_longPollServiceDto = new
AtomicReference<>();
m_remoteMessages
= new
AtomicReference<>();
m_loadConfigRateLimiter = RateLimiter.create(m_configUtil.getLoadConfigQPS());
m_configNeedForceRefresh = new
AtomicBoolean(true);
m_loadConfigFailSchedulePolicy = new
ExponentialSchedulePolicy(m_configUtil.getOnErrorRetryInterval(),
m_configUtil.getOnErrorRetryInterval() * 8);
gson
= new
Gson();
this.trySync();
this.schedulePeriodicRefresh();
this.scheduleLongPollingRefresh();
}
2.继续追溯类DefaultConfigFactory:
LocalFileConfigRepository createLocalConfigRepository(String
namespace)
{
if
(m_configUtil.isInLocalMode())
{
logger.warn(
"==== Apollo is in local mode! Won't pull configs from remote server
for namespace {} ! ====",
namespace);
return
new
LocalFileConfigRepository(namespace);
}
return
new
LocalFileConfigRepository(namespace, createRemoteConfigRepository(namespace));
}
RemoteConfigRepository createRemoteConfigRepository(String namespace) {
return
new
RemoteConfigRepository(namespace);
}
然后到:
@Override
public
Config create(String namespace) {
DefaultConfig defaultConfig =
new DefaultConfig(namespace, createLocalConfigRepository(namespace));
return
defaultConfig;
}
3. 类DefaultConfigManager:
@Override
public
Config getConfig(String namespace) {
Config config
= m_configs.get(namespace);
if
(config
== null)
{
synchronized
(this)
{
config = m_configs.get(namespace);
if (config == null)
{
ConfigFactory factory = m_factoryManager.getFactory(namespace);
config = factory.create(namespace);
m_configs.put(namespace, config);
}
}
}
return
config;
}
4. 类ConfigService:
public
static
Config getConfig(String namespace) {
return
s_instance.getManager().getConfig(namespace);
}
5. 类ApolloApplicationContextInitializer:
@Override
public
void
initialize(ConfigurableApplicationContext context) {
ConfigurableEnvironment environment = context.getEnvironment();
initializeSystemProperty(environment);
String enabled
= environment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED,
"false");
if
(!Boolean.valueOf(enabled)) {
logger.debug("Apollo bootstrap config is not enabled for context
{}, see property: ${{}}", context,
PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED);
return;
}
logger.debug("Apollo bootstrap config is enabled for context
{}", context);
if
(environment.getPropertySources().contains(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
//already initialized
return;
}
String namespaces
= environment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_NAMESPACES, ConfigConsts.NAMESPACE_APPLICATION);
logger.debug("Apollo bootstrap namespaces: {}", namespaces);
List namespaceList = NAMESPACE_SPLITTER.splitToList(namespaces);
CompositePropertySource 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);
}
6. 接口PropertySourcesConstants:
package com.ctrip.framework.apollo.spring.config;
public interface
PropertySourcesConstants {
String APOLLO_PROPERTY_SOURCE_NAME = "ApolloPropertySources";
String APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME = "ApolloBootstrapPropertySources";
String APOLLO_BOOTSTRAP_ENABLED = "apollo.bootstrap.enabled";
String APOLLO_BOOTSTRAP_NAMESPACES = "apollo.bootstrap.namespaces";
}
最终找出原因是跟 APOLLO_BOOTSTRAP_NAMESPACES有关
3.2总结
3.2.1问题解决
最终找出原因:如果配置文件没有配置apollo.bootstrap.namespaces时,系统默认namespaces为application,所以客户端会去请求application配置项。
String namespaces = environment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_NAMESPACES, ConfigConsts.NAMESPACE_APPLICATION);
所以在application.properties文件中添加:
apollo.bootstrap.namespaces =
mysqlcfg,保证客户端不会去获取application配置项,这样结果输出正常。
3.2.2逻辑流程图
四、其他
关于apollo.meta参数配置优先级
(1) JVM system property 'apollo.meta',
(2) OS env variable 'APOLLO_META'
(3) property 'apollo.meta' from
server.properties
(4) property 'apollo.meta' from app.properties
一般项目常用(3)的方式,配置apollo.meta。
本文纯属个人观点