spring-cloud-config配置中心使用细节
微服务架构中需要使用配置中心来统一管理所有服务的配置,spring-cloud-config
可使用git
仓库作为配置的存储服务。配置中心有两种角色,服务端和客户端,服务端从git
仓库获取配置信息,客户端从服务端拉取配置信息。
使用spring-boot-2.7.3
和spring-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
表示指定使用的分支,在此指定了master
,search-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.yaml
和search-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.yaml
和dev/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);
}
}