實戰!魔改 Swagger,Knife4j的另外一種打開方式
哈嘍大家好,我是阿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-register-server | ? |
在一切開始之前,需要了解下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類的定義如下
- 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;
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注冊中心
該項目啟動需要連接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
第一步:引入依賴
<!-- 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文檔,必須激活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 | |
swagger-spring-boot-starter |
所以現在再加上實戰案例放到同一個項目中進行管理。
三合一項目地址:
項目 | 地址 |
gitee地址 | |
github地址 | https://github.com/kdyzm/swagger-knife4j-spring-boot-starter |
以后的更新均會放到該項目中進行。