Spring Cloud Gateway网关下Knife4j文档聚合,以及动态路由的读取和配置
一.Knife4j文档聚合
1.1 基础环境明细
我的环境明细如下:
Springboot: 2.7.17
Spring-cloud: 2021.0.7
Spring-cloud-alibaba:2021.0.5.0
spring-cloud-starter-alibaba-nacos-config:2021.0.5.0
spring-cloud-starter-alibaba-nacos-discovery:2021.0.5.0
Swagger 2
knife4j-gateway-spring-boot-starter:4.5.0
knife4j-openapi2-spring-boot-starter:4.50
至于其他相关环境自行搭建哦,这里不再赘述!捡重要的配置
1.2 集成knife4j
knife4j官网Blog自行查阅: 链接: https://doc.xiaominfo.com/docs/blog/gateway/knife4j-gateway-introduce
1.2.1 maven
<!--其他模块knife4j-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
<version>4.5.0</version>
</dependency>
<!--网关进行聚合的组件knife4j-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-gateway-spring-boot-starter</artifactId>
<version>4.5.0</version>
</dependency>
1.2.2 yml配置
1.2.2.1 其他模块配置
<!--其他模块knife4j-->
knife4j:
enable: true
openapi:
title: Knife4j官方文档
description: CLOUD-AUTH接口文档(REMOTE)
email: [email protected]
concat: siyu
url: https://docs.xiaominfo.com
version: v4.0
license: Apache 2.0
license-url: https://stackoverflow.com/
terms-of-service-url: https://stackoverflow.com/
group:
default:
group-name: default <!--此处可自定义-->
api-rule: package
api-rule-resources:
- com.siyu.controller
1.2.2.2 manual 手动配置模式
<!--网关进行聚合的组件knife4j-->
<!--博主这里使用的手动配置-->
knife4j:
gateway:
# ① 第一个配置,开启gateway聚合组件
enabled: true
# ② 第二行配置,设置聚合模式采用discover服务发现的模式
strategy: manual #discover 服务发现模式 #manual 手动配置模式
basic:
enable: true
username: *******
password: *******
1.2.2.3 discover 服务发现模式
<!--自动,服务发现模式-->
knife4j:
gateway:
# ① 第一个配置,开启gateway聚合组件
enabled: true
# ② 第二行配置,设置聚合模式采用discover服务发现的模式
strategy: discover #discover 服务发现模式 #manual 手动配置模式
discover:
# ③ 第三行配置,开启discover模式
enabled: true
# ④ 第四行配置,聚合子服务全部为Swagger2规范的文档
version: swagger2
basic:
enable: true
username: *******
password: *******
1.2.2.3 这里请注意:如果你使用了:SpringSecurity,Sa-token等安全框架请自行开放如下白名单
1.3 遇到的问题总览并解决
问题1:org.springframework.web.servlet.mvc.condition.PatternsRequestCondition.getPatterns() 报错 NULL
问题2:Failed to start bean ‘documentationPluginsBootstrapper
参考帖子:链接: Failed to start bean ‘documentationPluginsBootstrapper
问题3:访问地址后报404
参考帖子:链接: 访问地址后报404
问题1是我遇到的问题,新解法,问题2,3来自同一篇位置,位置如下图:
二.Knife4j文档聚合,GateWay网关,动态路由的配置
(我这里逻辑是放一起的,也可以自行逻辑分开)
GateWay当然也可以使用动态路由(感觉path加服务名感觉不nice)
Knife4j(Swagger2)文档聚合也可以使用服务发现模式,有一个问题就是,在切换分组的时候前缀path无法自动加入会报错(所以改成了manual模式,加动态配置路由的方式,嘿嘿!)
2.1 nacos config 配置
2.1.1 创建Data ID:
config-gateway-router
2.1.2 将动态路由的配置填入,并使用TXT的方式
如下格式,多个路由就直接在数组里面添加就行了
[
{
"id": "自定义1",
"uri": "lb://服务名称1", //这里是负责均衡,也可以设置服务的访问路径 IP:PORT
"predicates": [
{
"name": "Path",
"args": {
"pattern": "/服务1path/**"
}
}
]
},
{
"id": "自定义2",
"uri": "lb://服务名称2",
"predicates": [
{
"name": "Path",
"args": {
"pattern": "/服务2path/**"
}
}
]
}
]
如下图:
2.1.3 小插曲
可能有其他读取方式,后续补充!比如yaml中的集合
data:
- "A"
- "A"
- "A"
- "A"
- "A"
可以直接这样获取
@ConfigurationProperties(prefix = "data")
List<String> excludePaths() {
return new ArrayList<>();
}
这里knife4j 是沾光了gateway,因为文档聚合的相关属性也是从路由中获取的哦,还有服务名等等,
但是gateway配置的routes是一个对象,从nacos config 读取并转换,额~暂时没想到其他办法,(搞定后)后续更新吧!有其他办法的给我也说说!嘿嘿!
@Validated
public class RouteDefinition {
private String id;
private @NotEmpty @Valid List<PredicateDefinition> predicates = new ArrayList();
private @Valid List<FilterDefinition> filters = new ArrayList();
private @NotNull URI uri;
private Map<String, Object> metadata = new HashMap();
private int order = 0;
}
2.2 读取配置并操作路由
2.2.1 读取配置初始化
/**
* 初始化网关路由 nacos config
*/
private ConfigService initConfigService(){
try{
Properties properties = new Properties();
properties.setProperty("serverAddr", serverAddr);
if(!StringUtils.isEmpty(namespace))
properties.setProperty("namespace", namespace);
properties.setProperty("username", username);
properties.setProperty("password", password);
return configService = NacosFactory.createConfigService(properties);
} catch (Exception e) {
log.error("初始化网关路由时发生错误",e);
return null;
}
}
@PostConstruct
public void dynamicRouteByNacosListener() {
try {
if(StringUtils.isEmpty(group))
group = "";
configService = initConfigService();
if(configService == null){
log.warn("initConfigService 失败!");
return;
}
String configInfo = configService.getConfig(dataId, group, 3000);
List<RouteDefinition> definitionList = JSON.parseArray(configInfo, RouteDefinition.class);
log.info("[->获取<-]到dataId为{},group为{}的配置内容:{}",dataId,group,definitionList);
for(RouteDefinition definition : definitionList){
log.info("从nacos动态获取到的路由配置为 : {}",definition.toString());
addGateWayRoute(definition);//添加gateway路由
addKnife4jRoute(definition);//添加knife4j-gateway路由(开启手动配置模式)
}
} catch (NacosException e) {
log.error("发生错误!", e);
}
dynamicRouteByNacosListener(dataId, group);//监听该Data ID
}
2.2.2 添加gateway路由
/**
* 添加网关路由
* @param definition
*/
private void addGateWayRoute(RouteDefinition definition) {
log.info("开始添加Gateway路由配置 {}",definition);
try {
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
this.applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this));
log.info("添加Gateway路由成功 {}",definition);
} catch (Exception e) {
log.error("发生错误!", e);
}
}
2.2.3 添加knife4j-gateway路由
有一个问题:如果swagger group为自自定义名称,此处的逻辑需要修改哦,博主文章中统一使用的default,根据实际需求,如果有分多个接口文档组的话,额~,后续有空再补充吧,哈哈
/**
* 向Knife4jGatewayProperties中添加路由
* @param definition 网关路由对象
*/
private void addKnife4jRoute(RouteDefinition definition) {
log.info("开始添加Knife4j路由配置 {}",definition);
Knife4jGatewayProperties.Router router = new Knife4jGatewayProperties.Router();
router.setName(definition.getId());
router.setServiceName(definition.getUri().getHost());
definition.getPredicates().forEach(predicate -> {
if (Objects.equals(predicate.getName(), "Path")) {
String replace = predicate.getArgs().get("pattern").replace("/**", "").replace("/*", "");
router.setUrl(replace + router.getUrl());
}
});
knife4jGatewayProperties.getRoutes().add(router);
log.info("添加Knife4j路由成功,当前路由配置为 {}", JSONObject.toJSONString(knife4jGatewayProperties.getRoutes()));
}
2.3 监听配置的变化并操作路由
/**
* 监听Nacos下发的动态路由配置
*/
public void dynamicRouteByNacosListener (String dataId, String group){
try {
configService.addListener(dataId, group, new Listener() {
@Override
public void receiveConfigInfo(String configInfo) {
List<RouteDefinition> definitionList = JSON.parseArray(configInfo, RouteDefinition.class);
log.info("[->监听<-]到dataId为{},de group为{}的配置发生变更,配置内容:{}",dataId,group,definitionList);
updateGateWayRoute(definitionList);//更新gateway路由
updateKnife4jRoute(definitionList);//更新Knife4j-gateway路由
}
@Override
public Executor getExecutor() {
log.info("getExecutor");
return null;
}
});
} catch (NacosException e) {
log.error("从nacos接收动态路由配置出错!!!",e);
}
}
2.3.1 更新gateway路由
/**
* 更新路由
* @param definition 集合
*/
private void updateGateWayRoute(List<RouteDefinition> definition) {
definition.stream().iterator().forEachRemaining(route -> {
log.info("开始更新路由配置 {},先删除路由,再重新添加路由",route);
try {
routeDefinitionWriter.delete(Mono.just(route.getId()));
routeDefinitionWriter.save(Mono.just(route)).subscribe();
this.applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this));
log.info("更新路由配置成功 {}", route);
} catch (Exception e) {
log.error("更新路由配置失败:\r\n"+e);
}
});
}
2.3.1 更新knife4j-gateway路由
/**
* 更新Knife4j路由
* @param gatewayRouteDefinition 网关路由配置集合
*/
public void updateKnife4jRoute(List<RouteDefinition> gatewayRouteDefinition){
//删除不需要的路由
knife4jGatewayProperties.getRoutes().stream()
.filter(router -> !gatewayRouteDefinition.stream().map(RouteDefinition::getId).collect(Collectors.toList()).contains(router.getName()))
.collect(Collectors.toList()).iterator().forEachRemaining(router -> {
log.info("开始删除Knife4j路由配置 {}",router.getName());
knife4jGatewayProperties.getRoutes().remove(router);
});
//添加需要的路由
gatewayRouteDefinition.stream()
.filter(route -> !knife4jGatewayProperties.getRoutes().stream().map(Knife4jGatewayProperties.Router::getName).collect(Collectors.toList()).contains(route.getId()))
.collect(Collectors.toList()).iterator().forEachRemaining(route -> {
log.info("开始添加Knife4j路由配置 {}", route.getId());
addKnife4jRoute(route);
});
}
2.4 完整代码展示(相关变量自行配置注入)
package com.siyu.cloudgateway.routes;
import com.alibaba.cloud.commons.lang.StringUtils;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.nacos.api.NacosFactory;
import com.alibaba.nacos.api.annotation.NacosInjected;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import com.github.xiaoymin.knife4j.spring.gateway.Knife4jGatewayProperties;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.List;
import java.util.Objects;
import java.util.Properties;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
/**
* @author Siyu
* @date 2024/06/19 19:34
* @description 动态配置Route 包括Gateway和Knife4j
*/
@Slf4j
@RefreshScope
@Component
public class DynamicRouteByGateWayAndKnife4jForNacosConfig implements ApplicationEventPublisherAware {
@Value("${gateway.data-id}")
private String dataId;
@Value("${spring.cloud.nacos.config.group}")
private String group = "remote";
@Value("${spring.cloud.nacos.config.server-addr}")
private String serverAddr;
@Value("${spring.cloud.nacos.config.namespace}")
private String namespace;
@Value("${spring.cloud.nacos.config.username}")
private String username;
@Value("${spring.cloud.nacos.config.password}")
private String password;
@NacosInjected
private ConfigService configService;
@Autowired
private RouteDefinitionWriter routeDefinitionWriter;
@Resource
private Knife4jGatewayProperties knife4jGatewayProperties;
private ApplicationEventPublisher applicationEventPublisher;
@Override
public void setApplicationEventPublisher(@NotNull ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
/**
* 初始化网关路由 nacos config
*/
private ConfigService initConfigService(){
try{
Properties properties = new Properties();
properties.setProperty("serverAddr", serverAddr);
if(!StringUtils.isEmpty(namespace))
properties.setProperty("namespace", namespace);
properties.setProperty("username", username);
properties.setProperty("password", password);
return configService = NacosFactory.createConfigService(properties);
} catch (Exception e) {
log.error("初始化网关路由时发生错误",e);
return null;
}
}
@PostConstruct
public void dynamicRouteByNacosListener() {
try {
if(StringUtils.isEmpty(group))
group = "";
configService = initConfigService();
if(configService == null){
log.warn("initConfigService 失败!");
return;
}
String configInfo = configService.getConfig(dataId, group, 3000);
List<RouteDefinition> definitionList = JSON.parseArray(configInfo, RouteDefinition.class);
log.info("[->获取<-]到dataId为{},group为{}的配置内容:{}",dataId,group,definitionList);
for(RouteDefinition definition : definitionList){
log.info("从nacos动态获取到的路由配置为 : {}",definition.toString());
addGateWayRoute(definition);
addKnife4jRoute(definition);
}
} catch (NacosException e) {
log.error("发生错误!", e);
}
dynamicRouteByNacosListener(dataId, group);
}
/**
* 监听Nacos下发的动态路由配置
*/
public void dynamicRouteByNacosListener (String dataId, String group){
try {
configService.addListener(dataId, group, new Listener() {
@Override
public void receiveConfigInfo(String configInfo) {
List<RouteDefinition> definitionList = JSON.parseArray(configInfo, RouteDefinition.class);
log.info("[->监听<-]到dataId为{},de group为{}的配置发生变更,配置内容:{}",dataId,group,definitionList);
updateGateWayRoute(definitionList);
updateKnife4jRoute(definitionList);
}
@Override
public Executor getExecutor() {
log.info("getExecutor");
return null;
}
});
} catch (NacosException e) {
log.error("从nacos接收动态路由配置出错!!!",e);
}
}
/**
* 更新路由
* @param definition 集合
*/
private void updateGateWayRoute(List<RouteDefinition> definition) {
definition.stream().iterator().forEachRemaining(route -> {
log.info("开始更新路由配置 {},先删除路由,再重新添加路由",route);
try {
routeDefinitionWriter.delete(Mono.just(route.getId()));
routeDefinitionWriter.save(Mono.just(route)).subscribe();
this.applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this));
log.info("更新路由配置成功 {}", route);
} catch (Exception e) {
log.error("更新路由配置失败:\r\n"+e);
}
});
}
/**
* 更新Knife4j路由
* @param gatewayRouteDefinition 网关路由配置集合
*/
public void updateKnife4jRoute(List<RouteDefinition> gatewayRouteDefinition){
//删除不需要的路由
knife4jGatewayProperties.getRoutes().stream()
.filter(router -> !gatewayRouteDefinition.stream().map(RouteDefinition::getId).collect(Collectors.toList()).contains(router.getName()))
.collect(Collectors.toList()).iterator().forEachRemaining(router -> {
log.info("开始删除Knife4j路由配置 {}",router.getName());
knife4jGatewayProperties.getRoutes().remove(router);
});
//添加需要的路由
gatewayRouteDefinition.stream()
.filter(route -> !knife4jGatewayProperties.getRoutes().stream().map(Knife4jGatewayProperties.Router::getName).collect(Collectors.toList()).contains(route.getId()))
.collect(Collectors.toList()).iterator().forEachRemaining(route -> {
log.info("开始添加Knife4j路由配置 {}", route.getId());
addKnife4jRoute(route);
});
}
/**
* 向Knife4jGatewayProperties中添加路由
* @param definition 网关路由对象
*/
private void addKnife4jRoute(RouteDefinition definition) {
log.info("开始添加Knife4j路由配置 {}",definition);
Knife4jGatewayProperties.Router router = new Knife4jGatewayProperties.Router();
router.setName(definition.getId());
router.setServiceName(definition.getUri().getHost());
definition.getPredicates().forEach(predicate -> {
if (Objects.equals(predicate.getName(), "Path")) {
String replace = predicate.getArgs().get("pattern").replace("/**", "").replace("/*", "");
router.setUrl(replace + router.getUrl());
}
});
knife4jGatewayProperties.getRoutes().add(router);
log.info("添加Knife4j路由成功,当前路由配置为 {}", JSONObject.toJSONString(knife4jGatewayProperties.getRoutes()));
}
/**
* 添加网关路由
* @param definition
*/
private void addGateWayRoute(RouteDefinition definition) {
log.info("开始添加Gateway路由配置 {}",definition);
try {
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
this.applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this));
log.info("添加Gateway路由成功 {}",definition);
} catch (Exception e) {
log.error("发生错误!", e);
}
}
}
三.效果展示
3.1 聚合状态呈现
点击每一个分组会自动调用其他模块的swagger分组数据
3.2 测试网关服务-调用其他模块的服务
略,根据实际情况自行测试和验证
四. 结束语
4.1 相关说明:
这里集成的Swagger2规范,如果是Swagger3,很多不一样的地方:yaml配置,maven,静态资源path放行 等不一样哦,其他请自行踩坑或者查询官方集成Blog:比如Spring boot 3+的版本,Swagger3
有空后面再折腾Swagger3,如果有其他集成博文可以@,私信我呀,资源共享嘛,哈哈!
此篇文章有借鉴其他文章的内容并融合而成:介意请联系修改呀!
有其他问题请在文章评论留下各位的宝贵意见(听劝),私信或者邮箱联系呀:[email protected]
索取的同时也做做贡献,和各位一起成长,逐渐成为大佬而不断努力。同时也记录一下开发的经验和思路。
标签:definition,网关,log,Spring,配置,Knife4j,import,gateway,路由 From: https://blog.csdn.net/x910380566/article/details/139830652