魔改swagger:knife4j的另外一種開啟方式

狂盜一枝梅發表於2021-10-16

之前公司使用了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處做了文件聚合,它的聚合模式如下圖所示

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

image-20211014144112775

這段程式碼詳細講解了如何獲取Swagger物件,這給我的實現提供了很大的參考依據。

image-20211014144631384

2、/swagger-resources介面

2.1 原始碼解析

在通過閘道器聚合模式下檢視swagger文件的時候,會發現前端會請求後端一個介面獲取所有的group資訊:/swagger-resources,老規矩,還是ctrl+shift+f快捷鍵全域性查詢,可以看到相關程式碼的實現

springfox.documentation.swagger.web.ApiResourceController#swaggerResources

image-20211014151658646

可以看到,改介面僅僅是呼叫了swaggerResource的get方法,然後就直接返回了,那就再看看swaggerResource是什麼東西

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

image-20211014151922637

它只是個介面,那它的實現類呢,它的實現類只有一個,就是InMemorySwaggerResourcesProvider類

image-20211014152125219

它的GET方法是這樣子的

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

image-20211014152220534

看到這裡我不禁陷入了思考,難道要給documentationCache手動填充文件。。但是看這個名字就知道是基於記憶體的東西,要維護CRUD狀態似乎有點麻煩。。看看這個程式碼是咋寫的

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

image-20211014152532816

確實是基於記憶體的東西,但是值提供了add方法,沒提供remove方法,那獲取到documentionLookup物件之後手動移除呢?仔細看看all()方法

image-20211014152721597

它被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

image-20211014153418621

  • 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

歡迎大家收藏訪問~

相關文章