spring-cloud-config配置中心使用细节

微服务架构中需要使用配置中心来统一管理所有服务的配置,spring-cloud-config可使用git仓库作为配置的存储服务。配置中心有两种角色,服务端和客户端,服务端从git仓库获取配置信息,客户端从服务端拉取配置信息。

使用spring-boot-2.7.3spring-cloud-2021.0.3演示配置中心的使用过程

1、服务端配置
  • 需要引入配置中心依赖
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-config-server</artifactId>
</dependency>
  • 启动类中增加配置@EnableConfigServer

  • 配置文件application.yaml如下,使用gitee演示,实际可能是自建的gitlab

server:
  port: 8099
spring:
  cloud:
    config:
      server:
        git:
          uri: https://gitee.com/xxx/spring-cloud-config.git
          username: xxx
          password: root123456
          default-label: master
          search-paths: '{profile}/{application}'
logging:
  level:
    root: info
    _org.springframework.web.servlet.HandlerMapping.Mappings: debug

需要注意其中的几个参数,default-label表示指定使用的分支,在此指定了mastersearch-paths是一个数组,是指哪些地方搜索配置文件,默认是根目录且不会搜索子目录,由于微服务的配置信息众多,所以这个配置项就比较有讲究了。如在使用中生产、非生产环境是独立的两套gitlab,非生产环境又区分开发环境(dev)、测试环境(test)、预发布环境(pre),使用中可以按一定的规则来规范配置文件,避免配置过于混乱。如按'{profile}/{application}'配置的情况下,由在git根目录下的配置如下

.
├── dev
│   ├── admin-service
│   │   └── admin-service-dev.yaml
│   └── search-service
│       └── search-service-dev.yaml
├── pre
│   ├── admin-service
│   │   └── admin-service-pre.yaml
│   └── search-service
│       └── search-service-pre.yaml
└── test
    ├── admin-service
    │   └── admin-service-test.yaml
    └── search-service
        └── search-service-test.yaml

profile首先针对不同的环境配置区分目录,再按application(即spring.application.name)应用名来区分目录。

HandlerMapping.Mappings设置为debug是为了打印出相关的请求路径

  • 启动配置中心后可观察配置的结果

http://localhost:8099/search-service/dev 或者可直接访问http://localhost:8099/search-service-dev.yaml查看配置信息。

可以发现每一次请求服务端都有一行日志

INFO 353550 --- [nio-8099-exec-1] o.s.c.c.s.e.NativeEnvironmentRepository  : Adding property source: Config resource 'file [/tmp/config-repo-8683050507780455598/dev/search-service/search-service-dev.yaml]' via location 'file:/tmp/config-repo-8683050507780455598/dev/search-service/'

进入到临时目录查看相应的状态

[root@VM-12-14-centos ~]# cd /tmp/config-repo-8683050507780455598
[root@VM-12-14-centos config-repo-8683050507780455598]# git status
On branch master
Your branch is up to date with 'origin/master'.

nothing to commit, working tree clean
[root@VM-12-14-centos config-repo-8683050507780455598]# 

可以看到是将整个配置项目下载到了临时目录,需注意客户端的请求路径search-service-dev.yamlsearch-paths的路径不是同一回事,日志可观察到客户端可请求的路径。

  • 扩展一下关于search-paths配置多个目录

前面看到search-paths是一个数组,表示可以在多个目录中搜索配置,那么如果多个目录下有相同的配置文件,且配置的值不一样是什么样的取值规则呢?如在temp目录下也有个配置文件search-service-dev.yaml

search-paths:
  - '{profile}/{application}'
  - temp

目录结构如下

.
├── dev
│   └── search-service
│       └── search-service-dev.yaml
└── temp
    └── search-service-dev.yaml

跟踪接口 http://localhost:8099/search-service-dev.yaml查看配置获取的规则,在类org.springframework.cloud.config.server.environment.EnvironmentController的方法yaml中跟踪,如下

@GetMapping({ "/{name}-{profiles}.yml", "/{name}-{profiles}.yaml" })
public ResponseEntity<String> yaml(@PathVariable String name, @PathVariable String profiles,
		@RequestParam(defaultValue = "true") boolean resolvePlaceholders) throws Exception {
	return labelledYaml(name, profiles, null, resolvePlaceholders);
}

继续跟踪重要方法convertToMap=>convertToProperties

Map<String, Map<String, Object>> map = new LinkedHashMap<>();
List<PropertySource> sources = new ArrayList<>(profiles.getPropertySources());
Collections.reverse(sources);
Map<String, Object> combinedMap = new LinkedHashMap<>();
for (PropertySource source : sources) {

	@SuppressWarnings("unchecked")
	Map<String, Object> value = (Map<String, Object>) source.getSource();
	for (String key : value.keySet()) {

		if (!key.contains("[")) {

			// Not an array, add unique key to the map
			combinedMap.put(key, value.get(key));
    ... 省略

可以看到非数组的话,一个for循环里面由combinedMap不断将值设置进去,所以在sources这个列表的配置项中,相同的配置项后面的将会把前面的覆盖掉,即在前面的配置中如果temp/search-service-dev.yamldev/search-service/search-service-dev.yaml有相同配置项的话,将使用temp目录中的配置值。同时,可以关注到一个细节代码Collections.reverse(sources);,将sources反转了,莫非原来的值不是按先'{profile}/{application}'temp这个配置顺序吗?可以调试一下发现,反转前temp是在前面,经过reverse后才跟配置文件中的数组顺序一致。

跟着这个思路继续找一下是如何初始化的,从方法labelled中跟踪,下一步是找到NativeEnvironmentRepository.findOne以及ConfigDataEnvironmentPostProcessor.applyTo

Environment environment = labelled(name, profiles, label);

再往后跟踪路径如下

postProcessor.postProcessEnvironment
->
getConfigDataEnvironment(environment, resourceLoader, additionalProfiles).processAndApply();`,
->
processInitial(this.contributors, importer);
->
contributors = contributors.withProcessedImports(importer, null);
-> 在一个while(true)里面解析并加载
importer.resolveAndLoad(activationContext, locationResolverContext, loaderContext, imports);
->
return load(loaderContext, resolved);

其中最关键的地方就是这个load方法了,此时的candidates顺序依然是按配置文件中搜索路径数组的顺序,结果此处的列表循环是倒过来的,这就解析了前面为什么需要反转了。

//org.springframework.boot.context.config.ConfigDataImporter
private Map<ConfigDataResolutionResult, ConfigData> load(ConfigDataLoaderContext loaderContext,
		List<ConfigDataResolutionResult> candidates) throws IOException {
	Map<ConfigDataResolutionResult, ConfigData> result = new LinkedHashMap<>();
	for (int i = candidates.size() - 1; i >= 0; i--) {
...省略
	}
	return Collections.unmodifiableMap(result);
}
2、客户端配置
  • 引入客户端依赖
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-config-client</artifactId>
</dependency>

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>

需要注意在bootstrap.yaml文件中配置

  • 配置文件bootstrap.yaml内容如下
server:
  port: 8088
spring:
  application:
    name: search-service
  cloud:
    config:
      uri: http://localhost:8099
  profiles:
    active: dev
logging:
  level:
    root: info
    org.springframework.web.client.RestTemplate: debug

跟踪RestTemplate的请求,可以查看从配置中心获取配置的请求信息

: Fetching config from server at : http://localhost:8099
: HTTP GET http://localhost:8099/search-service/dev
: Accept=[application/json, application/*+json]
: Response 200 OK
: Reading to [org.springframework.cloud.config.environment.Environment]

按目前的配置,无法实现动态配置刷新,需要增加spring-cloud-bus实现

3、动态刷新配置

动态刷新配置可以使用spring-cloud-bus实现,在gitee或自建的gitlab上配置webhook触发配置更新。

  • 服务端、客户端增加以下依赖,并均需配置上rabbitmq的配置
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
spring:
  rabbitmq:
    host: 192.168.1.88
    username: admin
    password: admin123
    virtual-host: /config
  • 配置中心服务端增加刷新端点配置
management:
  endpoints:
    web:
      exposure:
        include: '*'
      base-path: /actuator
  server:
    port: 8090

表示暴露所有请求,所以刷新配置中心的请求地址就为POST http://localhost:8090/actuator/busrefresh,将此地址配置到webhook中就可以实现配置变更后动态刷新了。

  • 客户端需要动态更新配置的参数增加@RefreshScope
@RefreshScope
@Slf4j
@RequestMapping("/config")
@RestController
public class ConfigController {

    @Value("${search.version:}")

    private String value;

    @GetMapping
    public void search() {
        log.info("{}", value);
    }

}

赞赏(Donation)
微信(Wechat Pay)

donation-wechatpay