配置中心Config-Apollo

Scroll Down

title: Apollo接入使用方式
author: Mood
tags:

  • SpringCloud
    categories:
  • Apollo
  • Config
    date: 2018-09-24 22:07:00

一、 准备工作

  • 1.1 环境要求
  • Java: 1.7+
  • Guava: 15.0+
    • Apollo客户端默认会引用Guava 19

注:对于Apollo客户端,如果有需要的话,可以做少量代码修改来降级到Java 1.6,详细信息可以参考Issue 483

1.2 AppId Apollo Meta Server

1.2.1 AppId
AppId是应用的身份信息
有以下几种方式设置,按照优先级从高到低分别为:

  • System Property

Apollo 0.7.0+支持通过System Property传入app.id信息,如-Dapp.id=APP-ID

  • 操作系统的System Environment

Apollo 1.4.0+支持通过操作系统的System Environment APP_ID来传入app.id信息,如APP_ID=APP-ID

  • Spring Boot application.properties

Apollo 1.0.0+支持通过Spring Boot的application.properties文件配置,如app.id=APP-ID 该配置方式不适用于多个war包部署在同一个tomcat的使用场景

  • app.properties

在classpath:/META-INF/创建app.properties文件,并且其中内容形如:app.id=APP-ID

1.2.2 Apollo Meta Server

MetaServer这个角色,它其实是一个Eureka的Proxy,将Eureka的服务发现接口以更简单明确的HTTP接口的形式暴露出来,方便Client/Protal通过简单的HTTPClient就可以查询到Config/AdminService的地址列表。获取到服务实例地址列表之后,再以简单的客户端软负载(Client SLB)策略路由定位到目标实例,并发起调用。

Apollo支持应用在不同的环境有不同的配置,所以需要在运行提供给Apollo客户端当前环境的Apollo Meta Server信息。默认情况下,meta server和config service是部署在同一个JVM进程,所以meta server的地址就是config service的地址。

1.0.0版本开始支持以下方式配置apollo meta server信息,按照优先级从高到低分别为:

  • 通过Java System Property

apollo.meta 可以通过Java的System Property apollo.meta来指定
在Java程序启动脚本中,可以指定-Dapollo.meta=http://config-service-url,$
_meta
如果当前env是dev,那么用户可以配置-Ddev_meta=http://config-service-url

如果是运行jar文件,需要注意格式是java -Dapollo.meta=http://config-service-url -jar xxx.jar
也可以通过程序指定,如System.setProperty("apollo.meta", "http://config-service-url");

  • 通过Spring Boot的配置文件

可以在Spring Boot的application.properties或bootstrap.properties中指定apollo.meta=http://config-service-url
该配置方式不适用于多个war包部署在同一个tomcat的使用场景

通过操作系统的System EnvironmentAPOLLO_META
可以通过操作系统的System Environment APOLLO_META来指定
注意key为全大写,且中间是_分隔

  • 通过server.properties配置文件

可以在server.properties配置文件中指定apollo.meta=http://config-service-url

  • 对于Mac/Linux,文件位置为/opt/settings/server.properties
  • 对于Windows,文件位置为C:\opt\settings\server.properties
  • 通过app.properties配置文件
    可以在classpath:/META-INF/app.properties指定apollo.meta=http://config-service-url

如果通过以上各种手段都无法获取到Meta Server地址,Apollo最终会fallback到http://apollo.meta作为Meta Server地址

1.2.2.1 自定义Apollo Meta Server

在1.0.0版本中,Apollo提供了MetaServerProvider SPI,用户可以注入自己的MetaServerProvider来自定义Meta Server地址定位逻辑。

由于我们使用典型的Java Service Loader模式,所以实现起来还是比较简单的。

有一点需要注意的是,apollo会在运行时按照顺序遍历所有的MetaServerProvider,直到某一个MetaServerProvider提供了一个非空的Meta Server地址,因此用户需要格外注意自定义MetaServerProvider的Order。规则是较小的Order具有较高的优先级,因此Order=0的MetaServerProvider会排在Order=1的MetaServerProvider的前面。

假如公司有很多应用需要接入Apollo,可以封装一个jar包,然后提供自定义的Apollo Meta Server定位逻辑,从而可以让接入Apollo的应用零配置使用。比如自己写一个dingxing-company-apollo-client,该jar包依赖apollo-client,在该jar包中通过spi方式定义自定义的MetaServerProvider实现,然后应用直接依赖dingxing-company-apollo-client即可。

MetaServerProvider的实现可以参考LegacyMetaServerProvider和DefaultMetaServerProvider。

1.2.2.2 跳过Apollo Meta Server服务发现

适用于apollo-client 0.11.0及以上版本

一般情况下都建议使用Apollo的Meta Server机制来实现Config Service的服务发现,从而可以实现Config Service的高可用。不过apollo-client也支持跳过Meta Server服务发现,主要用于以下场景:

  • Config Service部署在公有云上,注册到Meta Server的是内网地址,本地开发环境无法直接连接
  • Config Service部署在docker环境中,注册到Meta Server的是docker内网地址,本地开发环境无法直接连接
  • Config Service部署在kubernetes中,希望使用kubernetes自带的服务发现能力(Service)

针对以上场景,可以通过直接指定Config Service地址的方式来跳过Meta Server服务发现,按照优先级从高到低分别为:

  • Java System Property apollo.configService
    可以通过Java的System Property apollo.configService来指定
    在Java程序启动脚本中,可以指定-Dapollo.configService=http://config-service-url:port
  • 如果是运行jar文件,需要注意格式是java -Dapollo.configService=http://config-service-url:port -jar xxx.jar
    也可以通过程序指定,如System.setProperty("apollo.configService", "http://config-service-url:port");
  • 通过操作系统的System EnvironmentAPOLLO_CONFIGSERVICE
    可以通过操作系统的System Environment APOLLO_CONFIGSERVICE来指定
    注意key为全大写,且中间是_分隔
  • 通过server.properties配置文件

可以在server.properties配置文件中指定apollo.configService=http://config-service-url:port

  • 对于Mac/Linux,文件位置为/opt/settings/server.properties
  • 对于Windows,文件位置为C:\opt\settings\server.properties

1.2.3 本地缓存路径

Apollo客户端会把从服务端获取到的配置在本地文件系统缓存一份,用于在遇到服务不可用,或网络不通的时候,依然能从本地恢复配置,不影响应用正常运行。

本地缓存路径默认位于以下路径,所以请确保/opt/data或C:\opt\data\目录存在,且应用有读写权限。

  • Mac/Linux: /opt/data//config-cache
  • Windows: C:\opt\data{appId}\config-cache

本地配置文件会以下面的文件名格式放置于本地缓存路径下:

{appId}+{cluster}+{namespace}.properties

  • appId 就是应用自己的appId,如100004458
  • cluster 就是应用使用的集群,一般在本地模式下没有做过配置的话,就是default
  • namespace 就是应用使用的配置namespace,一般是application

文件内容以properties格式存储,比如如果有两个key,一个是request.timeout,另一个是batch,那么文件内容就是如下格式:

request.timeout=2000
batch=2000

1.2.3.1 自定义缓存路径
1.0.0版本开始支持以下方式自定义缓存路径,按照优先级从高到低分别为:和上述的优先级一样,指定apollo.cacheDir=/opt/data/some-cache-dir即可

1.2.4.2 Cluster(集群)

Apollo支持配置按照集群划分,也就是说对于一个appId和一个环境,对不同的集群可以有不同的配置。

1.0.0版本开始支持以下方式集群,按照优先级从高到低分别为:

  • Java System Property

可以通过Java的System Property apollo.cluster来指定
在Java程序启动脚本中,可以指定-Dapollo.cluster=SomeCluster

  • 如果是运行jar文件

需要注意格式是java -Dapollo.cluster=SomeCluster -jar xxx.jar
也可以通过程序指定,如System.setProperty("apollo.cluster", "SomeCluster");

  • Spring Boot的配置文件

可以在Spring Boot的application.properties或bootstrap.properties中指定apollo.cluster=SomeCluster

  • 通过Java System Property
    可以通过Java的System Property idc来指定环境
    • 在Java程序启动脚本中,可以指定-Didc=xxx
    • 如果是运行jar文件,需要注意格式是java -Didc=xxx -jar xxx.jar
      注意key为全小写
  • 通过操作系统的System Environment

还可以通过操作系统的System Environment IDC来指定
注意key为全大写

  • 通过server.properties配置文件

可以在server.properties配置文件中指定idc=xxx
对于Mac/Linux,文件位置为/opt/settings/server.properties
对于Windows,文件位置为C:\opt\settings\server.properties

Cluster Precedence(集群顺序)

  1. 如果apollo.cluster和idc同时指定:
  • 我们会首先尝试从apollo.cluster指定的集群加载配置
  • 如果没找到任何配置
    会尝试从idc指定的集群加载配置
  • 如果还是没找到,会从默认的集群(default)加载
  1. 如果只指定了apollo.cluster:
  • 我们会首先尝试从apollo.cluster指定的集群加载配置
  • 如果没找到,会从默认的集群(default)加载
  1. 如果只指定了idc:
  • 我们会首先尝试从idc指定的集群加载配置
  • 如果没找到,会从默认的集群(default)加载
  1. 如果apollo.cluster和idc都没有指定:
  • 我们会从默认的集群(default)加载配置

二、Maven Dependency

Apollo的客户端jar包已经上传到中央仓库,应用在实际使用时只需要按照如下方式引入即可。

<dependency>
    <groupId>com.ctrip.framework.apollo</groupId>
    <artifactId>apollo-client</artifactId>
    <version>1.1.0</version>
</dependency>

三、Apollo Client接入

Apollo支持API方式和SpringBoot整合方式,该怎么选择用哪一种方式?

3.1 API使用方式

API方式是最简单、高效使用Apollo配置的方式,不依赖Spring框架即可使用。

3.1.1 获取默认namespace的配置(application)

Config config = ConfigService.getAppConfig(); //config instance is singleton for each namespace and is never null
String someKey = "someKeyFromDefaultNamespace";
String someDefaultValue = "someDefaultValueForTheKey";
String value = config.getProperty(someKey, someDefaultValue);

通过上述的config.getProperty可以获取到someKey对应的实时最新的配置值。
另外,配置值从内存中获取,所以不需要应用自己做缓存。

3.1.2 监听配置变化事件

监听配置变化事件只在应用真的关心配置变化,需要在配置变化时得到通知时使用,比如:数据库连接串变化后需要重建连接等。

如果只是希望每次都取到最新的配置的话,只需要按照上面的例子,调用config.getProperty即可。

Config config = ConfigService.getAppConfig(); //config instance is singleton for each namespace and is never null
config.addChangeListener(new ConfigChangeListener() {
    @Override
    public void onChange(ConfigChangeEvent changeEvent) {
        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()));
        }
    }
});

3.1.3 获取公共Namespace的配置

String somePublicNamespace = "CAT";
Config config = ConfigService.getConfig(somePublicNamespace); //config instance is singleton for each namespace and is never null
String someKey = "someKeyFromPublicNamespace";
String someDefaultValue = "someDefaultValueForTheKey";
String value = config.getProperty(someKey, someDefaultValue);

3.1.4 获取非properties格式namespace的配置

3.1.4.1 yaml/yml格式的namespace

apollo-client 1.3.0版本开始对yaml/yml做了更好的支持,使用起来和properties格式一致。

Config config = ConfigService.getConfig("application.yml");
String someKey = "someKeyFromYmlNamespace";
String someDefaultValue = "someDefaultValueForTheKey";
String value = config.getProperty(someKey, someDefaultValue);
3.1.4.2 非yaml/yml格式的namespace

获取时需要使用ConfigService.getConfigFile接口并指定Format,如ConfigFileFormat.XML。

String someNamespace = "test";
ConfigFile configFile = ConfigService.getConfigFile("test", ConfigFileFormat.XML);
String content = configFile.getContent();

3.2 SpringBoot整合方式

3.2.1 基于Java的配置(推荐)

相对于基于XML的配置,基于Java的配置是目前比较流行的方式。

注意@EnableApolloConfig要和@Configuration一起使用,不然不会生效。

1.注入默认namespace的配置到Spring中

//这个是最简单的配置形式,一般应用用这种形式就可以了,用来指示Apollo注入application namespace的配置到Spring环境中

@Configuration
@EnableApolloConfig
public class AppConfig {
  @Bean
  public TestJavaConfigBean javaConfigBean() {
    return new TestJavaConfigBean();
  }
}

2.注入多个namespace的配置到Spring中

@Configuration
@EnableApolloConfig
public class SomeAppConfig {
  @Bean
  public TestJavaConfigBean javaConfigBean() {
    return new TestJavaConfigBean();
  }
}
   
//这个是稍微复杂一些的配置形式,指示Apollo注入FX.apollo和application.yml namespace的配置到Spring环境中
@Configuration
@EnableApolloConfig({"FX.apollo", "application.yml"})
public class AnotherAppConfig {}

3.注入多个namespace,并且指定顺序

//这个是最复杂的配置形式,指示Apollo注入FX.apollo和application.yml namespace的配置到Spring环境中,并且顺序在application前面

@Configuration
@EnableApolloConfig(order = 2)
public class SomeAppConfig {
  @Bean
  public TestJavaConfigBean javaConfigBean() {
    return new TestJavaConfigBean();
  }
}
@Configuration
@EnableApolloConfig(value = {"FX.apollo", "application.yml"}, order = 1)
public class AnotherAppConfig {}

3.2.1.2 Spring Boot集成方式(推荐)

Spring Boot除了支持上述两种集成方式以外,还支持通过application.properties/bootstrap.properties来配置,该方式能使配置在更早的阶段注入,比如使用@ConditionalOnProperty的场景或者是有一些spring-boot-starter在启动阶段就需要读取配置做一些事情(如dubbo-spring-boot-project),所以对于Spring Boot环境建议通过以下方式来接入Apollo(需要0.10.0及以上版本)。

使用方式很简单,只需要在application.properties/bootstrap.properties中按照如下样例配置即可。

注入默认application namespace的配置示例
     # will inject 'application' namespace in bootstrap phase
     apollo.bootstrap.enabled = true
注入非默认application namespace或多个namespace的配置示例
     apollo.bootstrap.enabled = true
     # will inject 'application', 'FX.apollo' and 'application.yml' namespaces in bootstrap phase
     apollo.bootstrap.namespaces = application,FX.apollo,application.yml

3.2.2 Spring Placeholder的使用

Spring应用通常会使用Placeholder来注入配置,使用的格式形如$,如$。冒号前面的是key,冒号后面的是默认值。

建议在实际使用时尽量给出默认值,以免由于key没有定义导致运行时错误。

从v0.10.0开始的版本支持placeholder在运行时自动更新.

如果需要关闭placeholder在运行时自动更新功能,可以通过以下两种方式关闭:

  • 通过设置System Property apollo.autoUpdateInjectedSpringProperties,如启动时传入-Dapollo.autoUpdateInjectedSpringProperties=false

  • 通过设置META-INF/app.properties中的apollo.autoUpdateInjectedSpringProperties属性,如

app.id=SampleApp   
apollo.autoUpdateInjectedSpringProperties=false

3.2.2.1 Java Config使用方式
假设我有一个TestJavaConfigBean,通过Java Config的方式还可以使用@Value的方式注入:

public class TestJavaConfigBean {
  @Value("${timeout:100}")
  private int timeout;
  private int batch;
 
  @Value("${batch:200}")
  public void setBatch(int batch) {
    this.batch = batch;
  }
 
  public int getTimeout() {
    return timeout;
  }
 
  public int getBatch() {
    return batch;
  }
}
在Configuration类中按照下面的方式使用(假设应用默认的application namespace中有timeout和batch的配置项):

@Configuration
@EnableApolloConfig
public class AppConfig {
  @Bean
  public TestJavaConfigBean javaConfigBean() {
    return new TestJavaConfigBean();
  }
}

3.2.2.2 ConfigurationProperties使用方式

Spring Boot提供了@ConfigurationProperties把配置注入到bean对象中。

Apollo也支持这种方式,下面的例子会把redis.cache.expireSeconds和redis.cache.commandTimeout分别注入到SampleRedisConfig的expireSeconds和commandTimeout字段中。

@ConfigurationProperties(prefix = "redis.cache")
public class SampleRedisConfig {
  private int expireSeconds;
  private int commandTimeout;

  public void setExpireSeconds(int expireSeconds) {
    this.expireSeconds = expireSeconds;
  }

  public void setCommandTimeout(int commandTimeout) {
    this.commandTimeout = commandTimeout;
  }
}
在Configuration类中按照下面的方式使用(假设应用默认的application namespace中有redis.cache.expireSeconds和redis.cache.commandTimeout的配置项):

@Configuration
@EnableApolloConfig
public class AppConfig {
  @Bean
  public SampleRedisConfig sampleRedisConfig() {
    return new SampleRedisConfig();
  }
}

需要注意的是,@ConfigurationProperties如果需要在Apollo配置变化时自动更新注入的值,需要配合使用EnvironmentChangeEvent或RefreshScope。相关代码实现,可以参考apollo-use-cases项目中的ZuulPropertiesRefresher.java和apollo-demo项目中的SampleRedisConfig.java以及SpringBootApolloRefreshConfig.java

3.2.3 Spring Annotation支持

Apollo同时还增加了几个新的Annotation来简化在Spring环境中的使用。

  • @ApolloConfig

用来自动注入Config对象

  • @ApolloConfigChangeListener

用来自动注册ConfigChangeListener

  • @ApolloJsonValue

用来把配置的json字符串自动注入为对象
使用样例如下:

public class TestApolloAnnotationBean {
  @ApolloConfig
  private Config config; //inject config for namespace application
  @ApolloConfig("application")
  private Config anotherConfig; //inject config for namespace application
  @ApolloConfig("FX.apollo")
  private Config yetAnotherConfig; //inject config for namespace FX.apollo
  @ApolloConfig("application.yml")
  private Config ymlConfig; //inject config for namespace application.yml
 
  /**
   * ApolloJsonValue annotated on fields example, the default value is specified as empty list - []
   * <br />
   * jsonBeanProperty=[{"someString":"hello","someInt":100},{"someString":"world!","someInt":200}]
   */
  @ApolloJsonValue("${jsonBeanProperty:[]}")
  private List<JsonBean> anotherJsonBeans;

  @Value("${batch:100}")
  private int batch;
  
  //config change listener for namespace application
  @ApolloConfigChangeListener
  private void someOnChange(ConfigChangeEvent changeEvent) {
    //update injected value of batch if it is changed in Apollo
    if (changeEvent.isChanged("batch")) {
      batch = config.getIntProperty("batch", 100);
    }
  }
 
  //config change listener for namespace application
  @ApolloConfigChangeListener("application")
  private void anotherOnChange(ConfigChangeEvent changeEvent) {
    //do something
  }
 
  //config change listener for namespaces application, FX.apollo and application.yml
  @ApolloConfigChangeListener({"application", "FX.apollo", "application.yml"})
  private void yetAnotherOnChange(ConfigChangeEvent changeEvent) {
    //do something
  }

  //example of getting config from Apollo directly
  //this will always return the latest value of timeout
  public int getTimeout() {
    return config.getIntProperty("timeout", 200);
  }

  //example of getting config from injected value
  //the program needs to update the injected value when batch is changed in Apollo using @ApolloConfigChangeListener shown above
  public int getBatch() {
    return this.batch;
  }

  private static class JsonBean{
    private String someString;
    private int someInt;
  }
}

在Configuration类中按照下面的方式使用:

@Configuration
@EnableApolloConfig
public class AppConfig {
  @Bean
  public TestApolloAnnotationBean testApolloAnnotationBean() {
    return new TestApolloAnnotationBean();
  }
}

3.2.4 配置迁移

很多情况下,应用可能已经有不少配置了,比如Spring Boot的应用,就会有bootstrap.properties/yml, application.properties/yml等配置。

在应用接入Apollo之后,这些配置是可以非常方便的迁移到Apollo的,具体步骤如下:

  1. 在Apollo为应用新建项目
    在应用中配置好META-INF/app.properties
    建议把原先配置先转为properties格式,然后通过Apollo提供的文本编辑模式全部粘帖到应用的application namespace,发布配置
  2. 如果原来格式是yml,可以使用YamlPropertiesFactoryBean.getObject转成properties格式
    如果原来是yml,想继续使用yml来编辑配置,那么可以创建私有的application.yml namespace,把原来的配置全部粘贴进去,发布配置

    需要apollo-client是1.3.0及以上版本
    把原先的配置文件如bootstrap.properties/yml, application.properties/yml从项目中删除
    如果需要保留本地配置文件,需要注意部分配置如server.port必须确保本地文件已经删除该配置项
    如:

spring.application.name = reservation-service
server.port = 8080

logging.level = ERROR

eureka.client.serviceUrl.defaultZone = http://127.0.0.1:8761/eureka/
eureka.client.healthcheck.enabled = true
eureka.client.registerWithEureka = true
eureka.client.fetchRegistry = true
eureka.client.eurekaServiceUrlPollIntervalSeconds = 60

eureka.instance.preferIpAddress = true
text-mode-spring-boot-config-sample