前言:java
本文主要是分享Apollo Client客戶端使用過程當中,遇到的問題、解決問題及分析代碼邏輯的過程。其中一個重要問題就是關於apollo.bootstrap.enabled = true的使用及注意事項。mysql
1、準備工做linux
1.1 環境要求spring
本文是基於Apollo v1.1.1版本,springboot項目客戶端引入的是:sql
<dependency>bootstrap
<groupId>com.ctrip.framework.apollo</groupId>windows
<artifactId>apollo-client</artifactId>api
<version>1.1.0</version>springboot
</dependency>服務器
<dependency>
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo-core</artifactId>
<version>1.1.0</version>
</dependency>
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.5 springboot項目執行結果
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日誌文件:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<properties>
<!-- 文件輸出格式 -->
<property name="PATTERN">%d{yyyy-MM-dd HH:mm:ss.SSS} |-%-5level [%T-%thread] %c [%L] -| %msg%n</property>
</properties>
<appenders>
<Console name="CONSOLE" target="system_out">
<PatternLayout pattern="${PATTERN}" />
</Console>
</appenders>
<loggers>
<logger name="com.apollo.demo" level="debug" />
<root level="warn">
<appenderref ref="CONSOLE" />
</root>
</loggers>
</configuration>
2、引入問題
原本認爲執行結果是:
+++++++++++++++++++++++++++++++++++++++++
localhost
application-des
爲何會出現這個狀況?(注意:mysqlcfg無論是關聯仍是私用,des的值原本應該爲mysqlcfg-des)
下面將分析出現這個結果的緣由及解決辦法。
3、代碼分析
3.1 apollo-client源碼分析
3.3.1 第一步-切入點
首先分析,客戶端確定從服務端,獲取了application 和mysqlcfg的配置項,因此從 客戶端請求url組裝進行分析:
類RemoteConfigLongPollService:
String assembleLongPollRefreshUrl(String uri, String appId, String cluster, String dataCenter, Map<String, Long> notificationsMap) {
Map<String, String> 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<String> 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邏輯流程圖
4、其餘
關於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。
本文純屬我的觀點