Spring Cloud -Hoxton.RELEASE(四):配置中心-Config

Spring Cloud Config

Config的Github页面Readme对Config描述如下:

Spring Cloud Config provides server-side and client-side support for externalized configuration in a distributed system. With the Config Server, you have a central place to manage external properties for applications across all environments. The concepts on both client and server map identically to the Spring Environment and PropertySource abstractions, so they fit very well with Spring applications but can be used with any application running in any language. As an application moves through the deployment pipeline from dev to test and into production, you can manage the configuration between those environments and be certain that applications have everything they need to run when they migrate. The default implementation of the server storage backend uses git, so it easily supports labelled versions of configuration environments as well as being accessible to a wide range of tooling for managing the content. It is easy to add alternative implementations and plug them in with Spring configuration.

Spring Cloud Config为分布式系统中的外部化配置提供服务器端和客户端支持。使用Config Server,您可以在所有环境中管理应用程序的外部属性。客户端和服务器上的概念映射与Spring Environment和PropertySource抽象,因此它们非常适合Spring应用程序,但可以与任何语言运行的任何应用程序一起使用。当应用程序通过部署管道从开发到测试再到生产时,您可以管理这些环境之间的配置,并确保应用程序具有迁移时需要运行的所有内容。服务器存储后端的默认实现使用git,因此它可以轻松支持配置环境的标签版本,以及可用于管理内容的各种工具。添加替代实现并使用Spring配置插入它们很容易。

简单来讲(从名字看)官方对Config的定位是外部化配置支持

创建Config项目

创建config项目依赖继承config-server项目,pom.xml文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.github.liuzhuoming23</groupId>
<artifactId>config-center</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<packaging>jar</packaging>

<artifactId>config</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>config</name>
<description>Demo project for Spring Cloud</description>

<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

application.yml配置为:

server:
port: 8040

spring:
application:
name: @pom.artifactId@
cloud:
config:
server:
git:
#存放配置文件的git仓库地址
uri: https://gitee.com/liuzhuoming23/config-repo
#配置文件的存放路径(可以根据目录名存放不同项目的配置文件)
search-paths: config
#git账号密码,公共仓库不需要配置
username:
password:

因为连接速度原因,这里选择了gitee作为配置仓库。
ConfigApplication启动类添加EnableConfigServer注解:

package com.gihub.liuzhuoming23.config;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;

/**
* 启动类
*
* @author x-047
*/
@SpringBootApplication
@EnableConfigServer
public class ConfigApplication {

public static void main(String[] args) {
SpringApplication.run(ConfigApplication.class, args);
}

}

git仓库的项目目录结构如下:

config
└─producer-client-dev.yml
└─producer-client-test.yml

其中producer-client-dev.yml文件内容为:

config:
name: 🍺🍐🐮🍺🐎

producer-client-test.yml文件内容为:

config.name: 🍺🍐🍺🍐🐮🍺

启动项目,访问http://localhost:8040/producer-client/dev,返回结果:

{
"name": "producer-client",
"profiles": [
"dev"
],
"label": null,
"version": "0c998449b05236b778b072ec7bd28d1f09cfc5a1",
"state": null,
"propertySources": [
{
"name": "https://gitee.com/liuzhuoming23/config-repo/config/producer-client-dev.yml",
"source": {
"config.name": "🍺🍐🐮🍺🐎"
}
}
]
}

访问http://localhost:8040/producer-client/test,返回结果:

{
"name": "producer-client",
"profiles": [
"test"
],
"label": null,
"version": "a124d99007d7ad0c849878ee1d8e9624a77368c3",
"state": null,
"propertySources": [
{
"name": "https://gitee.com/liuzhuoming23/config-repo/config/producer-client-test.yml",
"source": {
"config.name": "🍺🍐🍺🍐🐮🍺"
}
}
]
}

说明项目配置成功。
可以看出读取哪个文件的配置是根据url的后面两段来匹配的,http://localhost:8040/producer-client/test匹配producer-client-testhttp://localhost:8040/producer-client/dev匹配producer-client-dev,和spring的profiles十分类似,实际上在本doom里面就是当成profiles来使用的,producer-client为服务名,test/dev为profiles。

服务生产者获取外部配置

修改config项目的pom.xml文件,添加spring-cloud-starter-netflix-eureka-client依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.github.liuzhuoming23</groupId>
<artifactId>config-center</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<packaging>jar</packaging>

<artifactId>config</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>config</name>
<description>Demo project for Spring Cloud</description>

<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

然后把config服务注册到eureka,即修改application.yml为:

server:
port: 8040

spring:
application:
name: @pom.artifactId@
cloud:
config:
server:
git:
uri: https://gitee.com/liuzhuoming23/config-repo
search-paths: config
username:
password:

eureka:
client:
service-url:
defaultZone: http://admin:admin@localhost:8000/eureka/

然后在producer-client项目的pom.xml添加spring-cloud-starter-config依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.github.liuzhuoming23</groupId>
<artifactId>server-center</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<packaging>jar</packaging>

<artifactId>producer-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>producer-client</name>
<description>Demo project for Spring Cloud</description>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

在application.xml同级新建bootstrap.yml文件,并添加内容:

spring:
cloud:
config:
#服务名称
name: producer-client
#profiles
profile: dev
#分支/tag
label: master
discovery:
#是否从注册中心获取配置服务
enabled: true
#配置服务名称
service-id: config

eureka:
instance:
hostname: localhost
client:
service-url:
defaultZone: http://admin:admin@localhost:8000/eureka/

注意这里的服务名称不能是@pom.artifactId@这种从pom.xml中获取参数值的方式,必须配置完整的项目名称。这与bootstrap.xml和application.xml的加载时机有关,不在此赘述,有兴趣自行了解。
修改application.yml为:

server:
port: 8010

spring:
application:
name: @pom.artifactId@

在controller.PortController类添加可从配置中心获取的键名称:

package com.github.liuzhuoming23.producerclient.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* port
*
* @author liuzhuoming
*/
@RestController
@RequestMapping("port")
public class PortController {

@Value("${server.port:0}")
private Integer port;
@Value("${config.name:default}")
private String config;

@GetMapping("{name}")
public String port(@PathVariable String name) {
return config + " | " + name + ": client port | " + port;
}
}

按照eureka->config->producer-client的顺序启动项目,并访问http://localhost:8010/port/test,返回结果:
🍺🍐🐮🍺🐎 | test: client port | 8010
说明服务生产者已经从配置中心获取到了配置。

动态刷新外部配置

在producer-client项目的controller.PortController类添加添加@RefreshScope注解:

package com.github.liuzhuoming23.producerclient.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* port
*
* @author liuzhuoming
*/
@RestController
@RequestMapping("port")
@RefreshScope
public class PortController {

@Value("${server.port:0}")
private Integer port;
@Value("${config.name:default}")
private String config;

@GetMapping("{name}")
public String port(@PathVariable String name) {
return config + " | " + name + ": client port | " + port;
}
}

修改application.yml为:

server:
port: 8010

spring:
application:
name: @pom.artifactId@

management:
endpoints:
web:
exposure:
#开启actuator监控,其中refresh意思是支持刷新配置
include: refresh,info,health

重启producer-client项目,并访问http://localhost:8010/port/test,返回结果:
🍺🍐🐮🍺🐎 | test: client port | 8010
然后修改git里面的config/producer-client-dev.yml文件为:

config:
name: 🍺🍐🙅‍

在Postman用POST请求访问http://localhost:8010/actuator/refresh刷新配置,然后再访问http://localhost:8010/port/test,返回结果:
🍺🍐🙅‍ | test: client port | 8010
说明动态刷新外部配置成功。
很多博客有说到利用git服务的Webhook来实现提交git更新自动刷新外部配置,其实就是把http://localhost:8010/actuator/refresh这个刷新地址绑定在WebHook上。不过因为内网doom,没办法进行测试。

服务生产者集群刷新外部配置

启动8010和8011的两个服务生产者实例,并修改git里面的config/producer-client-dev.yml文件为:

config:
name: 🍺🍐🍺🍐

在Postman用POST请求访问http://localhost:8010/actuator/refresh刷新8010端口服务的配置,然后再访问http://localhost:8010/port/test,返回结果:
🍺🍐🍺🍐 | test: client port | 8011
然后再直接访问http://localhost:8011/port/test,返回结果:
🍺🍐🙅‍ | test: client port | 8011
可见并未按照服务来刷新整个集群的配置。
之后在Postman用POST请求访问http://localhost:8011/actuator/refresh刷新8011端口服务的配置,然后再访问http://localhost:8011/port/test,返回结果:
🍺🍐🍺🍐 | test: client port | 8011
可见刷新哪个实例的refresh endpoint,哪个实例的配置才会更新。
在集群实例比较少的时候似乎还凑合。要是集群实例比较多这样一个个刷新配置就十分的麻烦,集群扩增后也要手动添加新的刷新地址,并且因为需要请求的刷新地址比较多,也很难做到利用git服务的WebHook来自动刷新配置,甚至有漏刷新的可能。所以不建议在线上服务使用。
要是注意观察,可以发现在第一次刷新8010端口服务的时候返回的是🍺🍐🍺🍐 | test: client port | 8011,而并不是我们期待的🍺🍐🍺🍐 | test: client port | 8010,看上去似乎是config服务将同一个服务后启动的实例的本地配置保存到了动态配置中,并在动态刷新外部配置的时候一并刷新到了其他实例的配置里。虽然目前还未造成影响,但是这个特性也许会在没有使用消息总线的情况下让一些类似于灰度更新的需求变得更加难以实现。