前言:

本文主要是分享Apollo Client客户端使用过程中,遇到的问题、解决问题及分析代码逻辑的过程。其中一个重要问题就是关于apollo.bootstrap.enabled = true的使用及注意事项。

一、准备工作

1.1 环境要求

本文是基于Apollo v1.1.1版本,springboot项目客户端引入的是:

<dependency>
    <groupId>com.ctrip.framework.apollo</groupId>
apollo-client</artifactId>
    <version>1.1.0</version>
</dependency>
<dependency>
    <groupId>com.ctrip.framework.apollo</groupId>
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 配置中心添加配置

 

apollodocker部署后无法访问 apollo客户端使用_apollodocker部署后无法访问

 

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项目执行结果

 

apollodocker部署后无法访问 apollo客户端使用_spring_02

 

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
ApolloInjector.java:37)
ConfigService.java:25)
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>




二、引入问题

 本来认为执行结果是:

+++++++++++++++++++++++++++++++++++++++++
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<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

 

doLongPollingRefresh到startLongPolling到submit

3.3.2 第二步-反向追溯

1.类 RemoteConfigRepository:

privatevoidscheduleLongPollingRefresh() {
   
remoteConfigLongPollService.submit(m_namespace,
this);
 
}

然后:

publicRemoteConfigRepository(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);
     
returnnew
LocalFileConfigRepository(namespace);
   
}
   
returnnew
LocalFileConfigRepository(namespace, createRemoteConfigRepository(namespace));
 
}
 
 
RemoteConfigRepository createRemoteConfigRepository(String namespace) {
   
returnnewRemoteConfigRepository(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:

publicstatic
Config getConfig(String namespace) {
   
returns_instance.getManager().getConfig(namespace);
 
}

 

5. 类ApolloApplicationContextInitializer:

 

@Override
 
publicvoid
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逻辑流程图

  

apollodocker部署后无法访问 apollo客户端使用_jar_03

 

四、其他

关于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。

 

 

本文纯属个人观点