之前公司使用了swagger作为文档管理工具,原生的swagger-ui非常丑,之后就用了开源项目 萧明 / knife4j 的swagger组件进行了swagger渲染,改造之后界面漂亮多了,操作也方便了很多。当然这不是重点,重点是我们项目引用了knife4j之后出现的一些问题:

  1. 由于项目中使用了spring security,使用了knife4j之后,需要对knife4j单独做规则过滤,否则无法访问knife4j的静态资源
  2. 无论是knife4j还是原来的swagger-ui,只要服务一停止,swagger文档就打不开了
  3. 同一个项目下不同的人想要展示不同的文档,特别是在开发阶段,前端同学需要保存多个swagger地址查看不同的文档
  4. 集成knife4j实际上对于项目来说是比较重的,每个微服务都搞一遍也增加了工作量
  5. ......

一、两种文档聚合模式

1、gateway文档聚合模式

有人在gateway处做了文档聚合,它的聚合模式如下图所示

它的原理很简单,就是将请求转发到微服务,从微服务的restful接口中获取swagger的json信息,然后通过前端将swagger信息渲染出来。这样做的好处就是只需要在网关处集成swagger-ui,其它微服务不需要再单独集成swagger-ui,只需要收集swagger信息然后暴露接口给gateway,等着gateway来取信息即可。但是它没有完全解决上面提到的问题,而且还引入了新的问题

  1. 网关做文档聚合到底合不合理,本身来说网关是对外暴露的,这种接口文档有可能会被泄露给普通用户,而且个人认为在网关处做这个不符合网关的定位。
  2. 这种模式无法解决开发阶段文档问题,开发阶段文档是会随时更新的,这种模式需要将其发布到正式环境才能查看文档
  3. 还是要在spring security加白名单,放开swagger对外的restful接口
  4. 无法解决同一个项目不同文档的问题

针对这个问题,我想了想,使用另外一种方式尝试着进行改造。

2、集中注册模式

好吧,这个名字我瞎起的。具体技术架构如下图所示

系统流程如下:

  1. 每个微服务启动的时候从nacos、eureka等注册中心获取swagger注册中心服务的注册信息,然后调用swagger注册中心的接口,将swagger信息保存到数据库
  2. swagger注册中心集成knife4j,本身也是一个单独的微服务,其连接数据库并管理swagger文档
  3. 用户只能内网访问swagger注册中心,swagger注册中心从数据库取出swagger文档信息并通过knife4j渲染

需要注意的是swagger注册中心只部署开发环境或者公司局域网环境,我们公司局域网能直接访问开发环境。

集中注册模式的代码设计如下,这里搞两个单独的项目

项目名 功能
swagger-spring-boot-starter 客户端组件,微服务客户端使用封装好的该组件扫描项目中的swagger信息并上传到swagger注册中心
swagger-register-server swagger注册中心,它接收微服务客户端上传的swagger信息并保存到数据库。用户请求查看文档的时候直接从数据库中取swagger文档

在一切开始之前,需要了解下swagger-ui的实现原理

二、swagger-ui的实现原理

1、/v2/api-docs接口

正如之前所说,swagger-spring-boot-starter是客户端组件,微服务客户端使用封装好的该组件扫描项目中的swagger信息并上传到swagger注册中心。

关键的技术点是如何手动扫描项目的swagger信息,只要能拿到swagger信息,无论使用什么方式上传到swagger注册中心都很简单了。关于这个技术点想了一会儿没想到好办法,只能去看源代码,看了一会儿觉得云里雾里的,最终突然灵光一闪,swagger-ui的实现给了我灵感。

swagger-ui会请求后端一个接口获取swagger文档:/v2/api-docs,然后根据拿到的swagger文档渲染前端页面。在intelij下ctrl+shift+f组合键搜索该关键字很容易能够找到相关代码(springfox 2.9.2):

springfox.documentation.swagger2.web.Swagger2Controller#getDocumentation

这段代码详细讲解了如何获取Swagger对象,这给我的实现提供了很大的参考依据。

2、/swagger-resources接口

2.1 源码解析

在通过网关聚合模式下查看swagger文档的时候,会发现前端会请求后端一个接口获取所有的group信息:/swagger-resources,老规矩,还是ctrl+shift+f快捷键全局查询,可以看到相关代码的实现

springfox.documentation.swagger.web.ApiResourceController#swaggerResources

可以看到,改接口仅仅是调用了swaggerResource的get方法,然后就直接返回了,那就再看看swaggerResource是什么东西

https://github.com/springfox/springfox/blob/master/springfox-swagger-common/src/main/java/springfox/documentation/swagger/web/SwaggerResourcesProvider.java

它只是个接口,那它的实现类呢,它的实现类只有一个,就是InMemorySwaggerResourcesProvider类

它的GET方法是这样子的

https://github.com/springfox/springfox/blob/master/springfox-swagger-common/src/main/java/springfox/documentation/swagger/web/InMemorySwaggerResourcesProvider.java#L86

看到这里我不禁陷入了思考,难道要给documentationCache手动填充文档。。但是看这个名字就知道是基于内存的东西,要维护CRUD状态似乎有点麻烦。。看看这个代码是咋写的

https://github.com/springfox/springfox/blob/master/springfox-spring-web/src/main/java/springfox/documentation/spring/web/DocumentationCache.java#L28

确实是基于内存的东西,但是值提供了add方法,没提供remove方法,那获取到documentionLookup对象之后手动移除呢?仔细看看all()方法

它被Collections工具类包装成了不可修改的了,那手动移除的方式就没戏了。

换一种思路,其实还有另外一种方法,重新实现SwaggerResourcesProvider接口,并将实现类使用@Primary注解修饰,覆盖默认的InMemorySwaggerResourcesProvider实现类,重写get()方法即可,那这时候的自由度就大了去了,这里可以直接使用从数据库读的方式获取所有的group。

2.2 返回值解析

/swagger-resources接口的返回值是List类型,SwaggerResource类的定义如下

https://github.com/springfox/springfox/blob/master/springfox-swagger-common/src/main/java/springfox/documentation/swagger/web/SwaggerResource.java

  • name:显示的名字
  • url:前端根据该url获取swagger文档详情(默认是/v2/api-docs,其实可以修改该值让swagger-ui请求自定义的接口获取swagger文档)
  • swaggerVersion:就是swagger版本,一般就是2.0

三、swagger-register-server

项目源代码:https://gitee.com/kdyzm/swagger-register-server

它是一个swagger注册中心,对swagger文档进行持久化并进行CRUD操作,最终在knife4j中展示。它应当包含如下功能

  • 接收客户端传来的swagger文档信息并保存到数据库
  • 集成knife4j并展示文档
  • 提供knife4j前端页面/swagger-resources接口逻辑实现
  • 提供knife4j前端页面获取文档详情接口
  • 能够动态更新文档

1、表结构设计

设计上,用两张表分别存储group信息和文档详情信息

CREATE TABLE `group_info` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`name` varchar(64) NOT NULL COMMENT 'groupName',
`location` varchar(128) NOT NULL COMMENT 'location',
`version` varchar(16) NOT NULL COMMENT 'version',
`url` varchar(128) NOT NULL COMMENT 'url',
`app_name` varchar(64) DEFAULT NULL COMMENT '服务名(spring.application.name)',
`gateway` varchar(64) DEFAULT NULL COMMENT '网关,无则不填',
PRIMARY KEY (`id`),
UNIQUE KEY `group_info_name` (`name`) COMMENT 'group name唯一',
UNIQUE KEY `group_info_app_name` (`app_name`) COMMENT 'appname唯一'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `swagger_json` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`group_name` varchar(64) NOT NULL,
`content` longtext NOT NULL COMMENT 'swagger具体信息',
PRIMARY KEY (`id`),
UNIQUE KEY `swagger_json_groupname` (`group_name`) COMMENT 'groupName唯一'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

group_info表用于存储swagger的group信息,/swagger-resources接口将会从该表中取group数据

swagger_json表用于存储swagger的原始信息,用于文档渲染。

2、接收注册接口

https://gitee.com/kdyzm/swagger-register-server/blob/master/src/main/java/com/kdyzm/swagger/register/server/controller/SwaggerRegisterController.java#L35

对应以上两个表,注册接口有两个实体类。注册逻辑是:存在则更新,不存在就新增,groupName和appName都要保持唯一。

3、获取swagger详情接口

https://gitee.com/kdyzm/swagger-register-server/blob/master/src/main/java/com/kdyzm/swagger/register/server/controller/SwaggerRegisterController.java#L61

默认值是/v2/api-docs,但是可以自定义,这里要求客户端在注册的时候就约定好接口路径是/swagger/detail

该接口从数据库中获取swagger信息。

4、获取resources列表接口

从之前的/swagger-resources源码分析过,想要从数据库自定义获取group列表,就需要重新实现SwaggerResourcesProvider 接口并且标记为@Primary

https://gitee.com/kdyzm/swagger-register-server/blob/master/src/main/java/com/kdyzm/swagger/register/server/config/DocumentationConfig.java

四、swagger-spring-boot-starter

设计上,要求做到微服务客户端只需要引入组件jar包,然后配置文件配置一些swagger的基本信息,服务启动之后就能自动上传swagger文档到swagger注册中心,具体技术细节,应当包含如下功能

  • 能够实现swagger文档的完整上传,其效果和直接请求本地的/v2/api-docs一样
  • 支持服务发现swagger注册中心以及swagger注册中心url配置两种方式
  • 客户端能够以springboot starter方式自动配置实现无代码侵入式生效
  • swagger-spring-boot-starter客户端组件同时兼容eureka和nacos

1、swagger文档的扫描和上传

上面分析过/v2/api-docs的实现原理,利用它的实现原理,可以轻松获取到Swagger对象

https://gitee.com/kdyzm/swagger-spring-boot-starter/blob/master/src/main/java/com/kdyzm/swagger/spring/mvc/SwaggerMvcGenerator.java#L35

上传的话,根据配置文件中是否配置serverUrl决定采用服务发现方式还是直接请求方式上传Swagger信息

https://gitee.com/kdyzm/swagger-spring-boot-starter/blob/master/src/main/java/com/kdyzm/swagger/spring/mvc/SwaggerRegistryService.java#L50

2、springboot starter支持

这个非常简单,在resources/META-INF目录下新建文件

https://gitee.com/kdyzm/swagger-spring-boot-starter/blob/master/src/main/resources/META-INF/spring.factories

并配置好即可。

3、swagger-spring-boot-starter客户端组件同时兼容eureka和nacos

swagger-spring-boot-starter不依赖nacos client或者eurka client,而是依赖了它们的公共接口模块spring-cloud-commons,实际上nacos client或者eureka client均是该模块的具体实现,所以swagger-spring-boot-starter可以兼容两种客户端服务发现组件的实现,但是服务端因为具体依赖了某种服务发现组件,在我这里默认使用nacos,如果要用eureka需要自行改造。

五、实战

这篇文章介绍的两个项目的源代码地址:

项目名称 项目地址
swagger-register-server https://gitee.com/kdyzm/swagger-register-server
swagger-spring-boot-starter https://gitee.com/kdyzm/swagger-spring-boot-starter

1、启动swagger-register-server

该项目启动需要连接mysql数据库以及nacos。

准备好外部依赖之后,执行sql文件夹中的sql文件,最后启动项目即可,启动成功之后,访问项目的/doc.html,即可看到knife4j的文档页面。

这里我提供了线上部署好的版本:http://swagger.kdyzm.cn

2、编译打包swagger-spring-boot-starter

上一步启动好了swagger-register-server,接下来需要打包swagger-spring-boot-starter已提供微服务客户端使用。

因为这里并没有上传maven中央仓库,所以有条件的可以上传nexus私服,没条件的可以直接运行命令mvn clean install将jar包安装到本地maven仓库以便使用。

3、创建测试项目

可以使用intelij自带的工具初始化一个spring boot的项目,这里使用了2.3.4.REALEASE版本的springboot版本号(经过测试发现,nacos版本号过高会导致服务发现功能故障,版本号低一些程序功能会更稳定)。

利用intilij自带的spring initiallizer工具可以很方便的快速搭建起来web开发框架。写完Controller接口之后,开始整合swagger-spring-boot-starter,测试项目地址源代码:https://gitee.com/kdyzm/swagger-spring-boot-starter-test

第一步:引入依赖

<!-- swagger功能组件 -->
<dependency>
<groupid>com.kdyzm</groupid>
<artifactid>swagger-spring-boot-starter</artifactid>
<version>1.0-SNAPSHOT</version>
</dependency>

第二步:配置swagger信息

在配置文件中新增配置

swagger:
config:
#每个人只关心自己的包名,方便和前端文档对接
base-package: com.kdyzm.swagger.test
description: swagger测试项目
group:
#swagger注册唯一标识,每个人都要不一样
appName: ${spring.application.name}
name: swagger测试项目
api:
title: swagger测试项目
contactName: kdyzm@foxmail.com
#swagger注册中心地址,指定了server-url就优先使用该地址注册swagger文档信息;未指定则顺延使用服务发现模式
server-url: http://swagger.kdyzm.cn
#swgger注册中心serviceId,即servername,用于服务发现模式
service-id: swagger-register-server

第三步:激活swagger profile

只是做了前两步,不会对项目产生任何影响,也不会产生swagger文档,必须激活swagger profile才会生效

项目启动之后如果没有任何报错,打开文档地址:http://swagger.kdyzm.cn/doc.html 查看文档上传效果。

六、其它问题

1、公益nacos地址、eureka地址问题

还有swagger注册中心地址

服务名字 域名 访问地址
nacos地址 nacos.kdyzm.cn http://nacos.kdyzm.cn/nacos (不提供管理端账号密码)
eureka地址 eureka.kdyzm.cn http://eureka.kdyzm.cn (无需账号密码访问)
swagger注册中心地址 swagger.kdyzm.cn http://swagger.kdyzm.cn/doc.html (无需账号密码访问)

由于受限于资源和网络带宽,访问速度会比较慢;请善待公共资源,不要对它们进行压测和其它非正常操作。

2、如何切换服务发现模式和直连模式

配置文件中有个配置项:swagger.config.server-url ,若该配置项不为空,则走直连模式,即不通过服务发现直接请求该server-url上传swagger文档;

如果未配置该配置项,则检查swagger.config.service-id字段,如果该字段也没有配置值,则报错并跳过swagger文档上传。

3、appName和name配置要保持唯一

为了能在分组里唯一区分,必须要将appName和name保持唯一,而且现在上传文档之后不支持删除,如果误上传到了swagger.kdyzm.cn,发邮件给我我来删除,我的邮箱地址:kdyzm@foxmail.com

4、源代码

原本分了两个单独的项目,维护起来不是很方便

项目名称 项目地址
swagger-register-server https://gitee.com/kdyzm/swagger-register-server
swagger-spring-boot-starter https://gitee.com/kdyzm/swagger-spring-boot-starter

所以现在再加上实战案例放到同一个项目中进行管理。

三合一项目地址:

项目 地址
gitee地址 https://gitee.com/kdyzm/swagger-knife4j-spring-boot-starter
github地址 https://github.com/kdyzm/swagger-knife4j-spring-boot-starter

以后的更新均会放到该项目中进行。

博客原文地址

https://blog.kdyzm.cn/post/92

欢迎大家收藏访问~

魔改swagger:knife4j的另外一种打开方式的更多相关文章

  1. 文件的三种打开方式及with管理文件上下文

    文件的三种打开方式及with管理文件上下文 一.文件的三种打开方式 1.1 只读 f = open(r'D:\pycharm\yjy\上海python学习\456.txt','r',encoding= ...

  2. 【.NET 6】多线程的几种打开方式和代码演示

    前言: 多线程无处不在,平常的开发过程中,应该算是最常用的基础技术之一了.以下通过Thread.ThreadPool.再到Task.Parallel.线程锁.线程取消等方面,一步步进行演示多线程的一些 ...

  3. 石子归并的三种打开方式——难度递增———51Node

    1021 石子归并    N堆石子摆成一条线.现要将石子有次序地合并成一堆.规定每次只能选相邻的2堆石子合并成新的一堆,并将新的一堆石子数记为该次合并的代价.计算将N堆石子合并成一堆的最小代价.   ...

  4. java内存结构学习的一种打开方式

    用Java开发已经四年,中途学了python,Scala,接触这些开发语言后,总感觉Java就像老奶奶裹脚——又臭又长.然,Java虐我千百遍,我待Java如初恋.聊起Java,不得不谈Java的内存 ...

  5. 【bootstrap】modal模态框的几种打开方法+问题集锦

    第一部分: 关于bootstrap中modal的使用,下面把几种自己用的打开方法展示出来 首先呢,得有个Bootstrap的页面,这里就不说了. 其次呢,得有个modal放在页面中,不管你这段代码加在 ...

  6. 魔改——MDI多视图模板Tab/标签页 初始化/操作控件

    ==================================声明================================== 本文原创,转载在正文中显要的注明作者和出处,并保证文章的完 ...

  7. 魔改——MFC SDI程序 转换为 MDI程序

    ==================================声明================================== 本文原创,转载在正文中显要的注明作者和出处,并保证文章的完 ...

  8. 魔改——MFC MDI程序 定制 文档模板 运行时全部打开 禁用关闭按钮

    ==================================声明================================== 本文原创,转载在正文中显要的注明作者和出处,并保证文章的完 ...

  9. Java使用SFTP和FTP两种连接方式实现对服务器的上传下载 【我改】

    []如何区分是需要使用SFTP还是FTP? []我觉得: 1.看是否已知私钥. SFTP 和 FTP 最主要的区别就是 SFTP 有私钥,也就是在创建连接对象时,SFTP 除了用户名和密码外还需要知道 ...

随机推荐

  1. docker-compose权限不够

    root@kali:~# docker-compose version -bash: /usr/local/bin/docker-compose: 权限不够 chmod +x /usr/local/b ...

  2. 已知三角形ABC为锐角三角形,求 sinA + sinB·sin(C/2) 的最大值。

    已知三角形ABC为锐角三角形,求 sinA + sinBsin(C/2) 的最大值. 解:Δ := sinA + sinB·sin(C/2) = sin(B+C) + sinB·sin(C/2) = ...

  3. Qt5获取系统文件图标,文件路径

    获取系统图标: QFileIconProvider icon_provider; QIcon icon = icon_provider.icon(QFileIconProvider::Folder); ...

  4. Nginx对代理HTTP资源的限制访问

    为了限制连接的数量,首先,使用指令来定义密钥并设置共享内存区域的参数(工作进程将使用该区域来共享键值的计数器).作为第一个参数,指定作为关键字计算的表达式.在第二个参数区域中,指定区域的名称及其大小. ...

  5. K8S——Pod

    一.Pod概念 二.Pod存在的意义 三.Pod的实现机制 四.Pod镜像拉取策略 五.Pod资源限制 六.Pod重启机制 七.Pod的健康检查 八.Pod调度策略(创建Pod流程)

  6. Appium问题解决方案(10)- Original error: Swipe did not complete successfully

    背景 从搜索页面返回首页之后,执行  swipe 滑动操作,但是报错了,如上图 解决方法 只需要在第一次 swipe 之前加个 sleep,强制等待即可 备注 这种解决方案其实不好,强制等待能少用就少 ...

  7. 判断input radio选中那个

    var _sex=$("input[name='sex']:checked").val(); if(_sex==null){ layer.msg("请选择性别" ...

  8. Spring基于XML方式加载Bean定义信息(又名:Spring IOC源码时序图)-图解

  9. 跨域分布式系统单点登录的实现(CAS单点登录)

    1. 概述 上一次我们聊了一下<使用Redis实现分布式会话>,原理就是使用 客户端Cookie + Redis 的方式来验证用户是否登录. 如果分布式系统中,只是对Tomcat做了负载均 ...

  10. String底层使用是char数组还是byte数组

    结论:jdk1.8及以前String底层使用是char[],1.9开始使用byte[] jdk1.8 jdk13