一,统一配置管理
到目前为止我们已经解决了微服务相关的几个问题:
- 微服务远程调用
- 微服务注册、发现
- 微服务请求路由、负载均衡
- 微服务登录用户信息传递
不过,现在依然还有几个问题需要解决:
- 网关路由在配置文件中写死了,如果变更必须重启微服务
- 某些业务配置在配置文件中写死了,每次修改都要重启服务
- 每个微服务都有很多重复的配置,维护成本高
这些问题都可以通过统一的配置管理器服务解决。而 Nacos 不仅仅具备注册中心功能,也具备配置管理的功能:

1.1 配置共享
我们可以把微服务共享的配置抽取到 Nacos 中统一管理,这样就不需要每个微服务都重复配置了。分为两步:
- 在Nacos中添加共享配置
- 微服务拉取配置
1.1.1 在Nacos中添加共享配置
以 cart-service 为例,我们看看有哪些配置是重复的,可以抽取的:
首先是 jdbc 相关配置:

然后是日志配置:

然后是 swagger 以及 OpenFeign 的配置:

我们在 nacos 控制台分别添加这些配置。
- 首先是jdbc相关配置,在
配置管理->配置列表中点击+新建一个配置:

在弹出的表单中填写信息:

注意这里的 jdbc 的相关参数并没有写死,例如:
数据库ip:通过${hm.db.host:192.168.150.101}配置了默认值为192.168.150.101,同时允许通过${hm.db.host}来覆盖默认值数据库端口:通过${hm.db.port:3306}配置了默认值为3306,同时允许通过${hm.db.port}来覆盖默认值数据库database:可以通过${hm.db.database}来设定,无默认值
然后是统一的日志配置,命名为shared-log.``yaml,配置内容如下
logging:
level:
com.hmall: debug
pattern:
dateformat: HH:mm:ss:SSS
file:
path: "logs/${spring.application.name}"
然后是统一的 swagger 配置,命名为shared-swagger.yaml,配置内容如下:
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.1.2 微服务拉取配置
接下来,我们要在微服务拉取共享配置。将拉取到的共享配置与本地的application.yaml配置合并,完成项目上下文的初始化。
不过,需要注意的是,读取 Nacos 配置是 SpringCloud 上下文(ApplicationContext)初始化时处理的,发生在项目的引导阶段。然后才会初始化 SpringBoot 上下文,去读取application.yaml。
也就是说引导阶段,application.yaml文件尚未读取,根本不知道 nacos 地址,该如何去加载 nacos 中的配置文件呢?
SpringCloud 在初始化上下文的时候会先读取一个名为bootstrap.yaml( 或者bootstrap.properties) 的文件,如果我们将 nacos 地址配置到bootstrap.yaml中,那么在项目引导阶段就可以读取 nacos 中的配置了。

因此,微服务整合 Nacos 配置管理的步骤如下:
-
引入依赖:在 cart-service 模块引入依赖:
<!--nacos配置管理--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> <!--读取bootstrap文件--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bootstrap</artifactId> </dependency> -
新建 bootstrap.yaml
spring: application: name: cart-service # 服务名称 profiles: active: dev cloud: nacos: server-addr: 192.168.150.101 # nacos地址 config: file-extension: yaml # 文件后缀名 shared-configs: # 共享配置 - dataId: shared-jdbc.yaml # 共享mybatis配置 - dataId: shared-log.yaml # 共享日志配置 - dataId: shared-swagger.yaml # 共享日志配置 -
修改 application.yaml
由于一些配置挪到了 bootstrap.yaml,因此 application.yaml 需要修改为:
server: port: 8082 feign: okhttp: enabled: true # 开启OKHttp连接池支持 hm: swagger: title: 购物车服务接口文档 package: com.hmall.cart.controller db: database: hm-cart
2.2 配置热更新
有很多的业务相关参数,将来可能会根据实际情况临时调整。例如购物车业务,购物车数量有一个上限,默认是 10,对应代码如下:

现在这里购物车是写死的固定值,我们应该将其配置在配置文件中,方便后期修改。但现在的问题是,即便写在配置文件中,修改了配置还是需要重新打包、重启服务才能生效。能不能不用重启,直接生效呢?
这就要用到 Nacos 的配置热更新能力了,分为两步:
- 在Nacos中添加配置
- 在微服务读取配置
2.2.1 添加配置到Nacos
首先,我们在 nacos 中添加一个配置文件,将购物车的上限数量添加到配置中:

注意文件的 dataId 格式:
[服务名]-[spring.active.profile].[后缀名]
文件名称由三部分组成:
服务名:我们是购物车服务,所以是cart-servicespring.active.profile:就是spring boot中的spring.active.profile,可以省略,则所有profile共享该配置后缀名:例如yaml
这里我们直接使用cart-service.yaml这个名称,则不管是 dev 还是 local 环境都可以共享该配置。
配置内容如下:
hm:
cart:
maxAmount: 1 # 购物车商品数量上限
提交配置,在控制台能看到新添加的配置:

2.2.2 配置热更新
接着,我们在微服务中读取配置,实现配置热更新。在cart-service中新建一个属性读取类:

代码如下:
package com.hm.cart.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@RefreshScope
@Data
@Component
@ConfigurationProperties(prefix = "hm.cart")
public class CartProperties {
private Integer maxAmount;
}
接着,在业务中使用该属性加载类:

3.3 动态路由
网关的路由配置全部是在项目启动时由
org.springframework.cloud.gateway.route.CompositeRouteDefinitionLocator在项目启动的时候加载,并且一经加载就会缓存到内存中的路由表内(一个 Map),不会改变。也不会监听路由变更,所以,我们无法利用配置热更新来实现路由更新。
因此,我们必须监听 Nacos 的配置变更,然后手动把最新的路由更新到路由表中。这里有两个难点:
- 如何监听Nacos配置变更?
- 如何把路由信息更新到路由表?
3.3.1 监听Nacos配置变更
在 Nacos 官网中给出了手动监听 Nacos 配置变更的 SDK:Java SDK
如果希望 Nacos 推送配置变更,可以使用 Nacos 动态监听配置接口来实现。
public void addListener(String dataId, String group, Listener listener)
请求参数说明:
| 参数名 | 参数类型 | 描述 |
|---|---|---|
| dataId | string | 配置 ID,保证全局唯一性,只允许英文字符和 4 种特殊字符("."、":"、"-"、"_")。不超过 256 字节。 |
| group | string | 配置分组,一般是默认的DEFAULT_GROUP。 |
| listener | Listener | 监听器,配置变更进入监听器的回调函数。 |
示例代码:
String serverAddr = "{serverAddr}";
String dataId = "{dataId}";
String group = "{group}";
// 1.创建ConfigService,连接Nacos
Properties properties = new Properties();
properties.put("serverAddr", serverAddr);
ConfigService configService = NacosFactory.createConfigService(properties);
// 2.读取配置
String content = configService.getConfig(dataId, group, 5000);
// 3.添加配置监听器
configService.addListener(dataId, group, new Listener() {
@Override
public void receiveConfigInfo(String configInfo) {
// 配置变更的通知处理
System.out.println("recieve1:" + configInfo);
}
@Override
public Executor getExecutor() {
return null;
}
});
这里核心的步骤有 2 步:
- 创建ConfigService,目的是连接到Nacos
- 添加配置监听器,编写配置变更的通知处理逻辑
由于我们采用了spring-cloud-starter-alibaba-nacos-config自动装配,因此ConfigService已经在com.alibaba.cloud.nacos.NacosConfigAutoConfiguration中自动创建好了:

NacosConfigManager 中是负责管理 Nacos 的 ConfigService 的,具体代码如下:

因此,只要我们拿到NacosConfigManager就等于拿到了ConfigService,第一步就实现了。
第二步,编写监听器。虽然官方提供的 SDK 是 ConfigService 中的 addListener,不过项目第一次启动时不仅仅需要添加监听器,也需要读取配置,因此建议使用的 API 是这个:
String getConfigAndSignListener(
String dataId, // 配置文件id
String group, // 配置组,走默认
long timeoutMs, // 读取配置的超时时间
Listener listener // 监听器
) throws NacosException;
既可以配置监听器,并且会根据 dataId 和 group 读取配置并返回。我们就可以在项目启动时先更新一次路由,后续随着配置变更通知到监听器,完成路由更新。
3.3.2 更新路由
更新路由要用到org.springframework.cloud.gateway.route.RouteDefinitionWriter这个接口:
package org.springframework.cloud.gateway.route;
import reactor.core.publisher.Mono;
/**
* @author Spencer Gibb
*/
public interface RouteDefinitionWriter {
/**
* 更新路由到路由表,如果路由id重复,则会覆盖旧的路由
*/
Mono<Void> save(Mono<RouteDefinition> route);
/**
* 根据路由id删除某个路由
*/
Mono<Void> delete(Mono<String> routeId);
}
这里更新的路由,也就是 RouteDefinition,之前我们见过,包含下列常见字段:
- id:路由id
- predicates:路由匹配规则
- filters:路由过滤器
- uri:路由目的地
将来我们保存到 Nacos 的配置也要符合这个对象结构,将来我们以 JSON 来保存,格式如下:
{
"id": "item",
"predicates": [{
"name": "Path",
"args": {"_genkey_0":"/items/**", "_genkey_1":"/search/**"}
}],
"filters": [],
"uri": "lb://item-service"
}
以上 JSON 配置就等同于:
spring:
cloud:
gateway:
routes:
- id: item
uri: lb://item-service
predicates:
- Path=/items/**,/search/**
3.3.3 实现动态路由
-
我们在网关 gateway 引入依赖
<!--统一配置管理--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> <!--加载bootstrap--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bootstrap</artifactId> </dependency> -
在网关
gateway的resources目录创建bootstrap.yaml文件,内容如下: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,把之前的路由移除,最终内容如下:server: port: 8080 # 端口 hm: jwt: location: classpath:hmall.jks # 秘钥地址 alias: hmall # 秘钥别名 password: hmall123 # 秘钥文件密码 tokenTTL: 30m # 登录有效期 auth: excludePaths: # 无需登录校验的路径 - /search/** - /users/login - /items/** -
在
gateway中定义配置监听器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; // 路由配置文件的id和分组 private final String dataId = "gateway-routes.json"; private final String group = "DEFAULT_GROUP"; // 保存更新过的路由id private final Set<String> routeIds = new HashSet<>(); @PostConstruct public void initRouteConfigListener() throws NacosException { // 1.注册监听器并首次拉取配置 String configInfo = nacosConfigManager.getConfigService() .getConfigAndSignListener(dataId, group, 5000, new Listener() { @Override public Executor getExecutor() { return null; } @Override public void receiveConfigInfo(String configInfo) { updateConfigInfo(configInfo); } }); // 2.首次启动时,更新一次配置 updateConfigInfo(configInfo); } private void updateConfigInfo(String configInfo) { log.debug("监听到路由配置变更,{}", configInfo); // 1.反序列化 List<RouteDefinition> routeDefinitions = JSONUtil.toList(configInfo, RouteDefinition.class); // 2.更新前先清空旧路由 // 2.1.清除旧路由 for (String routeId : routeIds) { writer.delete(Mono.just(routeId)).subscribe(); } routeIds.clear(); // 2.2.判断是否有新的路由要更新 if (CollUtils.isEmpty(routeDefinitions)) { // 无新路由配置,直接结束 return; } // 3.更新路由 routeDefinitions.forEach(routeDefinition -> { // 3.1.更新路由 writer.save(Mono.just(routeDefinition)).subscribe(); // 3.2.记录路由id,方便将来删除 routeIds.add(routeDefinition.getId()); }); } } -
在 Nacos 控制台添加路由,路由文件名为
gateway-routes.json,类型为json:[ { "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" } ]
统一配置管理
本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。