前言
  在单体式应用中,我们通常的做法是将配置文件和代码放在一起,这没有什么不妥。当你的应用变得越来越大从而不得不进行服务化拆分的时候,会发现各种provider实例越来越多,修改某一项配置越来越麻烦,你常常不得不为修改某一项配置而重启某个服务所有的provider实例,甚至为了灰度上线需要更新部分provider的配置。这个时候有一套配置文件集中管理方案变得十分重要,SpringCloudConfig和SpringCloudBus是这种问题的解决方案之一,业界也有些知名的同类开源产品,比如百度的disconf。
  相比较同类产品,SpringCloudConfig大的优势是和Spring无缝集成,支持Spring里面Environment和PropertySource的接口,对于已有的Spring应用程序的迁移成本非常低,在配置获取的接口上是完全一致,结合SpringBoot可使你的项目有更加统一的标准(包括依赖版本和约束规范),避免了应为集成不同开软件源造成的依赖版本冲突。
  一. 简介
  SpringCloudConfig是我们通常意义上的配置中心,把应用原本放在本地文件的配置抽取出来放在中心服务器,从而能够提供更好的管理、发布能力。SpringCloudConfig分服务端和客户端,服务端负责将git(svn)中存储的配置文件发布成REST接口,客户端可以从服务端REST接口获取配置。但客户端并不能主动感知到配置的变化,从而主动去获取新的配置,这需要每个客户端通过POST方法触发各自的/refresh。
  SpringCloudBus通过一个轻量级消息代理连接分布式系统的节点。这可以用于广播状态更改(如配置更改)或其他管理指令。SpringCloudBus提供了通过POST方法访问的endpoint/bus/refresh,这个接口通常由git的钩子功能调用,用以通知各个SpringCloudConfig的客户端去服务端更新配置。
  下图是SpringCloudConfig结合SpringCloudBus实现分布式配置的工作流

  注意:这是工作的流程图,实际的部署中SpringCloudBus并不是一个独立存在的服务,这里单列出来是为了能清晰的显示出工作流程。
  二. SpringCloudConfig Server
  SpringCloudConfig提供基于以下3个维度的配置管理:
  · 应用
  这个比较好理解,每个配置都是属于某一个应用的
  · 环境
  每个配置都是区分环境的,如dev, test, prod等
  · 版本
  这个可能是一般的配置中心所缺乏的,是对同一份配置的不同版本管理
  Spring Cloud Config提供版本的支持,也是说对于一个应用的不同部署实例,可以从服务端获取到不同版本的配置,这对于一些特殊场景如:灰度发布,A/B测试等提供了很好的支持。
  2.1 ConfigServer 配置
  服务端要在pom中依赖spring-cloud-config-server、spring-cloud-starter-bus-kafka
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-kafka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-monitor</artifactId>
</dependency>
  application.properties中要配置仓库描述和消息队列地址,如果是私有项目还需要配置用户名密码
spring.cloud.config.server.git.uri=https://github.com/seagrape/SpringCloudConfig.git
spring.cloud.config.server.git.searchPaths=alan-config-repo
#spring.cloud.config.server.git.username=sihan2
#spring.cloud.config.server.git.password=MYPASSWORD
spring.cloud.stream.kafka.binder.brokers=10.79.96.52:9092
spring.cloud.stream.kafka.binder.zk-nodes=10.79.96.52:2182
  启动类中要有@EnableConfigServer注解
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
  2.2 ConfigServer 启动
  Server端启动后,提供了如下的接口地址,参数说明
  · application:应用名
  · profile:环境
  · label:版本
/{application}/{profile}[/{label}]
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties
  NOTE:是配置文件的名字一般是有两部分组成,举个例子感受下,alan-provider-data-config-dev.properties,其中alan-provider-data-config是第一部分,这部分建议通过命名规则能让你知道是哪一个项目的配置,并且客户端要配置spring.cloud.config.name=alan-provider-data-config,才能让客户端知道自己要去服务端找哪一个配置文件。dev是第二部分,这部分用以区别配置文件应用的场景,是开发环境、测试环境或者生产环境
  接口返回样例 curl http://localhost:8888/alan-provider-data-config/dev/master
{
"name": "alan-provider-data-config",
"profiles": ["dev"],
"label": "master",
"version": "78dce2b71473749a5298e11ef0d004ffa8d26bd1",
"propertySources": [{
"name": "https://github.com/seagrape/SpringCloudConfig.git/alan-config-repo/alan-provider-data-config-dev.properties",
"source": {
"spring.datasource.driver-class-name": "com.mysql.jdbc.Driver",
"spring.datasource.username": "username",
"spring.datasource.password": "password",
"spring.datasource.url": "jdbc:mysql://DEVIP:PORT/DBNAME?characterEncoding=UTF-8"
}
}]
}
  接口返回样例 curl http://localhost:8888/alan-provider-data-config-dev.properties
  spring.datasource.driver-class-name: com.mysql.jdbc.Driver
  spring.datasource.password: password
  spring.datasource.url: jdbc:mysql://DEVIP:PORT/DBNAME?characterEncoding=UTF-8
  spring.datasource.username: username
  2.3 ConfigServer 文件系统
  GIT做文件系统,文件都会被clone到本地文件系统中,默认这些文件会被放置到以config-repo-为前缀的系统临时目录,在 linux 上应该是 /tmp/config-repo-目录,如果你遇到了不可预知的问题出现,你可以通过设置spring.cloud.config.server.git.basedir参数值为非系统临时目录。
  Config Server中,还有一种从本地classpath 或文件系统中加载配置文件的方式,可以通过spring.cloud.config.server.native.searchLocations进行设置。但如果你连GIT环境都没有,你还是回去喝奶吧......
  三. SpringCloudConfig Client
  3.1 ConfigClient 配置
  客户端要在pom中依赖spring-cloud-starter-config、spring-cloud-starter-bus-kafka
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-kafka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
  bootstrap.properties中配置配置中心地址和消息队列地址
  1.特别注意 配置中心的地址一定要放在bootstrap.properties中,这个配置文件是由“根”上下文优先加载,可以保证程序启动之初感知到远程配置中心的存在,并从远程获取配置,随后继续启动系统,这点十分重要。 2.而application.properties是由子上下文加载,加载顺序低于前者,如果配置中心地址放在这里,并且你远程配置了一些启动相关的必要参数,那么,你的程序很可能由于缺少参数而启动失败。 3.下面这段代码,关键的是第一行,第二行如果不配置系统默认读取spring.application.name,第三行如果不配置,系统默认default,即:${spring.application.name}.properties 4.我们一般的做法是,在系统启动的时候,用命令行传入--spring.cloud.config.profile=dev|prod|test的方式,因为在启动的时候,我们是明确知道我要获取哪套配置的。 5.bus相关的配置(本例中用的kafka)完全可以放在远程。
spring.cloud.config.uri=http://127.0.0.1:${config.port:8888}
spring.cloud.config.name=alan-provider-data-config
spring.cloud.config.profile=${config.profile:dev}
spring.cloud.stream.kafka.binder.brokers=10.79.96.52:9092
spring.cloud.stream.kafka.binder.zk-nodes=10.79.96.52:2182
  引用配置的类要加@RefreshScope注解
@SpringBootApplication
@RestController
@RefreshScope
public class ConfigClientApplication {
@Value("${spring.datasource.username}")
String name = "World";
@RequestMapping("/")
public String home() {
System.out.println(name);
return name;
}
public static void main(String[] args) {
SpringApplication.run(ConfigClientApplication.class, args);
}
}
  3.2 RefreshScope 注解
  我们知道Spring原生提供了一些scope,如singleton,prototype,request等。 为了实现配置更新后,已经注入bean的值也能更新的目的,Spring Cloud提供了一个新的scope - RefreshScope。
  Spring Cloud对RefreshScope的定义如下:
  A Scope implementation that allows for beans to be refreshed dynamically at runtime (see refresh(String) and refreshAll()). If a bean is refreshed then the next time the bean is accessed (i.e. a method is executed) a new instance is created.
  所以,对于那些有注入值的bean,我们可以把它们标记为RefreshScope,这样当运行时发现有配置更新的时候,通过调用RefreshScope.refresh(beanName)或RefreshScope.refreshAll(),从而下次这些bean被使用时会被重新初始化,进而会被重新注入值,所以也达到了更新的目的。
  3.3 ConfigClient 启动顺序
  ConfigClient好要在ConfigServer之后启动,Spring加载配置文件是有顺序的,靠前的配置文件会覆盖靠后的配置文件中相同键的值,如果ConfigServer先启动可以保证ConfigClient将远程的配置文件加载到前面,如果使用中没有注意到这一点,有可能导致你本地的配置文件先于远程的加载,导致本地的配置覆盖远程配置。当然,你也可以让本地配置和远程配置完全不重复,这样也可以避免键/值覆盖的问题。
  后面会进一步说明这部分相关的知识点。
  四. 背景知识
  4.1 Spring中的Environment和PropertySource
  · Environment
  Spring的ApplicationContext会包含一个Environment
  Environment自身包含了很多个PropertySource
  · PropertySource
  属性源
  可以理解为很多个Key - Value的属性配置
  在运行时的结构形如:

  需要注意的是,PropertySource之间是有优先级顺序的,如果有一个Key在多个property source中都存在,那么在前面的property source优先。所以对上图的例子:
  · env.getProperty(“key1”) -> value1
  · env.getProperty(“key2”) -> value2
  · env.getProperty(“key3”) -> value4
  在ConfigClient启动阶段,从ConfigServer获取配置,然后组装成PropertySource并插入到第一个,在随后的获取配置过程中,来自Config Server的配置和其它本地的配置对使用者而言是没有任何差别的,从而实现了无缝集成。