微服务核心技术全解析:拆分原则 + Nacos + OpenFeign + 网关 + 配置管理
1.关于微服务
1.1 微服务与单体架构的区别
单体架构:将业务的所有功能集中在一个项目中完成,打包成一个包部署。

优点:
- 架构简单
- 部署成本低
缺点:
- 团队协作成本高
- 系统发布效率低
- 系统可用性差(tomcat资源有限,无法解决高并发)
总结:适合开发功能相对简单,规模较小的项目。
微服务架构:是服务化思想指导下的一套最佳实践架构方案。服务化:就是把单体架构中的功能模块拆分成为多个模块。

优点:
- 单一职责(粒度小)
- 团队自洽
- 服务自洽
总结:微服务架构解决了单体架构存在的问题,特别适合大型互联网项目的开发,因此被各大互联网公司普遍采用。分布式就是服务拆分的过程,其实微服务架构正是分布式架构的一种最佳实践的方案。
1.2 SpringCloud 技术
SpringCloud 是目前国内使用最广泛的微服务框架,集成了各种微服务功能和组件,并基于 SpringBoot 实现了这些组件的自动装配,从而提供了良好的开箱即用体验。(SpringCloud 与 SpringBoot 的版本有对应要求)


1.3 总结
- 单体架构:简单方便,高度耦合,扩展性差,适合小型项目。例如:学生管理系统
- 分布式架构:松耦合,扩展性好,但架构复杂,难度大。适合大型互联网项目,例如:京东、淘宝
- 微服务:一种良好的分布式架构方案
①优点:拆分粒度更小、服务更独立、耦合度更低
②缺点:架构非常复杂,运维、监控、部署难度提高
- SpringCloud是微服务架构的一站式解决方案,集成了各种优秀微服务功能组件
2.服务拆分和远程调用
任何分布式架构都离不开服务的拆分,微服务也是一样。
2.1.服务拆分原则
1.什么时候拆:
1)创业型项目:先单体架构 ,快速开发,快速试错,随着规模扩大,逐渐拆分。
【例如】:我们是一个小公司,想到了一个点子,但是由于公司资金和试错能力有限,因此可以先开发一个单体项目,等到项目用户量上来了再拆分成微服务项目。
2)确定的大型项目:资金充足,目标明确,可以直接选择微服务架构。
【例如】:某个大型的车企,想开发一个网上商城,因为这个车企已经有用户寄出了,所以可以直接开发微服务项目。
2.怎么拆(方向):
1)纵向拆分:按照业务模块进行拆分。
【例如】:订单管理、菜品管理、用户管理
2)横向拆分: 抽取公共服务,提高复用性。
【例如】:记录风险操作日志的操作和发送短信的操作很多场景都需要用到,因此可以拆分出来,避免写过多的冗余代码
目标:
- 高内聚:每个微服务的取责尽量单一,包含的业务关联度高,完整度高。
- 低耦合:每个微服务的功能要相对独立,尽量减少对其他服务的依赖。
分类:
1)独立的 project:拆分的每个微服务都是 project ,都放入一个文件夹中,项目结构上是分离的,耦合度低,管理困难。
2)Maven 聚合:拆的每个微服务作为模块,包含在一个项目中。
这里我总结了微服务拆分时的几个原则:
-
不同微服务,不要重复开发相同业务
-
微服务数据独立,不要访问其它微服务的数据库
-
微服务可以将自己的业务暴露为接口,供其它微服务调用
2.2 正式拆分
2.2.1 拆分商品功能模块
首先我们要将项目中的 商品 与 购物车 功能模块拆出,以 hmall 作为父工程在其下创建新的item-service 模块,导入所需的依赖,创建相应结构目录,创建 SpringBoot 的启动类,复制配置文件,将端口改为 8081 避免冲突,以及微服务名称、数据库名称(需提前将资料中单独功能模块的数据库导入),还有底部 swagger 文档配置扫描包结构的更改。


对应目录结构

将 hm-service 中与商品有关的类全部迁移到新建的 item-service 中,拆分的顺序依次为domain ——> mapper ——> service ——> controller,最后再检查是否遗漏相应的 config 配置类、enums 枚举类、utils 工具类等。
到此,商品功能模块的拆分就结束了,再将 service 界面中的 item-service 启动类的运行环境更改为 local 就可以启动服务了,地址栏输入 localhost:8081/doc.html 进入 swagger 接口文档管理界面,测试接口根据 id 批量查询,如下图响应成功返回状态码 200 。

2.2.2 拆分购物车功能模块
拆分购物车功能模块与拆分商品功能模块的方法区别不大,依照上面的步骤可以解决大部分问题,但在 cart-service 功能模块中我们发现需要实现 购物车 中的 实时 显示价格距加入购物车时的变化时,需要调用商品功能中的相应方法,而将这些方法重新引入到cart-service功能模块中时会让代码变得 冗余,导致我们无法查询。

这就引出了我们要学习的新技术 —— RestTemplate(远程调用)
2.3 服务调用介绍
在拆分的时候,我们发现一个问题:就是购物车业务中需要查询商品信息,但商品信息查询的逻辑全部迁移到了 item-service 服务,导致我们无法查询,一旦服务做了拆分,数据做了隔离,每个服务只能使用自己的服务和数据,无法使用别的服务的数据。

最终结果就是查询到的购物车数据不完整,因此要想解决这个问题,我们就必须改造其中的代码,把原本本地方法调用,改造成跨微服务的远程调用(RPC,即Remote Produce Call),解决了微服务之间无法互相调用之间的方法的问题
原理:微服务之间虽然在物理层面上被隔离开来,但是在网络上是可以互通的,我们可以通过网络发送请求(Http),来进行微服务之间的连接。
2.4 RestTemplate
Spring 提供 RestTemplate 工具,实现了 Http 请求的发送,是 Spring 用于同步 client 端的核心类,简化了与 http 服务的通信,程序代码可以给它提供 URL ,并提取结果。默认情况下,RestTemplate 默认依赖jdk的 HTTP 连接工具。
使用方法:
1)先将 RestTemplate 配置到 Spring 容器中,注册成一个 Bean。
@MapperScan("com.hmall.cart.mapper")
@SpringBootApplication
public class CartApplication {
public static void main(String[] args) {
SpringApplication.run(CartApplication.class, args);
}
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
2)发起远程调用。
在 service 的实现类中注入 RestTemplate,可以使用构造函数注入,但我们可以简化。在注入的元素前加上 final 修饰使之必须初始化,必需使用构造函数初始化,但我们可在类上加上 @RequirdArgsConstructor 注解代替构造函数。
@Service
// Lombok注解,会为类中所有final修饰的字段生成带参构造方法,从而实现依赖注入。
@RequiredArgsConstructor
public class CartServiceImpl extends ServiceImpl<CartMapper, Cart> implements ICartService {
private final RestTemplate restTemplate;
}
restTemplate 方法解析
restTemplate(url, 请求方式, 请求实体, 返回值类型, 请求参数)
public <T> ResponseEntity<T> exchange(
String url, // 请求路径
HttpMethod method, // 请求方式
@Nullable HttpEntity<?> requestEntity, // 请求实体,可以为空
Class<T> responseType, // 返回值类型
Map<String, ?> uriVariables // 请求参数
)
// 发起HTTP请求并接收响应,响应体被反序列化为List<ItemDTO>
ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
// 1. 请求URL:拼接服务实例地址 + 接口路径,{ids}是参数占位符
instance.getUri() + "/items?ids={ids}",
// 2. 请求方式:GET
HttpMethod.GET,
// 3. 请求实体:null 表示无请求体(GET请求无需请求体)
null,
// 4. 响应类型:指定泛型List<ItemDTO>(解决RestTemplate泛型解析问题)
new ParameterizedTypeReference<List<ItemDTO>>() {},
// 5. URL参数:替换{ids}占位符,将itemIds集合拼接为逗号分隔的字符串
Map.of("ids", CollUtil.join(itemIds, ","))
);
ParameterizedTypeReference:解决 RestTemplate 无法直接解析List<ItemDTO>这类泛型类型的问题(直接用List.class会丢失泛型信息);CollUtil.join(itemIds, ","):Hutool 工具类方法,将itemIds集合(如 [1,2,3])拼接为字符串 "1,2,3";Map.of("ids", ...):将参数键值对传入,自动替换 URL 中的{ids}占位符;ResponseEntity:包含响应状态码、响应头、响应体的完整响应对象。
请求实体可以填 null ,在传递的返回值类型为 List<> 时不能使用使用字节码,它的泛型会被字节码擦除。Java 编译时会擦除泛型信息,运行时 List<ItemDTO>.class 不存在,只能拿到 List.class,导致 RestTemplate 无法识别要反序列化的具体泛型类型(只能解析为 List<Object>,而非 List<ItemDTO>)。
当Java编译器编译带有泛型的代码时,它会进行类型检查以确保类型安全。但是,在生成字节码时,所有的泛型信息都会被擦除。
泛型类型被替换为它们的原始类型(即不带泛型的类型)。例如,List<String>在运行时会被视为List,所有的String类型信息都会被擦除。
所以,想要解决这个问题,需要创建一个对象,这是参数化类型的引用。
new ParameterizedTypeReference<List<ItemDTO>>() {}
在对象的泛型中写入要传的类型,它可以利用反射拿到对象的泛型。最后会返回一个响应 response。
解析响应 response:
response.getbody //获取响应体
response.getStatusCode //获取响应状态码
response.getHeaders //获取响应头
将 cart 与 item 两个微服务都启动后,便可实现远程调用,解决了微服务之间无法相互调用之间方法的问题。
服务远程调用时存在的问题:
1)无法解决压力大,并发请求多的情况,同时端口写死,无法确定端口的状态。
2)服务调用者不知道服务提供者的地址,当存在多个端口时,该调用哪一个。
3)选中的服务挂了,怎么办。

3. 服务治理-注册中心
3.1 注册中心原理
该技术用于解决上文中 RestTemplate 存在的问题,用来集中管理微服务,实现服务的注册,发现,检查等功能; 服务 A 与服务 B 注册进注册中心 C,形成服务注册表(表里登记了服务 A 和服务 B 的地址等相关信息)。
注意,可以实现功能的技术有很多,他们都实现了 Spring Cloud 中 DiscoveryClient 这个顶级接口,所以底层性质是类似的,只是细节上略微不同。

心跳续约:用于实时的检查服务提供者的状态,防止调用者调取到挂掉的服务。向注册中心报告自己的健康状态,如果有服务宕机,注册中心将去掉对应服务,服务调用者再来订阅时,获得的信息将不包含宕掉的服务,当发生变更时,也会推送变更给服务调用者
负载均衡的算法有:随机、轮询、加权轮询、哈希
服务调用者:调用其他服务提供的接口,通过负载均衡从提供的多个实例中选择一个。
服务提供者:暴露服务接口,供其他服务使用。
注册中心:记录并监控微服务各实例状态,推送服务变更信息。
服务治理中的三个角色分别是什么?
- 服务提供者:暴露服务接口,供其它服务调用
- 服务消费者:调用其它服务提供的接口
- 注册中心:记录并监控微服务各实例状态,推送服务变更信息
消费者如何知道提供者的地址?
- 服务提供者会在启动时注册自己信息到注册中心,消费者可以从注册中心订阅和拉取服务信息
消费者如何得知服务状态变更?
- 服务提供者通过心跳机制向注册中心报告自己的健康状态,当心跳异常时注册中心会将异常服务剔除,并通知订阅了该服务的消费者
当提供者有多个实例时,消费者该选择哪一个?
- 消费者可以通过负载均衡算法,从多个实例中选择一个

3.2 Nacos注册中心
Nacos是目前国内企业中占比最多的注册中心组件。它是阿里巴巴的产品,目前已经加入SpringCloudAlibaba中。

3.2.1 部署
提前将资料中的表导入到 Mysql 中,里面包含了 Nacos 的一些相关信息,同时将资料中准备好的 custom.env Nacos 配置文件连同文件夹以及打包好的 nacos.tar 镜像文件移动到 虚拟机中(配置文件中的 MYSQL_SERVICE_HOST 需要更改为自己的虚拟机 ip 地址)。
准备好Nacos的配置文件【里面指定数据库的类型、数据库地址、数据库名、数据库端口、数据库用户名、数据库密码】
PREFER_HOST_MODE=hostname
MODE=standalone
SPRING_DATASOURCE_PLATFORM=mysql
MYSQL_SERVICE_HOST=192.168.150.101
MYSQL_SERVICE_DB_NAME=nacos
MYSQL_SERVICE_PORT=3306
MYSQL_SERVICE_USER=root
MYSQL_SERVICE_PASSWORD=123
MYSQL_SERVICE_DB_PARAM=characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
使用 docker load -i nacos.tar 命令加载镜像,最后再使用 docker run 命令启动 nacos(mysql 要先于 nacos 启动,因为 nacos 的一些数据储存在 mysql 数据库中,如用户名,密码等)。
docker run -d \
--name nacos \
--env-file ./nacos/custom.env \
-p 8848:8848 \
-p 9848:9848 \
-p 9849:9849 \
--restart=always \
nacos/nacos-server:v2.1.0-slim
启动 Nacos 后,使用浏览器可以访问 http://【你的虚拟机ip地址】:8848/nacos 来进入 nacos 官方的控制平台,使用数据库中的用户名和密码登录(账号密码都是 nacos),以后就可以在此页面监控和查看你已在其中注册的微服务。

接下来我们需要在 项目中 引入 nacos 的依赖,同时完成 nacos 的地址的配置。
3.3 服务注册
服务注册:服务启动的时候,都应该将信息注册到注册中心里面
1)引入Nacos的依赖
<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
2)配置Nacos地址
spring:
application:
name: item-service # 服务名称
cloud:
nacos:
server-addr: 192.168.150.101:8848 # nacos地址,地址处为自己虚拟机的 ip 地址
3)启动服务即可
可以看到这时将服务启动后 nacos 的服务管理中出现了对应的功能模块,我们还可以点击详情查看关于单个服务的具体信息,如实例数等。


这样,服务的注册就完成了,上图即为服务的注册表。
3.4 服务发现
服务调用者想要去 nacos 订阅服务提供者的过程叫做服务发现,我们需要先引入依赖, 接着再去配置地址,步骤与服务治理中的配置方法一致。
1)引入依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
2)配置Nacos地址:
spring:
cloud:
nacos:
server-addr: 192.168.150.101:8848
然后我们就可以去使用它了,先在实现类中注入 DiscoveryClient 然后就可以去订阅其它微服务,动态感知服务提供者的地址 ,解决了地址写死的问题。
3)服务发现:
//1.注入对象【SpringCloud的规范就是这个】
@Autowired
private DiscoveryClient discoveryClient;
//2.在具体的方法中根据实例的id获取服务的实例
List<ServiceInstance> instances=discoveryClient.getInstances("item-service")//nacos的实例id默认是服务的名称
//3.判断集合是否为空
if(CollUtils.isEmpyt(instances)){
return;
}
//4.不为空,根据负载均衡选一个实例
ServiceInstance instance=instances.get(RandomUtils.randomInt(instances.size()))
//5.然后调用HttpClient等客户端发送方法【instance的getUri方法可以获取ip地址和端口号】

1)根据微服务的名称获取事务的服务列表,判断其不为 null。
2)使用负载均衡算法中的随机选择一个实例使用
3)使用 getUri( )方法获取 url 拼接成一串地址。
3.5 OpenFeign 技术
上面使用的方法还是太冗杂繁琐了,仅仅是调用一个微服务中的方法,就写了超过10行代码,所以我们学习了简化这个操作的新技术——OpenFeign,它简化了繁琐的代码,是一种声明式的 Http 客户端。
其作用就是基于SpringMVC的常见注解,帮我们优雅地实现http请求的发送。
使用方法:
1)引入依赖
<!--openFeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--负载均衡器-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
2)启动类加上@EnableFeignClients注解,启动OpenFeign功能
//如果FeignClient不在SpringBootApplication的扫描范围内可以在@EnableFeignClients中指定扫描范围
//主程序默认扫描 com.hmall.application 及其子包,无法扫描到 com.hmall.api.clients
@EnableFeignClients(basePackages="com.hmall.api.clients")
3)定义一个接口,用于编写 Feign 客户端【就代替了原本一大堆的代码】
//@FeignClient注解告诉java,这就是一个Feign的客户端
//value属性就说明以后拉取的是对应id的服务
//里面默认使用了负载均衡算法
@FeignClient(value = "item-service")
public interface ItemClient {
//这个请求路径【请求的端口号和ip在负载均衡那里已经自动获取了】
@GetMapping("/items")
//请求返回的类型会根据反射自动装配到我们的返回值中
//@RequestParam是请求的参数
List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
}
- @FeignClient(“服务名”)
- @GetMapping(“完整的请求路径”)
方法与具体微服务中要调用的方法,参数与原本方法一致,不用实现。
4)在需要发送请求的地方调用接口即可
//注入对象
@AutoWired
private ItemClient itemClient;
//需要的地方调用方法
List<ItemDTO> items=itemClient.queryItemByIds(List.of(1,2,3));
3.6 OpenFeign 连接池
OpenFeign 对 Http 请求做了优雅的伪装,不过其底层发起 http 请求,依赖于其它的框架。这些框架可以自己选择,包括以下三种:
HttpURLConnection:默认实现,不支持连接池,这意味着每次 HTTP 请求都需要建立一个新的连接,并在请求完成后关闭连接。这种方式在处理大量并发请求时会导致性能瓶颈,因为连接的建立和关闭都需要消耗时间和资源。
Apache HttpClient :支持连接池,可以显著提高性能,因为连接池允许 Feign 复用已经建立的连接,而不是每次请求都重新建立连接。这样可以减少连接的建立和关闭次数,降低资源消耗,提高请求的响应速度和吞吐量。
OKHttp:支持连接池。与 Apache HttpClient 类似,OKHttp 通过连接池来复用连接,提高请求的效率。此外,OKHttp 还提供了异步请求、缓存等功能,进一步增强了其性能和灵活性。
具体源码可以参考 FeignBlockingLoadBalancerClient 类中的 delegate 成员变量。
使用步骤:
1)引入 OKHttp 依赖
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
2)在配置中开启连接池功能
feign:
okhttp:
enabled: true # 开启OKHttp功能
3)重启服务后,连接池就生效了。
3.7 OpenFeign 最佳实践
解决重复代码问题
我们刚才只是在一个服务中使用OpenFeign发送了请求。但是这个请求在很多服务中都能使用到,如果每次都写一遍,会很麻烦。我们能否把这个请求抽象成一个公共的呢?
思路一:抽取到微服务之外的公共module(再写一个模块)

创建一个模块,和其他模块同级别,这个模块只存放Feign的API ,其他接口需要调用的时候直接引入模块API
思路二:每个微服务自己抽取一个module(模块拆分)

将一个模块抽象成三个子模块,一个模块专门存放Feign的API,其他的模块需要发送Feign请求的时候直接引入对应模块的依赖
- 方案一抽取更加简单,工程结构也比较清晰,但缺点是整个项目耦合度偏高。
- 方案二抽取相对麻烦,工程结构相对更复杂,但服务之间耦合度降低。
为了避免代码的复用性过高,选择思路一进行实现,不必在每个模块中都编写 Client 接口以及各种 dto 对象,我们可以将其再次拆分,重新聚合成一个 api 模块,统一将要调用的 dto 与 Client 写入一个模块中,之后在有需求的模块中引入 api 模块,解决重复代码问题。
而因为 ItemClient 现在定义到了 com.hmall.api.client 包下,而 cart-service 的启动类定义在 com.hmall.cart 包下,扫描不到 ItemClient,所有我们要在启动类的@EnableFeignClients中加上 api 中 client 所在的路径 ( "com.hmall.api.client" ) ,或声明要使用的 Client ( clients = {ItemClient.class} ) 。
3.8 OpenFeign 日志输出(多数情况不需要开启)
OpenFeign需要输出日志需要符合两个条件:
- FeignClient所在的包日志级别为debug
- Feign的日志级别在NONE以上
OpenFeign 只会在 Client 所在包的日志级别为 DeBug 时才会输出日志,且日志级别有4级:
- NONE:不记录任何日志信息,默认。
- BASIC:仅记录请求的方法,URL 及响应状态码和执行时间。
- HEADER:在 BASIC 的基础上,额外记录了请求和响应的头信息。
- FULL:记录所有的请求和响应的明细,包括头信息、请求体、元数据。
开启步骤:
1)要自定义日志级别需要声明一个类型为 Logger.Level 的 Bean,在其中定义日志级别(在这里,我们需在 api 模块声明一个 Logger.level 的 Bean 。在其中定义日志级别):

2)在 @FeignClient 中声明( configuration = Logger.level所在配置文件名.class )(局部),在启动类上的 @EnableFeignClient(defaultConfiguration = 配置文件.class)(全局)。
局部:
//如果需要局部配置,则在对应的FeignClient中配置configuration属性
@FeignClient(value = "item-service", configuration = DefaultFeignConfig.class)
全局:
//如果需要全局配置,则在启动类的EnableFeignClients,添加defaultConfiguration属性
@EnableFeignClients(defaultConfiguration = DefaultFeignConfig.class)
调试中可以看到日志输出:

4. 网关
网关就是网络的关口,负责请求的路由、转发、身份校验。相当于一个小区的保安,所有的请求都需要发送到网关,由它进行统一的校验和转发,并通过负载均衡选择一个微服务进行转发以及通过注册中心拉取相应微服务的地址。对前端来说,只需将请求地址变为网关的地址,与单体架构无异,且里面的微服务相对而言是隐藏的,增加了安全性。


对前端来说,由于他只知道网关地址,整个微服务对他来说是隐藏的,所以在前端看来此架构与单体架构无区别。
4.1 如何使用网关

在 SpringCloud 中网关的实现包括两种:
- Spring Cloud Gateway:Spring 官方出品,基于 WebFlux 响应式编程,性能优异。(荐)
- Netflie Zuul:Netflie 出品,基于 Servlet 的阻塞式编程,需要调优才能获得与前者类似的性能,现在已停止维护。
使用步骤:
1)在 hmall 父模块下创建新的模块 hm-gateway 。
2)引入依赖
<dependencies>
<!--common-->
<dependency>
<groupId>com.heima</groupId>
<artifactId>hm-common</artifactId>
<version>1.0.0</version>
</dependency>
<!--网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--负载均衡-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3)编写启动类

4)配置路由规则
新建 ymal 配置文件,在其中配置 port(端口号)、name(微服务名)、server-addr(服务拉取 nacos 地址)、以及 routes(路由规则)。

其中,id 为微服务名称;uri 为路由目标,lb 为负载均衡协议后跟 ://服务名称;predicates 下接具体的参数 - name 路由规则名 与 - args 参数 可简写为图中 - 路由规则名=参数 ,而多个colltroller 中的请求路径可以在其下方按规则接着写,也可在其后方使用 ‘,’ 隔开接着写。
成功启动项目后,可以看到无论是 8081 端口还是 8080 端口(gateway 模块)都可以成功的查询到商品的数据,证明了网关服务成功的实现了。


4.2 路由属性
进入源码中查看

网关路由对应的Java类型是RouteDefinition,其中常见的属性有:
- id:路由的唯一标识。
- uri:路由目标地址。
- predicates:路由断言,判断请求是否符合当前路由。
- filters:路由过滤器,对请求或响应做特殊处理。
Spring 提供了12 种路由断言(12种基本的RoutepredicateFactory实现):

网关中提供了 33 种路由过滤器,每种过滤器都有其独特的功能。
配置的默认过滤器对所有路由都生效。(default-filters,与routes同级)

网关请求处理流程:


HeadlerMapping:路由映射器,基于路由断言进行匹配。
——> WebHeadler:请求处理器,找到当前请求对应路由生效的过滤器,放到过滤器链中排序,依次执行。
——> NettyRouting Filter:默认在所有路由中都生效,在过滤器链的最后执行,负责将请求转发到微服务,当返回结果后将其存入上下文中,并按来时顺序依次返回执行 post 逻辑。
网关:内部含有两个逻辑,pre 和 post,在请求路由到微服务,之前和之后执行,如果 pre 逻辑中的任何检查失败,网关将拦截请求,并直接返回错误响应给客户端,而不会将请求转发到后端的微服务,接收到微服务的响应后,网关执行 post 逻辑。
- 网关核心流程:路由匹配(HandlerMapping)→ 构建过滤器链(FilterWebHandler)→ 执行 pre 逻辑 → 转发请求(NettyRoutingFilter)→ 执行 post 逻辑 → 返回响应;
- pre 逻辑是 “前置检查”,失败则直接拦截请求,不转发到微服务;post 逻辑是 “后置处理”,仅在微服务返回响应后执行;
4.3 使用网关实现登录校验

1)如何在网关转发之前做登录校验?
在网关内自定义一个过滤器( pre逻辑 ),进行 JWT 校验。
2)网关如何将信息传递给微服务?
将得到的用户信息存放在请求头中,发送请求到微服务,由微服务获取请求头。
3)如何在微服务之间传递用户信息?
与第二个问题解决方法类似,但微服务之间由 Openfeign 发起,具体实现有所不同。
4.3.1 自定义过滤器GlobalFilter与GatewayFilter
网关过滤器有两种,分别是:
1)GatewayFilter:路由过滤器,作用于任意指定的路由,默认不生效,要配置到路由后生效。
public interface GatewayFilter extends ShortcutConfigurable {
// 过滤器名称键(配置解析时使用)
String NAME_KEY = "name";
// 过滤器值键(配置解析时使用)
String VALUE_KEY = "value";
/**
* 处理Web请求,并且(可选地)通过给定的{@link GatewayFilterChain}委托给下一个过滤器
* @param exchange 当前的服务器请求-响应上下文对象(包含请求、响应、自定义属性等)
* @param chain 提供委托给下一个过滤器的方式(过滤器链),当前过滤器执行完后,要调用过滤器链的下一个过滤器
* @return {@code Mono<Void>} 用于标识请求处理完成的状态(响应式编程)
*/
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}
自定义GatewayFilter过滤器不是直接实现GatewayFilter,而是实现AbstractGatewayFilte rFactory 工厂。

需要指定执行顺序:
/**
* 自定义GatewayFilter工厂类
* 作用:生成可配置的路由过滤器,仅对绑定的路由生效
*/
@Component
public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<PrintAnyGatewayFilterFactory.Config> {
// 构造函数:指定配置类类型,让父类解析配置
public PrintAnyGatewayFilterFactory() {
super(Config.class);
}
/**
* 工厂方法:根据配置生成具体的过滤器实例
* @param config 配置对象,包含从配置文件读取的参数a、b、c
* @return 带执行顺序的GatewayFilter
*/
@Override
public GatewayFilter apply(Config config) {
// 用OrderedGatewayFilter包装,指定执行顺序为1(数字越小越先执行)
return new OrderedGatewayFilter(new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 从配置对象中获取参数
String a = config.getA();
String b = config.getB();
String c = config.getC();
// 打印配置参数,验证配置是否生效
System.out.println("a = " + a);
System.out.println("b = " + b);
System.out.println("c = " + c);
System.out.println("print any filter running");
// 放行请求,传递给下一个过滤器
return chain.filter(exchange);
}
}, 1);
}
/**
* 配置类:接收配置文件中的参数
*/
@Data
public static class Config {
private String a;
private String b;
private String c;
}
/**
* 配置参数顺序:支持快捷配置写法(如PrintAny=1,2,3)
*/
@Override
public List<String> shortcutFieldOrder() {
return List.of("a", "b", "c");
}
}
在 application.yaml 配置文件中可以配置 default-filters 为 构造函数前缀名=参数。
default-filters:
- PrintAny=1,2,3
- 实现 AbstractGatewayFilterFactory 工厂,实现 apply 方法,返回一个过滤器,我们也可以选择实现装饰类 Ordered 来控制过滤器的执行顺序。
- 自定义一个类用于接受配置的参数。
- 使用本类的构造函数调用父类的构造函数将自定义的参数类的字节码传递给父类,让它帮我们读取配置,固定的类名称后缀 GatewatFilterFactory。
- 实现 shortcutFieldOrder 方法,其中返回收集的参数。
2)GlobalFliter:全局过滤器,global(全局)作用范围是所有路由,声明后自动生效。
public interface GlobalFilter {
/**
* 处理Web请求,并且(可选地)通过给定的{@link GatewayFilterChain}委托给下一个过滤器
* @param exchange 当前的服务器请求-响应上下文对象(包含请求、响应、自定义属性等)
* @param chain 提供委托给下一个过滤器的方式(过滤器链),当前过滤器执行完后,要调用过滤器链的下一个过滤器
* @return {@code Mono<Void>} 用于标识请求处理完成的状态(响应式编程)
*/
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}
filter 方法中第一个参数是请求上下文,包含整个过滤器连内共享的数据。第二个参数是过滤器链,当前过滤器执行完后要调用下一个过滤器,将链条串起来。返回值 Mono 可以调用你写的回调函数完成 post 逻辑,不用等待,是一种非阻塞式的编程,但现在用不到。

注:通过实现Ordered接口,重写getOrder()方法返回0,让它在过滤器链中优先执行(数字越小越先执行),适合做全局前置校验(比如鉴权)。NettyRouting Filter就是最低优先级:2147483647。
4.3.2 实现登录校验
需求:在网关中基于过滤器实现登录校验功能。
首先将必备的 config 配置文件、util 工具类、以及关于 jwt 和 排除路径 配置到 applicatio n.yaml 配置文件中。
hm:
jwt:
location: classpath:hmall.jks
alias: hmall
password: hmall123
tokenTTL: 30m
auth:
excludePaths:
- /search/**
- /users/login
- /items/**
配置类:
1)这个类是 Spring Boot 的配置属性类,用来读取配置文件中以hm.auth为前缀的配置项,比如:
hm.auth.includePaths:需要进行鉴权的路径列表hm.auth.excludePaths:不需要鉴权的路径列表
它通常会配合网关的全局过滤器使用,来实现接口的权限控制。
package com.hmall.gateway.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
@Data
@Component
@ConfigurationProperties(prefix = "hm.auth")
public class AuthProperties {
private List<String> includePaths;
private List<String> excludePaths;
}
2)这是一个 Spring Boot 配置属性类,用于读取配置文件中以hm.jwt为前缀的配置项。
- 字段解析
location:密钥文件的资源路径,通常指向一个 JKS 格式的密钥库文件。password:密钥库的访问密码。alias:密钥库中用于 JWT 签名的密钥别名。tokenTTL:JWT 令牌的默认有效期,默认值为 10 分钟。
- 典型应用:配合
JwtTool使用,从配置的密钥库中加载密钥对,用于生成和验证 JWT 令牌。
package com.hmall.gateway.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.io.Resource;
import java.time.Duration;
@Data
@ConfigurationProperties(prefix = "hm.jwt")
public class JwtProperties {
private Resource location;
private String password;
private String alias;
private Duration tokenTTL = Duration.ofMinutes(10);
}
3)这是 Spring Security 的核心配置类,负责配置密码编码器和加载 JWT 签名所需的 RSA 密钥对。
核心方法:
passwordEncoder():配置并返回BCryptPasswordEncoder,用于密码的加密与验证。keyPair(JwtProperties properties):从配置的密钥库(JKS 文件)中加载 RSA 密钥对,供JwtTool生成和验证 JWT 令牌时使用。
package com.hmall.gateway.config;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.rsa.crypto.KeyStoreKeyFactory;
import java.security.KeyPair;
@Configuration
@EnableConfigurationProperties(JwtProperties.class)
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public KeyPair keyPair(JwtProperties properties) {
// 获取密钥工厂
KeyStoreKeyFactory keyStoreKeyFactory =
new KeyStoreKeyFactory(
properties.getLocation(),
properties.getPassword().toCharArray());
// 读取密钥对
return keyStoreKeyFactory.getKeyPair(
properties.getAlias(),
properties.getPassword().toCharArray());
}
}
工具类:
这是一个完整的 JWT 工具类,包含令牌生成、解析、验证的全流程。
核心方法
createToken:生成包含用户 ID 和过期时间的 JWT 令牌。parseToken:解析并验证令牌,返回用户 ID,过程中会校验令牌的有效性、过期时间和数据格式。
异常处理:通过自定义的UnauthorizedException统一处理鉴权失败的场景,便于上游捕获和返回标准的 401 响应。
package com.hmall.gateway.util;
import cn.hutool.core.exceptions.ValidateException;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTValidator;
import cn.hutool.jwt.signers.JWTSigner;
import cn.hutool.jwt.signers.JWTSignerUtil;
import com.hmall.common.exception.UnauthorizedException;
import org.springframework.stereotype.Component;
import java.security.KeyPair;
import java.time.Duration;
import java.util.Date;
@Component
public class JwtTool {
private final JWTSigner jwtSigner;
public JwtTool(KeyPair keyPair) {
this.jwtSigner = JWTSignerUtil.createSigner(keyPair);
}
public String createToken(Long userId, Duration ttl) {
return JWT.create()
.setPayload("user", userId)
.setExpiresAt(new Date(System.currentTimeMillis() + ttl.toMillis()))
.setSigner(jwtSigner)
.sign();
}
public Long parseToken(String token) {
if (token == null) {
throw new UnauthorizedException("未登录");
}
JWT jwt;
try {
jwt = JWT.of(token).setSigner(jwtSigner);
} catch (Exception e) {
throw new UnauthorizedException("无效的token", e);
}
if (!jwt.verify()) {
throw new UnauthorizedException("无效的token");
}
try {
JWTValidator.of(jwt).validateDate();
} catch (ValidateException e) {
throw new UnauthorizedException("token已经过期");
}
Object userPayload = jwt.getPayload("user");
if (userPayload == null) {
throw new UnauthorizedException("无效的token");
}
try {
return Long.valueOf(userPayload.toString());
} catch (RuntimeException e) {
throw new UnauthorizedException("无效的token");
}
}
}
创建一个新的拦截器,使用@Component标记该类为 Bean,同时实现GlobleFilter 和 Ordered。
import com.hmall.common.exception.UnauthorizedException;
import com.hmall.gateway.config.AuthProperties;
import com.hmall.gateway.util.JwtTool;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.List;
@Component
@RequiredArgsConstructor
public class AuthGlobalFilter implements GlobalFilter, Ordered {
private final AuthProperties authProperties;
private final JwtTool jwtTool;
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. 获取request
ServerHttpRequest request = exchange.getRequest();
// 2. 判断是否需要做登录拦截
if (isExclude(request.getPath().toString())) {
// 放行
return chain.filter(exchange);
}
// 3. 获取token
String token = null;
List<String> headers = request.getHeaders().get("authorization");
if (headers != null && !headers.isEmpty()) {
token = headers.get(0);
}
// 4. 校验并解析token
Long userId = null;
try {
userId = jwtTool.parseToken(token);
} catch (UnauthorizedException e) {
// 拦截,设置响应状态码为401
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
// TODO 5. 传递用户信息
System.out.println("userId = " + userId);
// 6. 放行
return chain.filter(exchange);
}
private boolean isExclude(String path) {
for (String pathPattern : authProperties.getExcludePaths()) {
if (antPathMatcher.match(pathPattern, path)) {
return true;
}
}
return false;
}
@Override
public int getOrder() {
return 0;
}
}
1)获取 Request
我们可以直接从上下文中获取到请求 request 。
// 1. 获取request
ServerHttpRequest request = exchange.getRequest();
2)判断是否要做登录拦截
通过我们自己定义的 isExclude 方法来判断请求路径是否是要排除的路径,使用 request.getPath () 获取请求,注入 AuthProperties(存放了放行的请求路径) 来获取放行的请求路径,在判断方法中,遍历获取放行路径,由于两种路径的格式不同,我们需要使用 Spring 提供的 AntPathMatcher(匹配器)来进行比对,使用 AntPathMatcher.match(参数1,参数2) 来比较。

3)获取 token
通过获取上下文 exchange 中请求头里规定好的 authorization 参数来获取请求头。
4)校验并验证 token
注入 util 包中的 JwtTool 后使用其中提供好的 parseToken () 方法来校验,之后使用 try-catch 捕获异常,在其中通过上下文 exchange 获取响应 response 并将状态码设置为 401 (HttpStatus.UNAUTHORIZED)表示用户未登录,最后返回 response.setComplete() 终止,后续拦截器不再进行,请求也不再转发。

5)传递用户信息
使用上下文 exchange 中提供好的 mutate () 方法就可以对上下文内容进行修改,写入的请求头要规定好名称,方便读取。

6)放行
最后使用 return chain.filter(webExchange) 将修改好的上下文传入放行给下一个过滤器。
4.3.3 网关传递用户到微服务及OpenFeign传递用户信息

由于最后是将用户信息存放在请求头中传递,所以我们需要用到学过的拦截器来将请求拦截获取用户信息,但每一个微服务都可能需要获取用户的信息所以,我们不能在每一个微服务里都写一个拦截器,所以我们需要将拦截器写在 common 模块中,避免重复编写。
因为在过滤器中已经写好了登录校验的逻辑,所以在拦截器中不需要拦截任何请求,全部放行,只需获取其中的用户信息即可。但在微服务的远程调用中,例如在购物车中提交订单后删除购物车中商品时还需要用户信息,微服务之间是用 OpenFeign 远程调用的,所以需要在它们之间定义一个拦截器将由网关转发请求而被拦截保存的用户信息再次写入到远程调用的请求头中,从而成功传递用户信息。


1)在 hm-common 模块中定义一个拦截器,使之能对所有微服务生效。
①实现 HandlerInterceptor 并重写其中的 preHandle(请求处理前被调用) 和 afterCompletion(整个请求结束后被调用)。
②preHandle 中获取到用户信息存入ThreadLocal 中后放行。

③afterCompletion 中在所有业务完成后清理用户信息。

2)想要拦截器生效,还需在配置中添加拦截器,在 config 包下定义 MvcConfig 配置类实现 WebMvcConfigurer 接口,在 addInterceptors 方法中添加拦截器,不配置默认拦截所有请求。

配置类想要生效需要被扫描到,但包不同无法扫描到。而想要被扫描到,需要在 resources 下的 META-INF 包下定义一个文件记录。

而在网关中引入了 common 的依赖 ,但我们不需要 MvcConfig 配置,且两者之间底层不同,网关底层是非阻塞式的、响应式编程,基于WebFlux,而非 SpringMvc,所以我们需要使用 @ConditionalOnClass 条件注解,在其中加入 SpringMvc 的核心 Api 作为条件进行排除,使之不在网关中生效。

3)OpenFeign 传递用户信息
使用 OpenFeign 中提供好的拦截器接口,之后所有由 OpenFeign 发起的请求都会先调用拦截器处理请求。

我们需要将其定义在所有微服务都引到的 api 模块中,又因为其功能较单一,我们可以将其写为匿名内部类,但因为我们将用户信息存入请求头中需要先获取,所以要在 pom 文件中引入 common 模块。

5.配置管理(扩展)
在微服务、网关等模块里,存在大量重复的配置,维护的成本高,且每一次配置变更都需重启微服务,网关的路由配置也是写死的,更改起来繁琐,因而引出了我们要学习的新技术——配置管理。

将配置统一分功能写入到配置管理中,在由微服务拉取配置,在配置变更时实时推送变更到微服务中。说是新技术,实则是我们此前学习过的 Nacos 中的配置管理组件,我们需要学习的只有功能的使用方法——共享配置、配置热更新、动态路由。

5.1 配置共享
5.1.1 配置到 Nocas 中
将 pom 配置文件中公共的配置写入到 Nacos 的配置管理中,例如 jdbc、MybatisPlus、日志、swgger、OpenFeign 等配置,配置后可点击详情查看。

我们使用 yaml 格式配置,在其中可以使用 ${ } 去动态的读取配置中的变量。像端口配置这种默认为 3306 的也可在其后加上 :3306 定义一个默认值,如果没有在项目中配置,默认端口就为 3306 。 同理别的配置也可以这样配置成动态的去读取,避免写死。

5.1.2 拉取共享配置
基于NacosConfig 拉取共享配置代替为服务的本地配置,但想要在服务启动时拉取 Nacos 的配置必须要知道 Nacos 的地址,而地址又配置在了 SpringBoot 的配置文件中,所以我们要用bootstrap.yaml 文件在其中配置相关信息,项目启动首先就会去读取其中的配置,获取到 Nacos 的地址,最后将 Nacos 的配置与 SpringBoot 中的配置合并。

1)引入依赖
<!--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 文件
将 yaml 文件的配置复制到其中,删掉不需要的,并加上 config 配置 Nacos 配置。

file-extension 表示配置文件的后缀名;在 shared-configs 下配置共享配置。
- date-id:配置管理中配置的名字

配置好后,修改原有 application.yaml 配置文件中的内容,删除与 Nacos 中相同内容的配置,并在其中写好要读取的变量,例如数据库名称,swagger 标题等。

可以看到,启动后的日志中正在读取 Nacos 中的配置文件。

5.2 配置热更新
配置热更新后,当我们修改项目中的一些配置时,我们无需重启服务就可以使配置生效。
前提条件:
1)Nacos 要有一个与微服务名称有关的配置文件

例: 微服务名称-项目profile(例:dev,可选填). 文件名后缀
但我们会发现,这些已经在之前的 bootstrap 文件中写好,它会自动读取。

2)微服务中要以特定的方式读取需要热更新的配置属性
1.Properties 读取(推荐)
使用 @ConfigurationProperties 批量绑定配置,通过 prefix = "hm.cart" 自动将配置文件中以 hm.cart 为前缀的属性映射到类的字段。
@Data
@ConfigurationProperties(prefix = "hm.cart")
public class CartProperties {
private int maxItems;
}
2. @Value 注解读取
使用 @Value 单个绑定配置,结合 @RefreshScope 实现配置动态刷新,当配置中心的 hm.cart.maxItems 变化时,该字段值会自动更新。
@Data
@RefreshScope
public class CartProperties {
@Value("${hm.cart.maxItems}")
private int maxItems;
}
案例:将购物车的上限更改为读取配置文件的属性,交给 Nacos 管理,实现热更新。
1.编写 Properties 类,在其中定义一个变量 maxItems。
@ConfigurationProperties(prefix = "hm.cart") 作用是将其与配置文件中前缀为 hm.cart 的属性绑定在一起,加上 @Component 使其生效。

2.在业务层注入 CartProperties 将业务中的固定的数 10 修改为从中读取数据。
3.在 Nacos 中配置
起名遵守前提条件一:微服务名称-项目profile(例:dev,可选填). 文件名后缀
配置内容遵循 @ConfigurationProperties 注解中的描述

配置完成热部署后我们将 maxItems 设置为 2 测试


5.3 动态路由
我们知道,路由是写死在网关的配置文件中,加载后保存在一个路由表的缓存中,以后会直接读取缓存,提高处理速度,但这样我们修改配置文件中路由后想要生效就只能重启网关,想要解决这个问题,就需要我们利用 Nacos 实现动态路由。
要实现动态路由首先要将路由配置保存到 Nacos,当 Nacos 中的路由配置变更时,推送最新配置到网关,实时更新网关中的路由信息。
我们需要完成两件事情:
① 监听 Nacos 配置变更的消息
② 当配置变更时,将最新的路由信息更新到网关路由表
监听 Nacos 配置变更可以参考官方文档:https://nacos.io/zh-cn/docs/sdk.html
6.3.1 将路由配置保存到 Nacos 中,可以在变更时推送给网关。
我们使用 json 格式来配置,是为了更方便的解析配置信息,对 json 格式更加熟悉,可以使用工具类进行解析。

5.3.2 实现更新网关中的路由配置信息
1) 监听 Nacos 配置解析
需要先读取一次配置,在监听配置的变化,而 alibaba 包中的自动装配里提供了一个已经装配好的 Bean,NacosConfigManager ,只需将其注入,调用其方法就可获取 ConfigService,省去了官方文档中第一步的操作。
而 CongfigService 提供了getConfigAndSignListener 方法可以先拉取配置再注册监听器将步骤二中的两次操作合二为一,最终简化代码。

2) 引入依赖并配置
<!--统一配置管理-->
<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 配置文件配置相关信息

以及将 application.yaml 文件中删除路由信息及无关配置。

最后再创建监听器类,自定义一个初始化路由监听器的方法,在其上加上 @PostConstruct 注解使其在 Bean 初始化之后执行,注入NacosConfigManager 调用其方法先拉取配置,再添加监听器,参数 dataId、group 与 Nacos 配置管理中的一一对应,将其定义为变量传入。新建的监听器中,getExecutor 用来定义一个线程池,但我们的逻辑简单不需要用到。receiveConfigInfo 方法表示当配置变更时要干什么,所以我们需要更新到路由表。在最后我们要将第一次读取到的配置更新到路由表中。


而更新路由表需要用到新的 api 名为 RouteDefinitionWrite ,将其注入到类中,调用其中 save 和 delete 方法可以更新路由表的内容。我们先试用 JSONUtil 工具包中的方法将传来的 json格式信息转为 RouteDefinition 类型,然后根据 Id 遍历删除旧的路由表,开始时 Id 为 null ,在第一次读取后利用定义的 Set 集合容器记录下来,便于下一次更新时删除。之后遍历更新 save 路由信息,需传入SpringBoot 提供的一个响应式编程的容器,可以用 Mono 容器的 just 方法将数据装入,之后跟 subscribe 方法表示订阅容器中的消息,只在其有消息后才去处理。

将项目启动后,添加 Nacos 网关路由的配置,无需重启,即可读取到配置信息,成功访问到网址内容,并在控制台输出监听到的路由配置信息。


更多推荐


所有评论(0)