OpenAPI(Swagger3)接口文档自定义排序(万能大法,支持任意swagger版本)

前置参考文档
基于OpenAPI(Swagger3)使用AOP技术,进行日志记录
使用SpringAOP的方式修改controller接口返回的数据
SpringBoot3集成OpenAPI3(解决Boot2升级Boot3)

总结一句话:既然没办法去通过各种方法或者官方的接口去修改接口顺序,那我们就拦截swagger获取接口地址的列表,直接去修改他的返回结果,把返回的接口列表改成我们自己想要的排序列表。

成果描述

如何才能在提供的在线OpenAPI(Swagger3)接口文档,按照下面我们能自定义的接口顺序排序?
例如:下面的两个排序
排序1:API-1项目管理、API-2文档管理、API-3资源管理、API-10代码生成工具
排序2:1新增项目、2按照主键删除项目、3按照主键修改项目
在这里插入图片描述

问题描述

在SpringBoot2中,使用Springfoc来简化和swagger/openAPI的集成,
在SpringBoot3中,使用springdoc来简化和swagger/openAPI的集成,

在SpringBoot2,想要排序很简单,网上也能找到很多教程。这里说下我的实现方式。接口方法这个直接添加
@ApiOperationSupport(order = 1)就可以排序,也就是针对AuthController中的接口方法,可以使用order来排序。而对于controller类就目前来说,官方并没有提供统一的排序方式,或者说就算有,也会因为不通版本的原因,依赖文档导致排序失败。我的实现方式就是,通过名称的数字来进行排序。
在这里插入图片描述
比如1用户管理、2角色管理、3菜单管理。就可以直接进行排序了。也就是使用name排序。

但是有个问题,就是如果有个controller是10文件管理,最后排序会变成:10文件管理、1用户管理、2角色管理、3菜单管理,也就是排序规则变成了下面这种方式:假设有30个controller(tags)
[1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 2, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 3, 30, 4, 5, 6, 7, 8, 9]。
因此SpringBoot2以前,我排序是在接口前面加上数字前缀,例如
API-10、API-11、API-12、API-13
API-20、API-21、API-22、API-23
API-30、API-31、API-32、API-33
API-40、API-41、API-42、API-43

然后针对SpringBoot3,这个办法失效了,尝试用了很多方法,始终没解决。所以最后没得办法,基于上述的方式,自己来实现
基于OpenAPI(Swagger3)使用AOP技术,进行日志记录
使用SpringAOP的方式修改controller接口返回的数据
SpringBoot3集成OpenAPI3(解决Boot2升级Boot3)

实现方式

既然官方不能保证百分百的有可以设置排序这个接口,那么我们就参照上面的例子,利用AOP来修改参数
下面是swagger启动的在线接口地址。
http://127.0.0.1:18086/doc.html
该页面会调用接口,并且拿到接口返回值
http://127.0.0.1:18086/v3/api-docs
着重观察path3中的信息,结合我们看到的在线接口文档,可以知道
相同的tags下,如果代码中设置了order,那么对于接口方法,则会产生x-oder,页面会根据使用tags进行分组,然后按照x-oder进行排序(前端页面渲染是排序)
在这里插入图片描述
那么针对我现在用的这个版本SpringBoot3,接口方法可以排序,而controller则没有排序。
现在我定义了tags标签,并且加上特定的数字,我希望生成
API-1项目管理,
API-2文档管理,
API-3资源管理,
API-10代码生成工具,
API-90动态资源接口demo测试,
API-98字典码表,

而实际上顺序为:
API-3资源管理,
API-1项目管理,
API-2文档管理,
API-10代码生成工具,
API-98字典码表,
API-90动态资源接口demo测试
现在我们打印整个paths中的tags来看看
在这里插入图片描述
仔细观察,首先出现的是API-3资源管理,其次是API-1项目管理、在其次是API-2文档管理,tags出现的顺序和最后生成的接口文档顺序一样,也就是说针对tags的排序,可能是根据paths中tags出现的顺序排序的。而针对方法的排序,则是针对order排序的。
API-3资源管理,
API-1项目管理,
API-2文档管理,
API-10代码生成工具,
API-98字典码表,
API-90动态资源接口demo测试
那不防,我们更改下这个tags出现的顺序。来看看是否会改变接口文档的顺序。也就是我们要把顺序调整为下面这样。

API-1项目管理        3按照主键修改项目
API-1项目管理        5查询项目树结构
API-1项目管理        1新增项目
API-1项目管理        2按照主键删除项目
API-1项目管理        4按照主键查询项目API-2文档管理        4保存文档和资源的关系
API-2文档管理        5删除文档和资源的关系
API-2文档管理        11预览报告文档
API-2文档管理        7按照主键修改文档信息
API-2文档管理        2新增文档信息
API-2文档管理        6按照主键删除文档
API-2文档管理        9按照条件进行分页查询文档
API-2文档管理        1导入文档模板
API-2文档管理        10使用报告配置生成报告文档
API-2文档管理        12文档保存callback回调
API-2文档管理        3查询文档和资源的关系
API-2文档管理        8按照主键查询文档API-3资源管理        4按照主键修改资源基本信息
API-3资源管理        1新增资源
API-3资源管理        1.1保存静态资源内容
API-3资源管理        1.2保存静态资源的图片
API-3资源管理        1.3保存动态资源配置
API-3资源管理        3按照主键删除资源
API-3资源管理        5按照条件进行分页查询资源
API-3资源管理        2按照主键查询资源API-10代码生成工具        1按照系统数据库表生成Controler、Service、Mapper、Entity
API-10代码生成工具        2下载数据库表结构的注释说明API-98字典码表        1获取当前所支持的字典组
API-98字典码表        2根据字典组获取字典码表

实现思路

1、构建有序Map<tags, List<接口>>,即
API-3资源管理 :【4按照主键修改资源基本信息、5删除文档和资源的关系…】
API-1项目管理 :【3按照主键修改项目、5查询项目树结构、1新增项目…】
API-2文档管理 :【4保存文档和资源的关系、5删除文档和资源的关系…】
2、对tags进行排序
即变成
API-1项目管理
API-2文档管理
API-3资源管理
3、按照排序的tags从新生成paths

API-1项目管理        3按照主键修改项目
...............
API-2文档管理        4保存文档和资源的关系
API-2文档管理        5删除文档和资源的关系
...............
API-3资源管理        4按照主键修改资源基本信息
API-3资源管理        1新增资源
...............
API-10代码生成工具        1按照系统数据库表生成Controler、Service、Mapper、Entity
API-10代码生成工具        2下载数据库表结构的注释说明
API-98字典码表        1获取当前所支持的字典组
API-98字典码表        2根据字典组获取字典码表

4、把从新生成的paths替换旧的paths

完整代码

LogApiConfig

import java.util.ArrayList;
import java.util.List;import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.util.ObjectUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;import com.alibaba.druid.support.jakarta.WebStatFilter.StatHttpServletResponseWrapper;
import com.pcgy.gis.common.utils.JsonUtils;
import com.pcgy.gis.common.utils.TagsSortUtils;import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;/*** @description:基于swagger进行AOP日志记录* @author:hutao* @mail:hutao1@epri.sgcc.com.cn* @date:2022年10月17日 下午4:58:54*/
@Aspect
@SpringBootConfiguration
public class LogApiConfig {private final Logger logger = LoggerFactory.getLogger(LogApiConfig.class);/** @Description: RestController和Controller切入点* @author: 胡涛* @mail: hutao_2017@aliyun.com* @date: 2025年5月27日 下午2:50:17*/@Pointcut("within(@org.springframework.web.bind.annotation.RestController *) || within(@org.springframework.stereotype.Controller *)")public void controllerPointcut() {}/** @Description: Operation切入点* @author: 胡涛* @mail: hutao_2017@aliyun.com* @date: 2025年5月27日 下午2:49:41*/@Pointcut("@annotation(io.swagger.v3.oas.annotations.Operation)" )public void operationPpointcut() {}/*** @description:进行日志切入* @author:hutao* @throws Throwable * @mail:hutao1@epri.sgcc.com.cn* @date:2022年10月17日 下午5:00:09*/@Around("controllerPointcut() && operationPpointcut()")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();//获取请求地址if(attributes == null) {return joinPoint.proceed();}HttpServletRequest request = attributes.getRequest();String url = request.getRequestURL().toString();//获取OpenAPI的类说明@TagClass<?> controller = joinPoint.getThis().getClass();Tag tag = controller.getAnnotation(Tag.class);String apiDes = (tag != null && !ObjectUtils.isEmpty(tag.name())) ? tag.name() : controller.getSimpleName();//获取被调用的方法名String methodName = joinPoint.getSignature().getName();//获取OpenAPI的方法说明@OperationMethodSignature ms = (MethodSignature) joinPoint.getSignature();Operation operation = ms.getMethod().getDeclaredAnnotation(Operation.class);String apiOperationDes = (operation != null && !ObjectUtils.isEmpty(operation.summary())) ? operation.summary() : methodName;logger.info("start-->请求{}模块的[{}]服务",apiDes, apiOperationDes);logger.info("  请求地址:{}",url);logger.info("  请求方法:{}.{}", abbreviateName(joinPoint.getSignature().getDeclaringTypeName()), methodName);Object[] args = joinPoint.getArgs();List<Object> params = new ArrayList<>();try {for (int i = 0; i < args.length; i++) {if( !(args[i] instanceof StatHttpServletResponseWrapper) && !(args[i] instanceof HttpServletRequest)&& !(args[i] instanceof HttpServletResponse) && !(args[i] instanceof MultipartFile)) {logger.info("  请求参数{}:{}",i+1,JsonUtils.objToStr(args[i]));params.add(args[i]);}}} catch (Exception e) {logger.info("记录服务器日志异常,异常原因", e);}//对API接口排序byte[] apiSort = TagsSortUtils.apiSort(request, joinPoint);if(apiSort != null) {return apiSort;}long start = System.currentTimeMillis();Object proceed = joinPoint.proceed();long end = System.currentTimeMillis() - start;logger.info("end-->请求{}模块的[{}]服务>>, 消耗时间:{}秒",apiDes, apiOperationDes, end / 1000f);return proceed;}/*** @description:缩写类名<p>* 例如com.plan.map.module.count.controller.CountController转变成<p>* c.p.m.m.c.c.CountController<p>* @author:hutao* @mail:hutao1@epri.sgcc.com.cn* @date:2023年5月25日 下午3:14:36*/private String abbreviateName(String classdName) {String[] arr = classdName.split("\\.");StringBuilder sb = new StringBuilder();for (int i = 0; i < arr.length - 1; i++) {sb.append(arr[i].charAt(0)).append(".");}sb.append(arr[arr.length - 1]);return sb.toString();}
}

TagsSortUtils

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;import org.aspectj.lang.ProceedingJoinPoint;import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;import jakarta.servlet.http.HttpServletRequest;/** @Description: 对swagger的tags排序工具类* @author: 胡涛* @mail: hutao_2017@aliyun.com* @date: 2025年8月8日 上午11:45:45*/
public class TagsSortUtils {private static final String API_DOCS_PATH = "/v3/api-docs";private static final ObjectMapper mapper = new ObjectMapper();/** @Description: 对接口文档的tags排序* @author: 胡涛* @mail: hutao_2017@aliyun.com* @date: 2025年8月8日 上午11:39:41*/public static byte[] apiSort(HttpServletRequest request, ProceedingJoinPoint joinPoint) {if (API_DOCS_PATH.equals(request.getRequestURI())) {try {byte[] originalBytes = (byte[]) joinPoint.proceed();JsonNode root = mapper.readTree(originalBytes);//对tags分组Map<String, List<Entry<String, JsonNode>>> hashPaths = getAllPath(root);//对tags排序,并且生成排序后的pathsObjectNode newPaths = getSortPathByTags(hashPaths);((ObjectNode) root).set("paths", newPaths);// 返回修改后的响应return mapper.writeValueAsBytes(root);} catch (Throwable e) {e.printStackTrace();}}return null;}/** @Description: 生成新的paths,并且按照自己的方式排序* @author: 胡涛* @mail: hutao_2017@aliyun.com* @date: 2025年8月8日 上午11:44:27*/private static ObjectNode getSortPathByTags(Map<String, List<Entry<String, JsonNode>>> hashPaths) {List<String> tagSort = new ArrayList<>(hashPaths.keySet());Collections.sort(tagSort, new Comparator<String>() {@Overridepublic int compare(String s1, String s2) {// 提取数字部分(例如从"API-10..."中提取10)int num1 = extractNumber(s1);int num2 = extractNumber(s2);// 按数字大小排序return Integer.compare(num1, num2);}// 提取API-后的数字private int extractNumber(String s) {String numStr = s.replaceAll("API-(\\d+).*", "$1");return Integer.parseInt(numStr);}});ObjectNode newPaths = mapper.createObjectNode();tagSort.forEach(temp ->{List<Entry<String, JsonNode>> list = hashPaths.get(temp);list.forEach(tempz ->{newPaths.set(tempz.getKey(), tempz.getValue());});});return newPaths;}/** @Description: 获取/v3/api-docs中所有path2,并且按照tags进行分组* @author: 胡涛* @mail: hutao_2017@aliyun.com* @date: 2025年8月8日 上午11:40:11*/private static Map<String, List<Entry<String, JsonNode>>> getAllPath(JsonNode root) {Map<String, List<Entry<String, JsonNode>>> mapPaths = new HashMap<>();if (root.has("paths") && root.get("paths").isObject()) {ObjectNode pathsNode = (ObjectNode) root.get("paths");Iterator<Entry<String, JsonNode>> pathIterator = pathsNode.fields();while (pathIterator.hasNext()) {Entry<String, JsonNode> pathEntry = pathIterator.next();JsonNode pathDetails = pathEntry.getValue();Iterator<Map.Entry<String, JsonNode>> methodIterator = pathDetails.fields();while (methodIterator.hasNext()) {Map.Entry<String, JsonNode> methodEntry = methodIterator.next();JsonNode methodDetails = methodEntry.getValue();// 提取第一个标签信息String tags = "";if (methodDetails.has("tags") && methodDetails.get("tags").isArray() && !methodDetails.get("tags").isEmpty()) {tags = methodDetails.get("tags").get(0).asText();System.out.println(tags + "        "+(methodDetails.get("summary").asText()));}if(!mapPaths.containsKey(tags)) {mapPaths.put(tags, new ArrayList<>());}mapPaths.get(tags).add(pathEntry);}}}return mapPaths;}
}

注意事项

下面是我自定义排序的规则,这个要根据各自的排序规则来实现,例如我的tags都是API-数字XXXXX

		Collections.sort(tagSort, new Comparator<String>() {@Overridepublic int compare(String s1, String s2) {// 提取数字部分(例如从"API-10..."中提取10)int num1 = extractNumber(s1);int num2 = extractNumber(s2);// 按数字大小排序return Integer.compare(num1, num2);}// 提取API-后的数字private int extractNumber(String s) {String numStr = s.replaceAll("API-(\\d+).*", "$1");return Integer.parseInt(numStr);}});

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/94984.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/94984.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vue3上传的文件在线查看

1、npm install vue-office/pdf vue-demi 安装依赖2、npm install vue-office/excel vue-demi 安装依赖3、npm install vue-office/docx vue-demi 安装依赖4、编写一个通用组件&#xff0c;现在只支持 .docx,.xlsx,.pdf 格式的文件&#xff0c;其他文件渲染不成功<temp…

深度学习中基于响应的模型知识蒸馏实现示例

在 https://blog.csdn.net/fengbingchun/article/details/149878692 中介绍了深度学习中的模型知识蒸馏&#xff0c;这里通过已训练的DenseNet分类模型&#xff0c;基于响应的知识蒸馏实现通过教师模型生成学生模型&#xff1a; 1. 依赖的模块如下所示&#xff1a; import arg…

【数据可视化-82】中国城市幸福指数可视化分析:Python + PyEcharts 打造炫酷城市幸福指数可视化大屏

&#x1f9d1; 博主简介&#xff1a;曾任某智慧城市类企业算法总监&#xff0c;目前在美国市场的物流公司从事高级算法工程师一职&#xff0c;深耕人工智能领域&#xff0c;精通python数据挖掘、可视化、机器学习等&#xff0c;发表过AI相关的专利并多次在AI类比赛中获奖。CSDN…

TikTok网页版访问障碍破解:IP限制到高效运营的全流程指南

在跨境电商与社媒运营的数字化浪潮中&#xff0c;TikTok网页版因其多账号管理便捷性、内容采集高效性等优势&#xff0c;成为从业者的核心工具&#xff0c;然而“页面空白”“地区不支持” 等访问问题却频繁困扰用户。一、TikTok网页版的核心应用场景与技术特性&#xff08;一&…

spring的知识点:容器、AOP、事物

一、Spring 是什么? Spring 是一个开源的 Java 企业级应用框架,它的核心目标是简化 Java 开发。 它不是单一的工具,而是一个 “生态系统”,包含了很多模块(如 Spring Core、Spring Boot、Spring MVC 等),可以解决开发中的各种问题(如对象管理、Web 开发、事务控制等)…

HTML ISO-8859-1:深入解析字符编码标准

HTML ISO-8859-1:深入解析字符编码标准 引言 在HTML文档中,字符编码的选择对于确保网页内容的正确显示至关重要。ISO-8859-1是一种广泛使用的字符编码标准,它定义了256个字符,覆盖了大多数西欧语言。本文将深入探讨HTML ISO-8859-1的原理、应用及其在现代网页开发中的重要…

【计算机网络 | 第4篇】分组交换

文章目录前言&#x1f95d;电路交换&#x1f34b;电路交换技术的优缺点电路交换的资源分配机制报文交换&#x1f34b;报文交换技术的优缺点存储转发技术分组交换&#x1f426;‍&#x1f525;分组交换的过程分组交换解决的关键问题传输过程的关键参数工作原理分组传输时延计算网…

LLM - AI大模型应用集成协议三件套 MCP、A2A与AG-UI

文章目录1. 引言&#xff1a;背景与三协议概览2. MCP&#xff08;Model Context Protocol&#xff09;起源与动因架构与规范要点开发实践3. A2A&#xff08;Agent-to-Agent Protocol&#xff09;起源与动因架构与规范要点开发实践4. AG-UI&#xff08;Agent-User Interaction P…

机器学习DBSCAN密度聚类

引言 在机器学习的聚类任务中&#xff0c;K-means因其简单高效广为人知&#xff0c;但它有一个致命缺陷——假设簇是球形且密度均匀&#xff0c;且需要预先指定簇数。当数据存在任意形状的簇、噪声点或密度差异较大时&#xff0c;K-means的表现往往不尽如人意。这时候&#xff…

RecyclerView 缓存机制

一、四级缓存体系1. Scrap 缓存&#xff08;临时缓存&#xff09;位置&#xff1a;mAttachedScrap 和 mChangedScrap作用&#xff1a;存储当前屏幕可见但被标记为移除的 ViewHolder用于局部刷新&#xff08;如 notifyItemChanged()&#xff09;特点&#xff1a;生命周期短&…

大模型SSE流式输出技术

文章目录背景&#xff1a;为什么需要流式输出SSE 流式输出很多厂商还是小 chunk背景&#xff1a;为什么需要流式输出 大模型的响应通常很长&#xff0c;比如几百甚至几千个 token&#xff0c;如果等模型一次性生成完才返回&#xff1a; 延迟高&#xff1a;用户要等很久才能看…

[Flutter] v3.24 AAPT:错误:未找到资源 android:attr/lStar。

推荐超级课程&#xff1a; 本地离线DeepSeek AI方案部署实战教程【完全版】Docker快速入门到精通Kubernetes入门到大师通关课AWS云服务快速入门实战 前提 将 Flutter 升级到 3.24.4 后&#xff0c;构建在我的本地电脑上通过&#xff0c;但Github actions 构建时失败。 Flutt…

go语言标准库学习, fmt标准输出,Time 时间,Flag,Log日志,Strconv

向外输出 fmt包实现了类似C语言printf和scanf的格式化I/O。主要分为向外输出内容和获取输入内容两大部分。 内置输出 不需要引入标准库&#xff0c;方便 package mainfunc main() {print("我是控制台打印&#xff0c;我不换行 可以自己控制换行 \n我是另一行")prin…

ElementUI之表格

文章目录使用ElementUI使用在线引入的方式表格1. 带状态表格row-class-name"Function({row, rowIndex})/String"2. 固定表头(height"string/number"属性)2.1 属性的取值2.2 动态响应式高度使用演示2.3 ​​自定义滚动条样式​​2.4 表头高度定制获取一行信…

K8S 的 Master组件

K8S 的 Master 组件有哪些&#xff1f;每个组件的作用&#xff1f; K8s 大脑的 4 大核心模块&#xff0c;掌控全局&#xff01; Kubernetes 集群的 Master&#xff08;主节点&#xff09; 就像一座 指挥中心&#xff0c;负责整个集群的调度、管理和控制。它由 4 大核心组件组成…

如何 让ubuntu 在root 下安装的docker 在 普通用户下也能用

在 Ubuntu 系统中&#xff0c;如果 Docker 是以 root 用户安装的&#xff0c;普通用户默认无法直接使用 Docker 命令&#xff08;会报权限错误&#xff09;。要让普通用户也能使用 Docker&#xff0c;可以按照以下步骤操作&#xff1a;方法 1&#xff1a;将用户加入 docker 用户…

模板方法模式:优雅封装算法骨架

目录 一、模板方法模式 1、结构 2、特性 3、优缺点 3.1、优点 3.2、缺点 4、使用场景 5、实现示例 5.1、抽象类 5.2、实现类 5.3、测试类 一、模板方法模式 模板方法模式&#xff08;Template Method Pattern&#xff09;是一种行为设计模式&#xff0c;它在一个方…

韦东山STM32_HAl库入门教程(SPI)学习笔记[09]内容

&#xff08;1&#xff09;SPI程序层次一、核心逻辑&#xff1a;“SPI Flash 操作” 是怎么跑起来的&#xff1f;要读写 SPI Flash&#xff0c;需同时理解 硬件连接&#xff08;怎么接线&#xff09; 和 软件分层&#xff08;谁负责发指令、谁负责控制逻辑&#xff09;&#xf…

线上Linux服务器的优化设置、系统安全与网络安全策略

一、Linux服务器的优化设置 线上Linux的优化配置序号基础优化配置内容说明1最小化安装系统【仅安装需要的&#xff0c;按需安装、不用不装】&#xff0c;必须安装的有基本开发环境、基本网络包、基本应用包。2ssh登录策略优化 Linux服务器上的ssh服务端配置文件是【/et…

基于人眼视觉特性的相关图像增强基础知识介绍

目录 1. 传统的灰度级动态范围优化配置方法 2.基于视觉特性的灰度级动态范围调整优化 1. 传统的灰度级动态范围优化配置方法 传统的灰度级动态范围调整方法主要包括线性动态范围调整及非线性动态 范围调整。线性动态范围调整是最简单的灰度级动态范围调整方法&#xff0c;观察…