配置管理
在微服务的世界里,我们已经攻克了不少难关:
- 微服务之间的远程调用顺畅了
- 服务能自动注册和发现,不用手动记地址了
- 请求路由和负载均衡也搞定了,流量分配更合理
- 登录用户的信息能在服务间顺畅传递,权限校验不再头疼
但新的问题又冒了出来,就像清理完一批杂草又长出新的:
- 网关的路由规则写死在配置文件里,改个路径都得重启服务,太影响用户体验了
- 有些业务参数,比如购物车能装多少商品,也硬编码在配置里,改一次就得重新打包、重启,效率低得让人着急
- 每个微服务里都有一堆重复的配置,像数据库连接、日志格式这些,改一处得同步改好几个地方,维护起来头都大了
还好,这些问题都能靠统一配置管理器解决。而 Nacos 这工具特给力,不仅能当服务注册中心,还自带配置管理功能 —— 就像一个既能管人员考勤又能管物资分配的万能助手。
有了 Nacos,微服务的共享配置可以统一存放在它那。在 Nacos 控制台改完配置,它会主动把新配置推送给相关的微服务,而且不用重启服务就能生效,这就是神奇的 “配置热更新”。网关的路由本质上也是一种配置,自然也能通过这功能实现 “动态路由”,改路由规则时网关照样跑,不用停下来。
1. 配置共享
微服务之间有很多配置是重复的,比如连接数据库的参数、日志的输出格式,要是每个服务都单独配一份,不仅麻烦还容易出错。我们可以把这些重复的配置抽出来,放到 Nacos 里统一管理,就像把大家都要用的工具集中放在一个工具箱里,谁用谁拿。具体分两步:
- 第一步:在 Nacos 里添加这些共享配置
- 第二步:让各个微服务去 Nacos 拉取这些配置
1.1. 添加共享配置
我们以购物车服务(cart-service)为例,看看哪些配置是重复出现、可以抽出来的:
- 首先是 jdbc 相关配置:连接数据库的 URL、驱动类、用户名密码等,几乎每个服务都得用
- 然后是日志配置:日志的级别、输出格式、保存路径等,一般整个系统得保持一致
- 还有 swagger 以及 OpenFeign 的配置:接口文档的基本信息、远程调用的设置等,很多服务都需要
接下来,我们就在 Nacos 控制台一个一个添加这些共享配置。
先处理 jdbc 相关配置:在 Nacos 的 “配置管理 → 配置列表” 里点击 “+” 号新建配置。
在弹出的表单里填写相关信息,其中详细的配置内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| spring: datasource: url: jdbc:mysql://${hm.db.host:192.168.150.101}:${hm.db.port:3306}/${hm.db.database}?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai driver-class-name: com.mysql.cj.jdbc.Driver username: ${hm.db.un:root} password: ${hm.db.pw:123} mybatis-plus: configuration: default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler global-config: db-config: update-strategy: not_null id-type: auto
|
这里的 jdbc 相关参数没有写死,而是用了一种灵活的方式,比如:
- 数据库 ip:通过
${hm.db.host:192.168.150.101} 设置,意思是如果没特别指定 hm.db.host,就用 192.168.150.101 这个默认值,要是想换个 ip,指定 hm.db.host 就行
- 数据库端口:
${hm.db.port:3306} 也是同样的道理,默认 3306,能通过 hm.db.port 修改
- 数据库 database:用
${hm.db.database} 表示,没有默认值,得由具体的服务自己指定,比如购物车服务用 hm-cart,用户服务用 hm-user
这样一来,这个配置模板既能通用,又能满足不同服务的个性化需求,特别方便。
然后是统一的日志配置,给它起名叫 shared-log.yaml,配置内容如下:
1 2 3 4 5 6 7
| logging: level: com.hmall: debug pattern: dateformat: HH:mm:ss:SSS file: path: "logs/${spring.application.name}"
|
再来是统一的 swagger 配置,命名为 shared-swagger.yaml,配置内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| knife4j: enable: true openapi: title: ${hm.swagger.title:黑马商城接口文档} description: ${hm.swagger.description:黑马商城接口文档} email: ${hm.swagger.email:zhanghuyi@itcast.cn} concat: ${hm.swagger.concat:虎哥} url: https://www.itcast.cn version: v1.0.0 group: default: group-name: default api-rule: package api-rule-resources: - ${hm.swagger.package}
|
这里的 swagger 相关配置也没写死,举个例子:
- title:接口文档的标题,用
${hm.swagger.title} 代替,以后哪个服务想用自己的标题,手动指定就行
- email:联系人邮箱,
${hm.swagger.email:zhanghuyi@itcast.cn} 表示默认是 zhanghuyi@itcast.cn,要是想换,用 hm.swagger.email 覆盖掉就行
1.2. 拉取共享配置
接下来,微服务要去拉取 Nacos 里的共享配置,然后和自己本地的 application.yaml 配置合并到一起,完成项目的初始化。
不过这里有个小问题:读取 Nacos 配置是在 SpringCloud 上下文(ApplicationContext)初始化的引导阶段进行的,这时候 SpringBoot 上下文还没开始初始化,本地的 application.yaml 文件都还没读呢 —— 也就是说,这时候微服务根本不知道 Nacos 在哪,怎么去拉配置呢?
别担心,SpringCloud 早就想到了这个问题。它在初始化上下文的时候,会先读一个叫 bootstrap.yaml(或者 bootstrap.properties)的文件。所以我们只要把 Nacos 的地址配置到 bootstrap.yaml 里,微服务在引导阶段就能知道 Nacos 在哪,顺利去拉配置了。
所以,微服务整合 Nacos 配置管理的步骤是这样的:
1)引入依赖:
在 cart-service 模块里引入这些依赖:
1 2 3 4 5 6 7 8 9 10
| <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bootstrap</artifactId> </dependency>
|
2)新建 bootstrap.yaml
在 cart-service 的 resources 目录下新建一个 bootstrap.yaml 文件,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| spring: application: name: cart-service profiles: active: dev cloud: nacos: server-addr: 192.168.150.101 config: file-extension: yaml shared-configs: - dataId: shared-jdbc.yaml - dataId: shared-log.yaml - dataId: shared-swagger.yaml
|
3)修改 application.yaml
因为一些配置挪到 bootstrap.yaml 里了,所以 application.yaml 得改一改:
1 2 3 4 5 6 7 8 9 10 11
| server: port: 8082 feign: okhttp: enabled: true hm: swagger: title: 购物车服务接口文档 package: com.hmall.cart.controller db: database: hm-cart
|
重启服务后,会发现所有配置都正常生效了。
2. 配置热更新
很多业务相关的参数,将来可能会根据实际情况随时调整。比如购物车业务,购物车能装的商品数量有个上限,默认是 10 个,现在这数值是写死在代码里的。
我们应该把这个数值配置在配置文件里,方便以后修改。但问题是,就算写在配置文件里,改完之后还是得重新打包、重启服务才能生效,太麻烦了。能不能不改代码、不重启服务,改了配置就能直接生效呢?
这就得用到 Nacos 的配置热更新能力了,具体分两步:
- 第一步:在 Nacos 里添加这个配置
- 第二步:在微服务里读取这个配置
2.1. 添加配置到 Nacos
首先,我们在 Nacos 里添加一个配置文件,把购物车的上限数量放进去。
要注意这个文件的 dataId 格式:[服务名]-[spring.active.profile].[后缀名]。这个文件名由三部分组成:
- 服务名:我们这里是购物车服务,所以是 cart-service
- spring.active.profile:就是 spring boot 里的 spring.active.profile,要是省略的话,所有环境都能共享这个配置
- 后缀名:比如 yaml
我们这里直接用 cart-service.yaml 这个名称,这样不管是 dev 环境还是 local 环境,购物车服务都能用上这个配置。
配置内容如下:
1 2 3
| hm: cart: maxAmount: 1
|
提交配置后,在 Nacos 控制台就能看到这个新添加的配置了。
2.2. 配置热更新
接着,我们要在微服务里读取这个配置,实现配置热更新。
在 cart-service 里新建一个属性读取类,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12
| package com.hmall.cart.config;
import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component;
@Data @Component @ConfigurationProperties(prefix = "hm.cart") public class CartProperties { private Integer maxAmount; }
|
然后,在业务代码里使用这个属性加载类。
我们来测试一下:先往购物车里添加多个商品,会发现超过上限就加不进去了。
接着,我们在 Nacos 控制台把购物车上限改成 5,不用重启服务,再去测试购物车功能,会发现能成功加入 5 个商品了 —— 配置热更新生效了,就是这么方便!
3. 动态路由
网关的路由配置有点特殊,它是在项目启动的时候,由 org.springframework.cloud.gateway.route.CompositeRouteDefinitionLocator 加载到内存里的路由表里(就是一个 Map),一旦加载完成就固定不变了,也不会去监听路由有没有变更。所以上一节学的配置热更新没法直接用来更新路由。
那怎么办呢?我们得自己想办法:监听 Nacos 的配置变更,然后手动把最新的路由更新到路由表里。这里有两个难点:
- 怎么监听 Nacos 的配置变更?
- 怎么把路由信息更新到路由表里?
3.1. 监听 Nacos 配置变更
Nacos 官网提供了手动监听配置变更的 SDK,地址是:https://nacos.io/zh-cn/docs/sdk.html
如果想让 Nacos 主动推送配置变更,可以用它的动态监听配置接口来实现,就是这个方法:
1
| public void addListener(String dataId, String group, Listener listener)
|
这个方法的参数说明如下:
| 参数名 |
参数类型 |
描述 |
| dataId |
string |
配置 ID,得保证全局唯一,只能用英文字符和 “.”、“:”、“-”、“_” 这 4 种特殊字符,不能超过 256 字节。 |
| group |
string |
配置分组,一般用默认的 DEFAULT_GROUP 就行。 |
| listener |
Listener |
监听器,配置变更的时候会触发这个监听器里的回调函数。 |
示例代码是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| String serverAddr = "{serverAddr}"; String dataId = "{dataId}"; String group = "{group}";
Properties properties = new Properties(); properties.put("serverAddr", serverAddr); ConfigService configService = NacosFactory.createConfigService(properties);
String content = configService.getConfig(dataId, group, 5000);
configService.addListener(dataId, group, new Listener() { @Override public void receiveConfigInfo(String configInfo) { System.out.println("recieve1:" + configInfo); } @Override public Executor getExecutor() { return null; } });
|
这里的核心步骤有两步:
- 创建 ConfigService,目的是连接到 Nacos
- 添加配置监听器,写好配置变更时的处理逻辑
不过我们项目里用了 spring-cloud-starter-alibaba-nacos-config,它会自动帮我们装配好 ConfigService,就在 com.alibaba.cloud.nacos.NacosConfigAutoConfiguration 里。而 NacosConfigManager 是负责管理 Nacos 的 ConfigService 的,所以只要拿到 NacosConfigManager,就等于拿到了 ConfigService,第一步就搞定了。
第二步是编写监听器。虽然官方 SDK 里 ConfigService 的 addListener 方法能用,但项目第一次启动的时候,不仅要添加监听器,还得读取配置,所以建议用这个 API:
1 2 3 4 5 6
| String getConfigAndSignListener( String dataId, // 配置文件id String group, // 配置组,用默认的 long timeoutMs, // 读取配置的超时时间 Listener listener // 监听器 ) throws NacosException;
|
这个方法既能配置监听器,又能根据 dataId 和 group 读取配置并返回。这样我们就能在项目启动时先更新一次路由,之后配置变了,监听器收到通知再更新路由。
3.2. 更新路由
更新路由要用到 org.springframework.cloud.gateway.route.RouteDefinitionWriter 这个接口,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| package org.springframework.cloud.gateway.route;
import reactor.core.publisher.Mono;
public interface RouteDefinitionWriter {
Mono<Void> save(Mono<RouteDefinition> route);
Mono<Void> delete(Mono<String> routeId);
}
|
这里说的路由,也就是 RouteDefinition,它包含这些常见字段:
- id:路由 id
- predicates:路由匹配规则
- filters:路由过滤器
- uri:路由目的地
将来我们存在 Nacos 里的配置也得符合这个对象结构,我们用 JSON 来保存,格式如下:
1 2 3 4 5 6 7 8 9
| { "id": "item", "predicates": [{ "name": "Path", "args": {"_genkey_0":"/items/**", "_genkey_1":"/search/**"} }], "filters": [], "uri": "lb://item-service" }
|
这个 JSON 配置就相当于:
1 2 3 4 5 6 7 8
| spring: cloud: gateway: routes: - id: item uri: lb://item-service predicates: - Path=/items/**,/search/**
|
好了,需要用到的 SDK 都清楚了。
3.3. 实现动态路由
首先,我们在网关 gateway 里引入依赖:
1 2 3 4 5 6 7 8 9 10
| <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bootstrap</artifactId> </dependency>
|
然后在网关 gateway 的 resources 目录下创建 bootstrap.yaml 文件,内容如下:
1 2 3 4 5 6 7 8 9 10
| spring: application: name: gateway cloud: nacos: server-addr: 192.168.150.101 config: file-extension: yaml shared-configs: - dataId: shared-log.yaml
|
接着,修改 gateway 的 resources 目录下的 application.yml,把之前的路由配置删掉,最后内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| server: port: 8080 hm: jwt: location: classpath:hmall.jks alias: hmall password: hmall123 tokenTTL: 30m auth: excludePaths: - /search/** - /users/login - /items/**
|
然后,在 gateway 里定义配置监听器,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| package com.hmall.gateway.route;
import cn.hutool.json.JSONUtil; import com.alibaba.cloud.nacos.NacosConfigManager; import com.alibaba.nacos.api.config.listener.Listener; import com.alibaba.nacos.api.exception.NacosException; import com.hmall.common.utils.CollUtils; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.gateway.route.RouteDefinition; import org.springframework.cloud.gateway.route.RouteDefinitionWriter; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.Executor;
@Slf4j @Component @RequiredArgsConstructor public class DynamicRouteLoader {
private final RouteDefinitionWriter writer; private final NacosConfigManager nacosConfigManager;
private final String dataId = "gateway-routes.json"; private final String group = "DEFAULT_GROUP"; private final Set<String> routeIds = new HashSet<>();
@PostConstruct public void initRouteConfigListener() throws NacosException { String configInfo = nacosConfigManager.getConfigService() .getConfigAndSignListener(dataId, group, 5000, new Listener() { @Override public Executor getExecutor() { return null; }
@Override public void receiveConfigInfo(String configInfo) { updateConfigInfo(configInfo); } }); updateConfigInfo(configInfo); }
private void updateConfigInfo(String configInfo) { log.debug("监听到路由配置变更,{}", configInfo); List<RouteDefinition> routeDefinitions = JSONUtil.toList(configInfo, RouteDefinition.class); for (String routeId : routeIds) { writer.delete(Mono.just(routeId)).subscribe(); } routeIds.clear(); if (CollUtils.isEmpty(routeDefinitions)) { return; } routeDefinitions.forEach(routeDefinition -> { writer.save(Mono.just(routeDefinition)).subscribe(); routeIds.add(routeDefinition.getId()); }); } }
|
重启网关后,随便访问一个接口,比如 http://localhost:8080/search/list?pageNo=1&pageSize=1,会发现是 404,访问不了,这是因为还没配置路由呢。
接下来,我们直接在 Nacos 控制台添加路由,路由文件名叫 gateway-routes.json,类型是 json,配置内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| [ { "id": "item", "predicates": [{ "name": "Path", "args": {"_genkey_0":"/items/**", "_genkey_1":"/search/**"} }], "filters": [], "uri": "lb://item-service" }, { "id": "cart", "predicates": [{ "name": "Path", "args": {"_genkey_0":"/carts/**"} }], "filters": [], "uri": "lb://cart-service" }, { "id": "user", "predicates": [{ "name": "Path", "args": {"_genkey_0":"/users/**", "_genkey_1":"/addresses/**"} }], "filters": [], "uri": "lb://user-service" }, { "id": "trade", "predicates": [{ "name": "Path", "args": {"_genkey_0":"/orders/**"} }], "filters": [], "uri": "lb://trade-service" }, { "id": "pay", "predicates": [{ "name": "Path", "args": {"_genkey_0":"/pay-orders/**"} }], "filters": [], "uri": "lb://pay-service" } ]
|
不用重启网关,等个几秒钟再访问刚才的地址,会发现网关能成功路由了 —— 动态路由实现了!