当前位置: 首页 > news >正文

微服务-30.配置管理-动态路由

一.需求

目前我们的路由不是动态的,而是写死在网关的配置文件中的,只要网关启动就回去加载其中的路由信息,然后将其保存在路由表中。这样以后再去处理请求判断路由时直接都缓存就行。

但这会有问题。因为如果我们的服务中路由信息需要变更,只改配置文件是不行的,因为已经缓存起来了。所以我们还要重启网关服务。但网关服务是所有服务的入口,是不能随便重启的,因此我们就需要在不重启网关服务的基础上进行路由信息的修改。且能生效。

这就是我们要实现的动态路由,首先我们分析一下如何在nacos中实现动态路由。

二.动态路由

首先要实现动态配置,就要依赖nacos的配置热更新的能力。首先我们要将路由配置交给nacos管理。利用其配置热更新来实现动态路由,所谓配置热更新就是当配置变更时就会推送该配置到微服务/网关。这时就会拿到该信息。接着只需要将配置信息实时更新到路由表中即可。

但是要注意,nacos只负责保存配置,并在配置变更时推送给你,但是推送给你的后续操作,即接收和更新路由表,需要自己实现。

三.监听Nacos配置变更信息

在Nacos官网中给出了手动监听Nacos配置变更的SDK:

Java SDK

我们在配置管理中找到getConfig方法,其中有三个参数:dataId,group,超时时间。

该api就是从nacos当中读取配置的。

dataId和group就是来确定要读取哪个配置文件的,在nacos当中每一个配置文件都有Data ID和Group,有这个就能唯一确定一个配置文件,从而加载到其中的信息。

该方法就是读取配置文件,如果没有超时就返回配置文件中的内容。然后你自己去做解析即可。

但这不是我们想要的,我们想要的是要监听配置变更,即监听配置什么时候变,变了告诉我,我去读取改变后的配置文件并进行更新。

这就要用到监听配置:addListener。添加一个监听器

这里需要三个参数,前两个和上面一样,确定监听哪一个配置文件,一会儿我们可以把网关的配置文件放入nacos并指定。第三个参数为监听器,要声明一个回调函数,将来nacos配置变更时就会调用这个监听器内部的回调函数,然后将最新配置推送给你。这样就能什么时候配置变更,还能拿到变更后的配置,然后就可以去更新网关的路由表了。

因此调用这个方法就行。

下面我们来看示例代码:

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);
System.out.println(content);
configService.addListener(dataId, group, new Listener() {@Overridepublic void receiveConfigInfo(String configInfo) {System.out.println("recieve1:" + configInfo);}@Overridepublic Executor getExecutor() {return null;}
});// 测试让主线程不退出,因为订阅配置是守护线程,主线程退出守护线程就会退出。 正式代码中无需下面代码
while (true) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}
}

第一行定义了nacos地址,第二三行定义了要监听的配置文件的dataid和group。然后定义properties并将serverAddr(nacos地址)存入。接着将properties传递给方法,这行代码的目的是利用指定的nacos地址和nacos建立连接,返回一个configService对象。拿到这个对象就可以利用这个对象取调用getConfig了,也可以调用调用addListener了。

那为什么要先要获取一次配置(getConfig方法),再去添加监听器(addListener方法)。那是因为监听配置的目的就是当网管的配置变更时,我们拿到最新的路由信息更新到路由表。如果我们不去做配置的加载,而是只是拿到监听器。但从今以后路由信息再也不变量,那这个监听器是不是就没有效果了。而且项目启动时也没有去加载过配置,等于这个路由信息项目启动时就没有。所以项目启动第一件事就是读一次配置,然后写到路由表里。然后添加监听器。以后配置在变更了,使用监听器处理即可。

因此要做两件事:

1.读取数据

2.添加监听器

监听器的生成很简单,直接new一个Listener()接口就行,然后实现其中的两个方法即可。getExecutor()方法是返回一个线程池。

真正处理监听到配置变更的是上面的receiveConfigInfo方法,这个方法接受一个String类型的configInfo。当指定的dataId配置发生变更时,nacos就会将变更后的配置推送过来,然后调用receiveConfigInfo方法,然后把最新的配置(configInfo)传递给你。你可以在该方法中可以对configInfo进行操作。

但是我们自己编写代码不用这么麻烦,我们的项目只要一启动就自动和nacos连接上了,因此不用和nacos建立连接的代码。我们可以直接拿到configService对象。

我们只要拿到NacosConfigManager对象调用getConfigService()方法即可拿到configService对象。configService对象可以调用getConfigAndSignListener方法来既获取配置又添加监听器。而NacosConfigManager又被自动装配了。所以咱们可以直接注入。

最终我们的代码就是这个样子:

四.实现动态路由

首先, 我们在网关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>

然后在网关gatewayresources目录创建bootstrap.yaml文件,内容如下:

spring:application:name: gatewaycloud:nacos:server-addr: 192.168.150.101config:file-extension: yamlshared-configs:- dataId: shared-log.yaml # 共享日志配置

接着,修改gatewayresources目录下的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";// 保存更新过的路由idprivate final Set<String> routeIds = new HashSet<>();@PostConstructpublic void initRouteConfigListener() throws NacosException {// 1.注册监听器并首次拉取配置String configInfo = nacosConfigManager.getConfigService().getConfigAndSignListener(dataId, group, 5000, new Listener() {@Overridepublic Executor getExecutor() {return null;}@Overridepublic 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());});}
}

initRouteConfigListener()方法我们希望他在DynamicRouteLoader类的Bean初始化后执行,我们要在上面加上@PostConstruct注解,该注解的意思就是在DynamicRouteLoader类的Bean初始化后执行。

要拿到getConfigService,我们注入NacosConfigManager对象即可,因此我们可以使用@RequiredArgsConstructor进行注入。

String configInfo = nacosConfigManager.getConfigService().getConfigAndSignListener(dataId, group, 5000, new Listener()

拉去配置并设置监听器。

    // 路由配置文件的id和分组private final String dataId = "gateway-routes.json";private final String group = "DEFAULT_GROUP";

要将dataId和group都确定下来。为什么dataId后缀名使用json,一会儿再解释。

得到的configInfo即为拉取到的配置信息。

五.更新路由表

当监听到配置变更,需要去更新路由表。项目启动时第一次拉取的配置也要添加到路由表。两者的逻辑是一样的。

我们先定义一个函数updateConfigInfo()来更新。

我们使用RouteDefinitionWriter接口来更新路由表,其中有两个方法:

1.save:更新路由到路由表,如果路由id重复,则会覆盖旧的路由。接受的对象为RouteDefinition,其实RouteDefinition就是网关路由的定义信息。更新路由需要一个RouteDefinition对象,这个对象长什么样子呢?

这就是RouteDefinition里面的信息。其中的id等就是RouteDefinition对象的属性。但是RouteDefinition对象我们从哪获取呢?

由上可知配置信息就是RouteDefinition对象的属性,而这些配置信息都在我们构建好的configInfo对象中,因此我们只要将configInfo这个字符串转换成RouteDefinition对象即可。

那字符串如何转换呢?那就取决于字符串中的内容。这个字符串内容就是配置文件的内容。也就是说它会将配置文件的内容以字符串形式返回。假设配置文件是yaml文件的样式,那么我们就需要解析yaml文件。但是我们不会。因此我们再往nacos里面保存路由信息时就不要用yaml格式,而要使用json格式,这样当我们从nacos中读取配置文件时也是json格式的,json格式解析起来很方便。这也是为什么我们要将dataID设置成json的原因。

json的样式配置如下图左边:

这只是一个路由,将来应该有很多路由,是一个路由数组。即一个json数组,里面有很多json对象。因此我们拿到的configInfo就是一个json格式的字符串,我们转换成RouteDefinition对象即可。

2.delete:根据路由id删除某个路由,只要将路由传递给我,我帮你进行删除。

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();// 3.更新路由routeDefinitions.forEach(routeDefinition -> {// 3.1.更新路由writer.save(Mono.just(routeDefinition)).subscribe();// 3.2.记录路由id,方便将来删除routeIds.add(routeDefinition.getId());});}

我们在更新路由时,首先要将nacos中的新路由json文件变成RouteDefinition对象的集合。

我们不光是有更新的可能,我们还有可能删除路由,但是删除路由的话,也要保证新的配置文件中的路由也被删除。我们不能比较新旧配置有哪些不同,然后进行针对性删除。因此我们要在每次更新前将所有旧路由全部删除,然后进行全部更新。

但是删除时我们并不知道删除的路由id,那么怎样将所有的路由配置id获取到呢?我们就要在第一次配置文件启动时(从0到1,第一次更新),就获取所有配置路由的id。我们使用routeDefinition.getId()来获取路由id,然后将其保存在一个Set集合中。以后我们每次更新路由配置文件时,就先使用RouteDefinitionWriter的delete方法一次将原来的配置路由删除。然后将Set集合清空。接着再遍历新的配置文件,使用RouteDefinitionWriter的save方法将路由更新。并将新的路由的id保存到Set集合中。

注意:save方法和delete方法的第一个参数都要求是Mono类型,但是我们是String类型(delete方法)和RouteDefinition类型(save方法),因此使用Mono的just方法转换一下,并且最后要使用.subscribe()方法订阅才可以。

六.测试

先把当前网关配置文件和nacos中网关配置文件全部删掉,启动服务:

访问不到。

接着将新的路由配置配置到nacos中。并不重启网关。我们直接在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"}
]

无需重启网关,稍等几秒钟后(动态路由会比较慢),再次访问刚才的地址:

成功!!!

http://www.xdnf.cn/news/1378063.html

相关文章:

  • 3 无重复字符的最长子串
  • 第二阶段Winfrom-8:特性和反射,加密和解密,单例模式
  • Gopher URL协议与SSRF二三事
  • 入门概念|Thymeleaf与Vue
  • 路由基础(二):路由表和FIB表
  • Day7--HOT100--54. 螺旋矩阵,48. 旋转图像,240. 搜索二维矩阵 II
  • 【JAVA实现websocket】
  • Java设计模式之《外观模式》
  • 大模型安全概述、LlamaFirewall
  • 深度学习---卷积神经网络CNN
  • Git-远程操作
  • AI-Agent 深度科普:从概念到架构、应用与未来趋势
  • JVM之【Java对象在内存中的结构】
  • Linux--->网络编程(TCP并发服务器构建:[ 多进程、多线程、select ])
  • Linux 系统调优与CPU-IO-网络内核参数调优
  • MySQL InnoDB vs MyISAM
  • 深度学习——卷积神经网络CNN(原理:基本结构流程、卷积层、池化层、全连接层等)
  • LeetCode - 反转链表 / K 个一组翻转链表
  • day2_softmax回归的实现 李沐动手学深度学习pytorch记录
  • 神经网络学习笔记12——高效卷积神经网络架构MobileNet
  • PLC_博图系列☞基本指令”S_ODT:分配接通延时定时器参数并启动“
  • leecode-三数之和
  • 如何防御安全标识符 (SID) 历史记录注入
  • 【Linux实时内核机制】ww_rt_mutex 的contending_lock异常问题
  • wireshark解析FLV插件分享
  • Unity Shader unity文档学习笔记(二十一):几种草体的实现方式(透明度剔除,GPU Instaning, 曲面细分+几何着色器实现)
  • HTML5超详细学习内容
  • GPIO推挽和开漏的名称由来和本质含义
  • FactoryBean接口作用
  • 使用Stone 3D快速制作第一人称视角在线小游戏