SpringCloud Gateway 网关路由全自动实现方案
动态网关路由
实现动态路由需要将路由配置保存到Nacos,然后在网关监听Nacos中的路由配置,并实现配置热更新,然而网关路由并不是自定义业务配置属性,本身不具备热更新功能!
详情可以参考
org.springframework.cloud.gateway.route
包下的CompositeRouteDefinitionLocator
类
如果希望 Nacos 推送配置变更,可以使用 Nacos 动态监听配置接口来实现。
这个详情也可以参考 Nacos JDK文档 监听配置部分可以帮助到您
通过Nacos的JDK文档,了解到Nacos支持 获取配置、发布配置、监听配置,那么我们可以根据以上JDK接口设计出以下的一种全自动方案。
网关启动后监听Nacos注册中心上的路由配置,而业务启动后,从Nacos注册中心拉取已有的路由配置信息,首先判断自身的路由是否存在,若存在则删除后将自身最新的路由信息添加上去,最后路由配置信息发布到Nacos上,网关监听到变化后将重新写入路由信息到内存当中。
自动发布路由实现
这种很多服务都需要用到的配置我们可以将其抽取成一个通用的Nacos自动路由注册发布配置类,注册成容器交给Spring管理
所需依赖
<dependencies>
<!-- SpringCloud Alibaba Nacos 服务注册发现依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- SpringCloud Alibaba Nacos Config 配置管理起步依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
</dependencies>
属性类
这里只是做了网关路由常见的断言和过滤器的适配支持,如果有别的特殊要求还得执行进行一个添加配置
package com.if010.common.nacos.properties;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
/**
* Nacos自动发布路由配置信息
* @author Kim同学
*/
@Data
@ConfigurationProperties(prefix = "autopublishroute")
public class NacosAutoPublishProperties {
/**
* 是否推送自身的路由信息
*/
public boolean enabled;
/**
* Nacos 网关路由配置命名空间
*/
public String dataId;
/**
* Nacos 网关路由配置分组
*/
public String group;
/**
* Nacos 网关路由配置超时时间
*/
public long timeoutMs = 60000;
/**
* 路由配置信息
*/
public RouteInfo routeinfo;
@Data
@ConfigurationProperties(prefix = "autopublishroute.routeinfo")
public class RouteInfo {
/**
* 路由ID
*/
public String id;
/**
* 路由URI
*/
public String uri;
/**
* 路由断言predicates信息
*/
public ArrayList<HashMap<String, String>> predicates;
/**
* 路由过滤器filters信息
*/
public ArrayList<HashMap<String, String>> filters;
}
}
实体类
这里的实体类主要用于从Nacos注册中心拉取回来的数据进行一个Json格式转对象,方便代码的书写和可读性的提高,当然也可以根据业务或者设计、习惯等进行一个调整
package com.if010.common.nacos.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
/**
* Nacos 网关路由信息实体类
* @author Kim同学
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class NacosRoute {
/**
* 路由ID
*/
public String id;
/**
* 路由URI
*/
public String uri;
/**
* 路由断言predicates信息
*/
public List<RouteAssert> predicates;
/**
* 路由过滤器filters信息
*/
public List<RouteAssert> filters;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RouteAssert {
/**
* 断言名称
*/
public String name;
/**
* 断言参数
*/
public Map<String,String> args;
}
}
配置类
package com.if010.common.nacos.config;
import com.alibaba.cloud.nacos.NacosConfigManager;
import com.alibaba.fastjson.JSON;
import com.alibaba.nacos.api.exception.NacosException;
import com.if010.common.nacos.entity.NacosRoute;
import com.if010.common.nacos.properties.NacosAutoPublishProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.*;
/**
* Nacos自动发布路由配置类
* 判断是否注册该类到Bean容器当中,当配置文件中autopublishroute.enabled = true时生效, 否则不生效
* @Author: Kim同学
*/
@Slf4j
@Component
@RequiredArgsConstructor
@EnableConfigurationProperties({NacosAutoPublishProperties.class, NacosAutoPublishProperties.RouteInfo.class})
@ConditionalOnProperty(name = "autopublishroute.enabled", havingValue = "true")
public class NacosAutoPublishConfig {
// 注入 NacosConfigManager
private final NacosConfigManager nacosConfigManager;
// 注入 NacosConfigProperties
private final NacosAutoPublishProperties nacosAutoPublishProperties;
// 注入 AppInfoRoute
private final NacosAutoPublishProperties.RouteInfo routeInfo;
/**
* 在Bean创建,且NacosConfigManager注入成功后,发布路由配置
*/
@PostConstruct
public void initPublish() throws NacosException {
// 1、获取网关路由配置信息
String routeConfig = nacosConfigManager.getConfigService().getConfig(
// Nacos 网关路由配置命名空间
nacosAutoPublishProperties.getDataId(),
// Nacos 网关路由配置分组
nacosAutoPublishProperties.getGroup(),
// Nacos 网关路由配置超时时间
nacosAutoPublishProperties.getTimeoutMs()
);
// 2、将Nacos中获取到的网关路由配置信息转换为数组对象
List<NacosRoute> nacosRoutes = JSON.parseArray(routeConfig, NacosRoute.class);
log.info("【Nacos自动发布路由配置】获取网关路由配置信息: {}", nacosRoutes);
// 2-1、获取路由断言predicates信息
ArrayList<HashMap<String, String>> predicates = routeInfo.getPredicates();
log.info("【Nacos自动发布路由配置】获取配置文件中的断言 predicates 信息: {}", predicates);
// 2-2、获取路由断言filters信息
ArrayList<HashMap<String, String>> filters = routeInfo.getFilters();
log.info("【Nacos自动发布路由配置】获取配置文件中的断言 filters 信息: {}", filters);
// 3、组装路由信息
NacosRoute nacosRoute = new NacosRoute(
// 路由ID
routeInfo.getId(),
// 路由URI
routeInfo.getUri(),
// 路由断言predicates信息
argsAssemble(predicates),
// 路由过滤filters信息
argsAssemble(filters)
);
// 4、检查Nacos中是否有该路由,并删除
for (int i = 0; i < nacosRoutes.size(); i++) {
if (nacosRoutes.get(i).getId().equals(nacosRoute.getId())) {
nacosRoutes.remove(i);
}
}
// 5、网关路由配置信息数组对象中
nacosRoutes.add(nacosRoute);
// 6、将网关路由配置信息数组发布到Nacos中
nacosConfigManager.getConfigService().publishConfig(
// Nacos 网关路由配置命名空间
nacosAutoPublishProperties.getDataId(),
// Nacos 网关路由配置分组
nacosAutoPublishProperties.getGroup(),
// 自身路由配置信息
JSON.toJSONString(nacosRoutes),
// Nacos 网关路由配置格式,根据动态路由获取规则定义
"json"
);
log.info("【Nacos自动发布路由配置】发布网关路由配置信息: {}", nacosRoutes);
}
/**
* 组装路由配置信息的方法
*/
private ArrayList<NacosRoute.RouteAssert> argsAssemble(ArrayList<HashMap<String, String>> args) {
// 判断是否存在配置
if (args == null || args.size() == 0) {
return null;
}
// 定义args集合
ArrayList<NacosRoute.RouteAssert> argsList = new ArrayList<>();
args.forEach(arg -> {
NacosRoute nacosRoute = new NacosRoute();
// 键名和值
String keyName = arg.keySet().iterator().next();
String value = arg.get(keyName);
// 将配置值以逗号分隔转数组,["/system-service/**","/api/sys/**"]
List<String> genkeyList = Arrays.asList(value.split(","));
// 循环组装断言信息,{"_genkey_0": "args.value"}
Map<String, String> argsMap = new HashMap<>();
for (int i = 0; i < genkeyList.size(); i++) {
argsMap.put("_genkey_"+i, genkeyList.get(i));
}
// 将断言信息转换为对象放入集合当中,[{"name": "keyName", "args": {"_genkey_0": "args.value", "_genkey_1": "args.value"}}]
argsList.add(nacosRoute.new RouteAssert(keyName, argsMap));
});
return argsList;
}
}
这里需要注意的是,因为是starter模块,可能他人的项目目录和starter模块的目录不一致,导致加载不到NacosAutoPublishConfig类,我们需要使用
spring.factories
把NacosAutoPublishConfig类装载到Spring容器,在resources/META-INF/spring添加org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件
测试
到此,我们已经将Nacos通用配置管理抽取完成,接下来我们仅需要在业务服务模块中引入我们抽取好的依赖即可,当然引入依赖后我们还需要进行一下application.yml
文件的属性定义配置
# Spring
spring:
application:
# 应用名称
name: if010-test
profiles:
# 环境配置
active: dev
cloud:
nacos:
discovery:
# 服务注册地址
server-addr: 127.0.0.1:8848
config:
# 配置中心地址
server-addr: 127.0.0.1:8848
# 配置文件格式
file-extension: yml
# 共享配置
shared-configs:
- application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
- application-${spring.profiles.active}-nacos.${spring.cloud.nacos.config.file-extension}
- ${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
# 超时时间
timeout: 3000
# 自动推送自己的路由注册信息
autopublishroute:
# 是否推送自身的路由信息
enabled: true
# Nacos 网关路由配置命名空间
dataId: if010-gateway-routes.josn
# Nacos 网关路由配置分组
group: DEFAULT_GROUP
# Nacos 网关路由配置超时时间
timeoutMs: 60000
routeinfo:
id: test-service
uri: lb://if010-test
predicates:
- Path: /test-service/**
filters:
- StripPrefix: 1
启动的过程中我们可以过滤一下日志看看拉取回来的配置和重新发布的配置信息
15:10:15.735 [main] INFO c.i.c.n.c.NacosAutoPublishConfig - [initPublish,57] - 【Nacos自动发布路由配置】获取网关路由配置信息: [NacosRoute(id=system-service, uri=lb://if010-system, predicates=[NacosRoute.RouteAssert(name=Path, args={_genkey_0=/system-service/**})], filters=[NacosRoute.RouteAssert(name=StripPrefix, args={_genkey_0=1})]), NacosRoute(id=test-service, uri=lb://if010-test, predicates=[NacosRoute.RouteAssert(name=Path, args={_genkey_0=/test-service/**, _genkey_1=/test/**})], filters=[NacosRoute.RouteAssert(name=StripPrefix, args={_genkey_0=1})])]
15:10:15.738 [main] INFO c.i.c.n.c.NacosAutoPublishConfig - [initPublish,61] - 【Nacos自动发布路由配置】获取配置文件中的断言 predicates 信息: [{Path=/test-service/**}]
15:10:15.739 [main] INFO c.i.c.n.c.NacosAutoPublishConfig - [initPublish,65] - 【Nacos自动发布路由配置】获取配置文件中的断言 filters 信息: null
15:10:15.779 [main] INFO c.i.c.n.c.NacosAutoPublishConfig - [initPublish,100] - 【Nacos自动发布路由配置】发布网关路由配置信息: [NacosRoute(id=system-service, uri=lb://if010-system, predicates=[NacosRoute.RouteAssert(name=Path, args={_genkey_0=/system-service/**})], filters=[NacosRoute.RouteAssert(name=StripPrefix, args={_genkey_0=1})]), NacosRoute(id=test-service, uri=lb://if010-test, predicates=[NacosRoute.RouteAssert(name=Path, args={_genkey_0=/test-service/**})], filters=null)]
最后从Nacos注册中心上查看配置是否成功发布
网关动态路由实现
所需依赖
<dependencies>
<!-- SpringCloud Gateway 网关服务起步依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- SpringCloud Alibaba Nacos 服务注册发现依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- SpringCloud Alibaba Nacos Config 配置管理起步依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- SpringCloud Loadbalancer 负载均衡依赖模块 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
</dependencies>
属性类
这个属性定义也不是必须得,只是方便可以更加灵活进行一个变更,所以才进行这样的一个属性定义类,可以根据系统业务的设计来进行定义
package com.if010.gateway.properties;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* Swagger Properties配置信息实体类
* @author Kim同学
*/
@Data
@NoArgsConstructor
@ToString
@Component
@ConfigurationProperties(prefix = "spring.cloud.gateway.discovery.locator")
public class RouteAutoLoaderProperties {
/**
* 是否启用自动加载路由配置
*/
public boolean enabled;
/**
* Nacos 网关路由配置命名空间
*/
public String dataId;
/**
* Nacos 网关路由配置分组
*/
public String group;
/**
* Nacos 网关路由配置拉取超时时间
*/
public Integer timeoutMs = 60000;
}
配置类
package com.if010.gateway.config;
import com.alibaba.cloud.nacos.NacosConfigManager;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import com.if010.gateway.properties.RouteAutoLoaderProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
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;
import java.util.concurrent.Executors;
/**
* 网关路由自动加载配置类
* 判断是否注册该类到Bean容器当中,当配置文件中spring.cloud.gateway.discovery.locator.enabled = true时生效, 否则不生效
* @author Kim同学
*/
@Slf4j
@Component
@RequiredArgsConstructor
@ConditionalOnProperty(name = "spring.cloud.gateway.discovery.locator.enabled", havingValue = "true")
public class RouteAutoLoaderConfig {
// 注入 NacosConfigManager
private final NacosConfigManager nacosConfigManager;
// 注入 RouteAutoLoaderProperties
private final RouteAutoLoaderProperties routeAutoLoaderProperties;
// 注入 RouteDefinitionWriter
private final RouteDefinitionWriter writer;
// 定义路由ID集合
private Set<String> routeIds = new HashSet<>();
/**
* 在Bean创建,且NacosConfigManager注入成功后,初始化路由配置
*/
@PostConstruct
public void initRouteConfiguration() throws NacosException {
log.info("【网关路由自动加载】开始初始化路由配置 {} {} {}", routeAutoLoaderProperties.getDataId(), routeAutoLoaderProperties.getGroup(),routeAutoLoaderProperties.getTimeoutMs());
// 1、第一次启动时,拉取路由表,并且添加监听器
String configAndSignListener = nacosConfigManager.getConfigService().getConfigAndSignListener(
// 设定 Nacos 网关路由配置命名空间
routeAutoLoaderProperties.getDataId(),
// 设定 Nacos 网关路由配置分组
routeAutoLoaderProperties.getGroup(),
// 设定 Nacos 网关路由配置拉取超时时间
routeAutoLoaderProperties.getTimeoutMs(),
new Listener() {
@Override
public Executor getExecutor() {
// 定义使用单线程处理监听事件
return Executors.newSingleThreadExecutor();
}
@Override
public void receiveConfigInfo(String configInfo) {
// 监听到路由变更时自动更新路由表
updateRouteConfigInfo(configInfo);
}
});
// 2、写入路由表
updateRouteConfigInfo(configAndSignListener);
}
/**
* 【方法】更新路由配置信息
* @param configInfo 路由配置信息
*/
private void updateRouteConfigInfo(String configInfo) {
// 1、解析路由配置信息 (json字符串 转 数组)
List<RouteDefinition> routeDefinitions = JSONArray.parseArray(configInfo, RouteDefinition.class);
log.info("【网关路由自动加载】监听到路由变更,开始更新路由表,路由数量:{}", routeDefinitions.size());
log.info("【网关路由自动加载】监听到路由变更,开始更新路由表,路由信息:{}", routeDefinitions.toString());
// 2、删除旧的路由配置信息
for (String routeId : routeIds) {
log.info("【网关路由自动加载】开始删除旧的路由配置信息,路由:{}", routeId);
writer.delete(Mono.just(routeId)).subscribe();
}
// 3、清空路由ID集合
routeIds.clear();
// 4、 判断是否有新路由
if (routeDefinitions == null || routeDefinitions.isEmpty()) {
log.info("【网关路由自动加载】监听到路由变更,但未发现新路由,无需更新路由表");
// 5、没有新路由,则直接返回
return;
}
// 6、更新路由表
for (RouteDefinition routeDefinition : routeDefinitions) {
log.info("【网关路由自动加载】开始写入路由表,路由:{}", routeDefinition.getId());
// 7、写入到 Gateway 路由表
writer.save(Mono.just(routeDefinition)).subscribe();
// 8、将路由ID添加到集合中,以便下次更新删除使用
routeIds.add(routeDefinition.getId());
}
}
}
测试
到此,我们已经将网关动态路由配置类定义完毕,接下来我们还需要进行一下application.yml
文件的属性定义配置
spring:
application:
# 应用名称
name: if010-gateway
profiles:
# 环境配置
active: dev
cloud:
# Nacos注册中心配置
nacos:
discovery:
# 服务注册地址
server-addr: 127.0.0.1:8848
config:
# 配置中心地址
server-addr: 127.0.0.1:8848
# 配置文件格式
file-extension: yml
# 共享配置
shared-configs:
- application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
- application-${spring.profiles.active}-nacos.${spring.cloud.nacos.config.file-extension}
- ${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
# 超时时间
timeout: 3000
# 路由配置
gateway:
discovery:
locator:
# 是否开启动态路由发现功能,这里的enabled属性是必须的,是SpringCloud Geateway的规范定义,和属性类无关
enabled: true
dataId: ${spring.application.name}-routes.josn
group: DEFAULT_GROUP
timeoutMs: 1000
listenInterval: 1000
注意:启动网关之前还需要再启动类上加上注解
@EnableDiscoveryClient
不然是无法正常重写路由的哦!!!
启动时我们可以过滤日志输出看看拉取回来的信息
15:05:33.264 [main] INFO c.i.g.c.RouteAutoLoaderConfig - [initRouteConfiguration,52] - 【网关路由自动加载】开始初始化路由配置 if010-gateway-routes.josn DEFAULT_GROUP 1000
15:05:33.350 [main] INFO c.i.g.c.RouteAutoLoaderConfig - [updateRouteConfigInfo,86] - 【网关路由自动加载】监听到路由变更,开始更新路由表,路由数量:2
15:05:33.350 [main] INFO c.i.g.c.RouteAutoLoaderConfig - [updateRouteConfigInfo,87] - 【网关路由自动加载】监听到路由变更,开始更新路由表,路由信息:[RouteDefinition{id='system-service', predicates=[PredicateDefinition{name='Path', args={_genkey_0=/system-service/**}}], filters=[FilterDefinition{name='StripPrefix', args={_genkey_0=1}}], uri=lb://if010-system, order=0, metadata={}}, RouteDefinition{id='test-service', predicates=[PredicateDefinition{name='Path', args={_genkey_0=/test-service/**, _genkey_1=/test/**}}], filters=[FilterDefinition{name='StripPrefix', args={_genkey_0=1}}], uri=lb://if010-test, order=0, metadata={}}]
15:05:33.350 [main] INFO c.i.g.c.RouteAutoLoaderConfig - [updateRouteConfigInfo,106] - 【网关路由自动加载】开始写入路由表,路由:system-service
15:05:33.370 [main] INFO c.i.g.c.RouteAutoLoaderConfig - [updateRouteConfigInfo,106] - 【网关路由自动加载】开始写入路由表,路由:test-service
到此我们如果能正常访问到自己的业务,就证明成功啦~~~