当前位置: 首页 > 科技观察

实战!魔改Swagger,Knife4j的另一种打开方式

时间:2023-03-18 23:42:15 科技观察

大家好,我是阿Q!之前公司使用swagger作为文档管理工具,原来的swagger-ui非常难看,后来转用开源项目小明/knife4j的swagger组件进行了swagger渲染。改造后界面美观多了,操作也方便多了。当然,这不是重点。重点是我们项目引用knife4j后出现了一些问题:由于项目中使用了springsecurity,在使用knife4j后,需要单独过滤knife4j,否则无法访问knife4j的静态资源。无论是knife4j还是原来的swagger-ui,只要停止服务,就无法打开swagger文档。同一个项目下不同的人想要展示不同的文档。特别是在开发阶段,前端同学需要保存多个swagger地址来查看不同的文档。集成knife4j对于项目来说其实是比较重的,每个微服务都做一遍也增加了工作量...两种文档聚合模式gateway文档聚合模式有人在gateway做过文档聚合,它的聚合模式如图以下。它的原理很简单,就是将请求转发给微服务,从微服务的restful接口获取swagger的json信息,然后通过前端渲染swagger信息。这样做的好处是只需要在网关处集成swagger-ui,其他微服务不需要单独集成。你只需要收集swagger信息,并将接口暴露给网关,等待网关获取信息即可。但是并没有完全解决上述问题,同时也引入了新的问题。网关做文档聚合是否合理?网关本身是对外暴露的,这个接口文档可能会泄露给普通用户,个人认为在网关这样做不符合网关的定位。这种模式无法解决开发阶段的文档问题。开发阶段的文档会随时更新。该模式需要发布到官方环境查看文档或者需要在springsecurity中添加。白名单,放开swagger对外的restful接口,解决不了同一个项目不同文档的问题。对于这个问题,我想了想,尝试着换一种方式进行改造。集中注册模式嗯,名字是我瞎起的。具体技术架构如下图所示。系统流程如下:每个微服务启动时,从nacos、eureka等注册中心获取swagger注册中心服务的注册信息,然后调用swagger注册中心的接口将swagger信息保存到数据库中swagger注册中心集成了knife4j,也是一个单独的微服务。它连接到数据库并管理swagger文档。用户只能在内网访问swagger注册中心。swagger注册中心从数据库中取出swagger文档信息,通过knife4j进行渲染。需要注意的是,swagger注册中心只部署开发环境或者公司的局域网环境,我们公司的局域网可以直接访问开发环境。集中注册模式的代码设计如下。这里有两个独立的项目名称函数swagger-spring-boot-starter客户端组件。微服务客户端使用封装的组件扫描项目中的swagger信息并上传到swagger注册中心swagger-register-serverswagger注册中心接收微服务客户端上传的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组合键搜索这个关键字很容易找到相关代码(springfox2.9.2):springfox.documentation.swagger2.web.Sw??agger2Controller#getDocumentation这段代码详细解释了如何获取Swagger对象,这里为我的实现提供了很好的参考。/swagger-resources接口源码分析网关聚合方式查看swagger文档时,会发现前端会向后端请求一个接口获取所有组信息:/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()方法。这时候自由度就更大了,这里可以直接使用从数据库读取的方式获取所有的分组。返回值分析/swagger-resources接口的返回值是一个List类型,SwaggerResource类的定义如下:name:显示的名称url:前端根据url获取swagger文档的详细信息(默认是/v2/api-docs,其实可以修改这个值让swagger-ui请求自定义接口获取swagger文档)swaggerVersion:就是swagger的版本,一般是2.0。在继续学习之前先来张图感受下,方便大家理解:注册中心项目源码:https://gitee.com/kdyzm/swagger-register-server是swagger注册center,持久化swagger文档并进行CRUD操作,最后在knife4j中展示。应该包含以下功能从客户端接收swagger文档信息并保存到数据库集成knife4j并展示文档提供knife4j前端页面/swagger-resources接口逻辑实现提供knife4j前端页面获取文档详情接口即可动态更新文档表结构设计设计以上,使用两张表分别存储组信息和文档明细CREATETABLE`group_info`(`id`int(11)NOTNULLAUTO_INCREMENTCOMMENT'自增主键',`name`varchar(64)NOTNULLCOMMENT'groupName',`location`varchar(128)NOTNULLCOMMENT'location',`version`varchar(16)NOTNULLCOMMENT'version',`url`varchar(128)NOTNULLCOMMENT'url',`app_name`varchar(64)DEFAULTNULLCOMMENT'服务名称(spring.application.name)',`gateway`varchar(64)DEFAULTNULLCOMMENT'网关,留空',PRIMARYKEY(`id`),UNIQUEKEY`group_info_name`(`name`)COMMENT'组名唯一',UNIQUEKEY`group_info_app_name`(`app_name`)COMMENT'appnameunique')ENGINE=InnoDBDEFAULTCHARSET=utf8mb4;CREATETABLE`swagger_json`(`id`int(11)NOTNULLAUTO_INCREMENT,`group_name`varchar(64)NOTNULL,`content`longtextNOTNULLCOMMENT'swaggerspecificinformation',PRIMARYKEY(`id`),UNIQUEKEY`swagger_json_groupname`(`group_name`)COMMENT'groupNameunique')ENGINE=InnoDBDEFAULTCHARSET=utf8mb4;group_info表用于存放swagger的group信息,/swagger-resources接口会从该表中取出group数据swagger_json表用于存放swagger的原始信息,用于文档渲染。注册接口swagger-register-server中SwaggerRegisterController的regist()方法对应上面两个表,注册接口有两个实体类。注册逻辑是:存在则更新,不存在则添加,groupName和appName必须保持唯一。swagger-register-server接口中SwaggerRegisterController的getSwaggerDetail()方法获取swagger详情的默认值为/v2/api-docs,但可以自定义。这里要求客户端注册时同意接口路径为/swagger/detail。该接口从数据库中获取swagger信息。获取资源列表的接口在之前的/swagger-resources源码中已经分析过了。如果要从数据库中自定义组列表,需要重新实现SwaggerResourcesProvider接口,标记为@Primaryswagger-register-serverDocumentationConfig创建starter项目源码:https://gitee.com/kdyzm/swagger-spring-boot-starter设计要求微服务客户端只需要导入组件jar包,然后在配置文件中配置一些基本的swagger信息即可。服务启动后,swagger可以自动上传文件到swagger注册中心,具体技术细节,应该包括以下功能,实现swagger文件的完整上传,其效果和直接请求本地/v2/一样api-docs支持服务发现swagger注册中心和swagger注册中心url配置方式客户端可以通过springbootstarter的方式自动配置,实现无代码侵入的效果。swagger-spring-boot-starter客户端组件也兼容eureka和nacossswagger文档的扫描上传。使用上面分析的/v2/api-docs的实现原理。其实现原理很容易获取Swagger对象swagger-spring-boot-starter中SwaggerMvcGenerator的getSwagger()方法并上传。根据配置文件中是否配置了serverUrl,决定使用服务发现方式还是直接请求方式上传Swagger信息swagger-spring-boot-starterspringbootstarter支持的SwaggerRegistryService的registry()方法这个很简单,只需要在resources/META-INF目录下新建一个文件并配置即可。兼容注册表swagger-spring-boot-starter不依赖nacos客户端或者eurka??客户端,而是依赖它们的公共接口模块spring-cloud-commons。其实nacosclient或者eurekaclient都是这个模块的具体实现,所以swagger-spring-boot-starter可以兼容两个client服务发现组件的实现,但是由于服务端依赖于某个服务发现组件,在我的案例这里默认使用nacos。如果要使用eureka,需要自己修改。本文介绍的两个项目源码地址:项目名称项目地址swagger-register-serverhttps://gitee.com/kdyzm/swagger-register-serverswagger-spring-boot-starterhttps://gitee。com/kdyzm/swagger-spring-boot-starter启动swagger注册中心。本项目需要连接mysql数据库和nacosnacos。我搭建了一个可以直接使用的在线版本(这里不提供管理账号密码),nacos在线地址:nacos.kdyzm.cnmysql需要自己创建数据库,运行脚本创建相关的数据库和表结构,以及初始化一些数据。脚本地址: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私服,直接运行命令mvncleaninstall将jar包安装到本地maven仓库使用即可。创建一个测试项目,可以使用intelij自带的工具初始化一个springboot项目。这里使用2.3.4.REALEASE版本的springboot版本号(经过测试发现nacos版本号过高会导致服务发现功能失效,版本号低一些程序功能会更稳定)。使用intilij自带的springinitializer工具可以方便快捷的搭建一个web开发框架。写好Controller接口后,开始集成swagger-spring-boot-starter。测试项目地址源码:https://gitee.com/kdyzm/swagger-spring-boot-starter-test第一步:引入依赖com.kdyzmswagger-spring-boot-starter1.0-SNAPSHOT第二步:配置swagger信息在配置文件中添加新的配置swagger:config:#Everyone只关心自己的包名,方便与前端文档对接-url指定,会优先用于注册swagger文档信息;如果不指定,后面会使用服务发现方式server-url:http://swagger.kdyzm.cn#swgger注册中心serviceId,即servername,用于服务发现方式service-id:swagger-register-server步骤3:激活只是前两步,不会对项目有任何影响,也不会生成swagger文件,swaggerprofile必须激活后才能生效。如果项目启动后没有报错,打开文档地址:http://swagger.kdyzm.cn/doc.html查看文档上传效果。其他问题公共地址问题包括swagger注册中心地址服务名域名访问地址nacos地址nacos.kdyzm.cnhttp://nacos.kdyzm.cn/nacos(不提供管理账号密码)eureka地址eureka.kdyzm.cnhttp:///eureka.kdyzm.cn(免账号密码访问)swagger注册中心地址swagger.kdyzm.cnhttp://swagger.kdyzm.cn/doc.html(免账号密码访问)由于资源和网络带宽有限,访问speed会比较慢;请善待公共资源,不要对其进行压力测试等异常操作。模式切换配置文件中有一个配置项:swagger.config.server-url,如果配置项不为空,则进入直连模式,即直接请求server-url上传swagger没有服务发现的文档;如果没有配置该配置项,查看swagger.config.service-id字段。如果该字段没有配置值,则会报错,并跳过swagger文档上传。配置唯一性为了能够在组内唯一区分,appName和name必须保持唯一性,上传文件后不支持删除。如果误上传到swagger.kdyzm.cn,请发邮件给我,我会删除。我的邮箱地址:kdyzm@foxmail.com源码本来是分成两个独立的项目,维护起来不是很方便。项目名称项目地址swagger-register-serverhttps://gitee.com/kdyzm/swagger-register-serverswagger-spring-boot-starterhttps://gitee.com/kdyzm/swagger-spring-boot-starter所以现在加入实战个案,并将它们放在同一个项目中进行管理。三合一项目地址:项目地址gitee地址https://gitee.com/kdyzm/swagger-knife4j-spring-boot-startergithub地址https://github.com/kdyzm/swagger-knife4j-spring-boot-starterlater所有的更新都会放到这个项目中。