哈喽大家好,我是阿Q!
之前公司使用了swagger
作为文档管理工具,原生的swagger-ui
非常丑,后来改用了开源项目 萧明 / knife4j 的swagger
组件进行了swagger
渲染,改造之后界面漂亮多了,操作也方便了很多。
当然这不是重点,重点是我们项目引用了knife4j
之后出现的一些问题:
- 由于项目中使用了
spring security
,使用了knife4j
之后,需要对knife4j
单独做规则过滤,否则无法访问knife4j
的静态资源 - 无论是
knife4j
还是原来的swagger-ui
,只要服务一停止,swagger
文档就打不开了 - 同一个项目下不同的人想要展示不同的文档,特别是在开发阶段,前端同学需要保存多个
swagger
地址查看不同的文档 - 集成
knife4j
实际上对于项目来说是比较重的,每个微服务都搞一遍也增加了工作量 - ......
两种文档聚合模式
gateway
文档聚合模式
有人在gateway
处做了文档聚合,它的聚合模式如下图所示
它的原理很简单,就是将请求转发到微服务,从微服务的restful
接口中获取swagger
的json
信息,然后通过前端将swagger
信息渲染出来。
这样做的好处就是只需要在网关处集成swagger-ui
,其它微服务不需要再单独集成,只需要收集swagger
信息然后暴露接口给gateway
,等着gateway
来取信息即可。但是它没有完全解决上面提到的问题,而且还引入了新的问题
- 网关做文档聚合到底合不合理?本身来说网关是对外暴露的,这种接口文档有可能会被泄露给普通用户,而且个人认为在网关处做这个不符合网关的定位
- 这种模式无法解决开发阶段文档问题,开发阶段文档是会随时更新的,这种模式需要将其发布到正式环境才能查看文档
- 还是要在
spring security
加白名单,放开swagger
对外的restful
接口 - 无法解决同一个项目不同文档的问题
针对这个问题,我想了想,使用另外一种方式尝试着进行改造。
集中注册模式
好吧,这个名字我瞎起的。具体技术架构如下图所示
系统流程如下:
- 每个微服务启动的时候从
nacos
、eureka
等注册中心获取swagger
注册中心服务的注册信息,然后调用swagger
注册中心的接口,将swagger
信息保存到数据库 swagger
注册中心集成knife4j
,本身也是一个单独的微服务,其连接数据库并管理swagger
文档- 用户只能内网访问
swagger
注册中心,swagger
注册中心从数据库取出swagger
文档信息并通过knife4j
渲染
需要注意的是swagger
注册中心只部署开发环境或者公司局域网环境,我们公司局域网能直接访问开发环境。
集中注册模式的代码设计如下,这里搞两个单独的项目
项目名 | 功能 |
---|---|
swagger-spring-boot-starter | 客户端组件,微服务客户端使用封装好的该组件扫描项目中的swagger信息并上传到swagger注册中心 |
swagger-register-server | swagger注册中心,它接收微服务客户端上传的swagger信息并保存到数据库。用户请求查看文档的时候直接从数据库中取swagger文档 |
在一切开始之前,需要了解下swagger-ui
的实现原理
swagger-ui 实现原理
/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
对象,这给我的实现提供了很大的参考依据。
/swagger-resources
接口
源码解析
在通过网关聚合模式下查看swagger
文档的时候,会发现前端会请求后端一个接口获取所有的group
信息:/swagger-resources
,老规矩,还是ctrl shift f
快捷键全局查询,可以看到相关代码的实现
springfox.documentation.swagger.web.ApiResourceController#swaggerResources
可以看到,该接口仅仅是调用了swaggerResource
的get
方法,然后就直接返回了,那就再看看swaggerResource
是什么东西
它只是个接口,那它的实现类呢,它的实现类只有一个,就是InMemorySwaggerResourcesProvider
类
它的GET方法是这样子的
看到这里我不禁陷入了思考,难道要给documentationCache
手动填充文档?但是看这个名字就知道是基于内存的东西,要维护CRUD
状态似乎有点麻烦,看看这个代码是咋写的
确实是基于内存的东西,但是只提供了add
方法,没提供remove
方法,那获取到documentionLookup
对象之后手动移除呢?仔细看看all()
方法
它被Collections
工具类包装成了不可修改的了,那手动移除的方式就没戏了......
换一种思路,其实还有另外一种方法,重新实现SwaggerResourcesProvider
接口,并将实现类使用@Primary
注解修饰,覆盖默认的InMemorySwaggerResourcesProvider
实现类,重写get()
方法即可。
那这时候的自由度就大了去了,这里可以直接使用从数据库读的方式获取所有的group
。
返回值解析
/swagger-resources
接口的返回值是List<swaggerresource>
类型,SwaggerResource类
的定义如下
name
:显示的名字url
:前端根据该url
获取swagger
文档详情(默认是/v2/api-docs
,其实可以修改该值让swagger-ui
请求自定义的接口获取swagger
文档)swaggerVersion
:就是swagger
版本,一般就是2.0
在继续往下学习之前先来张图感受下,便于大家的理解:
注册中心
项目源代码:
https://gitee.com/kdyzm/swagger-register-server
它是一个swagger
注册中心,对swagger
文档进行持久化并进行CRUD
操作,最终在knife4j
中展示。它应当包含如下功能
- 接收客户端传来的
swagger
文档信息并保存到数据库 - 集成
knife4j
并展示文档 - 提供
knife4j
前端页面/swagger-resources
接口逻辑实现 - 提供
knife4j
前端页面获取文档详情接口 - 能够动态更新文档
表结构设计
设计上,用两张表分别存储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;
代码语言:javascript复制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
的原始信息,用于文档渲染。
接收注册接口
swagger-register-server
中SwaggerRegisterController
的regist()
方法
对应以上两个表,注册接口有两个实体类。注册逻辑是:存在则更新,不存在就新增,groupName
和appName
都要保持唯一。
获取swagger
详情接口
swagger-register-server
中SwaggerRegisterController
的getSwaggerDetail()
方法
默认值是/v2/api-docs
,但是可以自定义,这里要求客户端在注册的时候就约定好接口路径是/swagger/detail
。
该接口从数据库中获取
swagger
信息。
获取resources
列表接口
从之前的/swagger-resources
源码分析过,想要从数据库自定义获取group
列表,就需要重新实现SwaggerResourcesProvider
接口并且标记为@Primary
swagger-register-server
中DocumentationConfig
创建 starter
项目源代码:
https://gitee.com/kdyzm/swagger-spring-boot-starter
设计上,要求做到微服务客户端只需要引入组件jar
包,然后配置文件配置一些swagger
的基本信息,服务启动之后就能自动上传swagger
文档到swagger
注册中心,具体技术细节,应当包含如下功能
- 能够实现
swagger
文档的完整上传,其效果和直接请求本地的/v2/api-docs
一样 - 支持服务发现
swagger
注册中心以及swagger
注册中心url
配置两种方式 - 客户端能够以
springboot starter
方式自动配置实现无代码侵入式生效 swagger-spring-boot-starter
客户端组件同时兼容eureka
和nacos
swagger
文档的扫描和上传
上面分析过/v2/api-docs
的实现原理,利用它的实现原理,可以轻松获取到Swagger
对象
swagger-spring-boot-starter
中SwaggerMvcGenerator
的getSwagger()
方法
上传的话,根据配置文件中是否配置serverUrl
决定采用服务发现方式还是直接请求方式上传Swagger
信息
swagger-spring-boot-starter
中SwaggerRegistryService
的registry()
方法
springboot starter
支持
这个非常简单,在resources/META-INF
目录下新建文件并配置好即可。
兼容注册中心
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 |
启动swagger
注册中心
该项目启动需要连接mysql
数据库以及nacos
nacos
我搭建了一个在线版本,可以直接使用(这里不提供管理端的账号密码),nacos
在线地址:nacos.kdyzm.cn
mysql
需要自己创建数据库,运行脚本创建相关的数据库和表结构以及初始化部分数据。
脚本地址:
https://gitee.com/kdyzm/swagger-register-server/blob/master/sql/init.sql
准备好外部依赖之后,执行sql
文件夹中的sql
文件,最后启动项目即可,启动成功之后,访问项目的/doc.html
,即可看到knife4j
的文档页面。
这里我提供了线上部署好的版本:http://swagger.kdyzm.cn
编译打包 starter
上一步启动好了swagger-register-server
,接下来需要打包swagger-spring-boot-starter
以供微服务客户端使用。
因为这里并没有上传maven
中央仓库,所以有条件的可以上传nexus
私服,没条件的可以直接运行命令mvn clean install
将jar
包安装到本地maven
仓库以便使用。
创建测试项目
可以使用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
第一步:引入依赖
代码语言:javascript复制<!-- swagger功能组件 -->
<dependency>
<groupid>com.kdyzm</groupid>
<artifactid>swagger-spring-boot-starter</artifactid>
<version>1.0-SNAPSHOT</version>
</dependency>
第二步:配置swagger
信息
在配置文件中新增配置
代码语言:javascript复制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
文档,必须激活swagger profile
才会生效。
项目启动之后如果没有任何报错,打开文档地址:http://swagger.kdyzm.cn/doc.html
查看文档上传效果。
其它问题
公益地址问题
还有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 (无需账号密码访问) |
由于受限于资源和网络带宽,访问速度会比较慢;请善待公共资源,不要对它们进行压测和其它非正常操作。
模式切换
配置文件中有个配置项:swagger.config.server-url
,若该配置项不为空,则走直连模式,即不通过服务发现直接请求该server-url
上传swagger
文档;
如果未配置该配置项,则检查swagger.config.service-id
字段,如果该字段也没有配置值,则报错并跳过swagger
文档上传。
配置唯一性
为了能在分组里唯一区分,必须要将appName
和name
保持唯一,而且现在上传文档之后不支持删除,如果误上传到了swagger.kdyzm.cn
,发邮件给我我来删除,我的邮箱地址:kdyzm@foxmail.com
源代码
原本分了两个单独的项目,维护起来不是很方便
项目名称 | 项目地址 |
---|---|
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 |
以后的更新均会放到该项目中进行。