配置管理
配置管理
配置管理
微服务常常需要修改业务配置,面临以下问题:
- 网关路由在配置文件中写死了,如果变更必须重启微服务
- 某些业务配置在配置文件中写死了,每次修改都要重启服务
- 每个微服务都有很多重复的配置,维护成本高
这些问题都可以通过统一的配置管理器服务解决。而Nacos不仅仅具备注册中心功能,也具备配置管理的功能:

微服务共享的配置可以统一交给Nacos保存和管理,在Nacos控制台修改配置后,Nacos会将配置变更推送给相关的微服务,并且无需重启即可生效,实现配置热更新。
网关的路由同样是配置,因此同样可以基于这个功能实现动态路由功能,无需重启网关即可修改路由配置。
配置共享
添加共享配置
我们在nacos控制台分别添加这些配置。
首先是jdbc相关配置,在配置管理->配置列表中点击+新建一个配置:
数据库共享配置:
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}来覆盖默认值
- 数据库端口:通过{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}来覆盖。
拉取共享配置
接下来,我们要在微服务拉取共享配置。将拉取到的共享配置与本地的application.yaml配置合并,完成项目上下文的初始化。
不过,需要注意的是,读取Nacos配置是SpringCloud上下文(ApplicationContext)初始化时处理的,发生在项目的引导阶段。然后才会初始化SpringBoot上下文,去读取application.yaml。
也就是说引导阶段,application.yaml文件尚未读取,根本不知道nacos 地址,该如何去加载nacos中的配置文件呢?
SpringCloud在初始化上下文的时候会先读取一个名为bootstrap.yaml(或者bootstrap.properties)的文件,如果我们将nacos地址配置到bootstrap.yaml中,那么在项目引导阶段就可以读取nacos中的配置了。

因此,微服务整合Nacos配置管理的步骤如下:
1)引入依赖:
在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>2)新建bootstrap.yaml
在resources目录新建一个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重启服务,发现所有配置都生效了。
配置热更新
首先,我们在nacos中添加一个配置文件
注意文件的dataId格式:[服务名]-[spring.active.profile].[后缀名]
文件名称由三部分组成:
- 服务名:我们是购物车服务,所以是cart-service
- spring.active.profile:就是spring boot中的spring.active.profile,可以省略,则所有profile共享该配置
- 后缀名:例如yaml
这里我们直接使用cart-service.yaml这个名称,则不管是dev还是local环境都可以共享该配置。
配置内容如下:
hm:
cart:
maxAmount: 1 # 购物车商品数量上限可以通过配置属性类参考读取:
@Data
@Component
@ConfigurationProperties(prefix = "hm.cart")
public class CartProperties {
private Integer maxAmount;
}动态路由
- 传统方式:静态路由的痛点 (The "Old" Way)
在没有动态路由时,我们通常将路由规则写在网关(Spring Cloud Gateway 或 Zuul)的配置文件 application.yml 或 application.properties 中。
# 静态路由示例
spring:
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/order/**这种硬编码方式带来了三大致命问题:
必须重启网关: 每次修改路由(例如:更改转发路径、添加新服务、修改限流参数),都必须修改配置文件 -> 重新打包/部署 -> 重启网关。
服务中断风险: 网关是所有流量的入口。重启网关意味着在重启期间,整个系统的外部访问会中断(除非你做了复杂的灰度停机摘除,但依然有风险)。
运维效率低下: 如果你的网关部署了 10 个节点(集群模式),你需要逐个登录服务器修改配置文件并重启,极易出错且耗时。
- 动态路由:Nacos 配置管理的福音 (The "New" Way)
网关的路由配置全部是在项目启动时由org.springframework.cloud.gateway.route.CompositeRouteDefinitionLocator在项目启动的时候加载,并且一经加载就会缓存到内存中的路由表内(一个Map),不会改变。也不会监听路由变更,所以,我们无法利用上节课学习的配置热更新来实现路由更新。
因此,我们必须监听Nacos的配置变更,然后手动把最新的路由更新到路由表中。这里有两个难点:
- 如何监听Nacos配置变更?
- 如何把路由信息更新到路由表?
监听nacos配置
在Nacos官网中给出了手动监听Nacos配置变更的SDK
https://nacos.io/zh-cn/docs/sdk.html
如果希望 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读取配置并返回。我们就可以在项目启动时先更新一次路由,后续随着配置变更通知到监听器,完成路由更新。
更新路由
更新路由要用到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配置就等同于和RouteDefinition对象一致的配置:
spring:
cloud:
gateway:
routes:
- id: item
uri: lb://item-service
predicates:
- Path=/items/**,/search/**OK,我们所需要用到的SDK已经齐全了。
实现动态路由
网关导入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: gateway # 服务名称
profiles:
active: dev
cloud:
nacos:
server-addr: 192.168.150.101 # nacos地址
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中定义配置监听器:
@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 //bean初始化时候执行
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());
});
}
}重启网关,任意访问一个接口,比如 http://localhost:8080/search/list?pageNo=1&pageSize=1:
发现是404,无法访问。
接下来,我们直接在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"
}
]无需重启网关,稍等几秒钟后,再次访问刚才的地址:
网关路由成功了!
