【Spring Boot】第23課-利用 Swagger 產生 API 文件

當系統開發出許多 Web API,想要開放給其他人串接時,我們可能會整理出文件,列出 API 的規格及使用說明。然而維護這種文件也是一種成本,若沒有持續地更新,就會發生遺漏或不準確的情形。

本文會引進叫做「Swagger」的工具,它能夠偵測程式專案中定義的 RESTful API,自動產生美觀的 API 文件。這份文件會在網頁上供人觀看,甚至能直接在上面實際呼叫,十分便利。

本文的練習用專案:
https://github.com/ntub46010/SpringBootTutorial/tree/Ch23-start

一、Swagger 介面介紹

下圖是中央氣象局開放資料平台的 API 文件,本文要產生的就是類似這樣子的文件。

首先請在程式專案匯入 Swagger 的函式庫。
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.5.9</version>
</dependency>

接著啟動 Spring 後,前往以下網址,就能看見 Swagger 所產生的文件了
http://localhost:8080/swagger-ui/index.html?configUrl=/v3/api-docs/swagger-config

點開其中一個 API,除了能看見其規格,透過右上方的「Try it out」按鈕,我們還能實際呼叫它。附帶一提,有些 API 需要在請求標頭(request header)攜帶 access token 才能呼叫,這部份我們留到第六節再來處理。

然而 Swagger 只會在文件自動產生 API 的 HTTP 方法、路徑、請求與回應格式等內容,並沒有其他文字說明。這可能會讓其他想串接 API 的人在閱讀上不容易了解。若想在文件提供更詳細的說明,還是得自行撰寫。因此接下來的小節,我們就來補充一些內容。

二、對 API 做簡介

透過 @Operation 標記,可以對 API 進行簡介。本節會介紹該標記的其中三個參數。summary 參數是對 API 進行簡短的介紹;description 則是進行更詳細的說明。以下的範例程式是對「GET /users/{id}」的 API進行簡介,其用途是透過 ID 查詢使用者。

@RestController
@RequestMapping(value = "/users", produces = MediaType.APPLICATION_JSON_VALUE)
public class AppUserController {
// 其餘略過
@Operation(
summary = "Query specific user.",
description = "Query specific user info according to user id.")
@GetMapping("/{id}")
public ResponseEntity<AppUserResponse> getUser(@PathVariable("id") String id) {
// 其餘略過
}
}

下圖是在 Swagger 顯示的結果。summary 會顯示在列表上,而 description 需要將列表項目展開才看得到。

接下來描述這個 API 會回傳的狀態碼。在 @Operation 標記的 responses 參數添加 @ApiResponse 標記,就能進行說明。假設已知此 API 回傳的狀態碼包含200、403與404,那我們就可將其紀錄在標記中。

@Operation(
summary = "Query specific user.",
description = "Query specific user info according to user id.",
responses = {
@ApiResponse(
responseCode = "200",
description = "Get user info successfully"),
@ApiResponse(
responseCode = "403",
description = "Only authenticated user can get user info.",
content = @Content),
@ApiResponse(
responseCode = "404",
description = "The user doesn't exist.",
content = @Content)
})
@GetMapping("/{id}")
public ResponseEntity<AppUserResponse> getUser(@PathVariable("id") String id) {
// 其餘略過
}

上面的範例程式,筆者在 @ApiResponse 標記中傳入了三個參數。responseCodedescription 分別是狀態碼與敘述,讀者可說明發生該狀態的可能原因。

至於 content 參數,則是描述 response body 的資料格式。預設為該 API 方法的回傳值,也就是 AppUserResponse。針對沒有 response body,或不需特別描述的狀態,可直接給予空的 @Content 標記。若讀者想針對不同狀態提供不同的 response body 格式,則可參考下面的做法,傳入額外的 schema 參數。

@ApiResponse(
responseCode = "404",
content = @Content(schema = @Schema(implementation = AppUserResponse.class)))

下圖是在 Swagger 顯示的結果。僅狀態碼200的區塊有顯示 response body 的格式,其他只有文字描述。


三、說明請求與回應的欄位

透過 @Schema 標記,可以對 request body 與 response body 的類別欄位進行說明。以下對「POST /users」的 API 進行示範,其用途是建立使用者。而 response 的欄位與 request 幾乎一樣,因此筆者就略過。

public class AppUserRequest {
@Schema(description = "The email address of user.", example = "vincent@gmail.com")
@NotBlank
private String emailAddress;

@Schema(description = "The password of user.", example = "123456", minLength = 6)
@NotBlank
private String password;

@Schema(description = "The full name of user.", example = "Vincent Zheng")
@NotBlank
private String name;

@Schema(description = "The authority of user.")
@NotEmpty
private List<UserAuthority> authorities;

// 略過 get 與 set 方法
}

該標記的 description 參數是用來解說欄位用途;example 參數是用來提供欄位值的範例;minLength 參數代表該欄位值的最小長度或數量。其中 password 欄位特別傳入 minLength 參數,提示最小長度為6個字。

下圖是在 Swagger 顯示的結果。在文件的「Response body」區塊,切換到「Schema」的頁籤,即可看到各個欄位的說明。

從上圖中,我們還會看到欄位名稱的右邊有紅色的星號,這是代表必填的意思。會出現星號,是因為 AppUserRequest 類別的欄位有加上第11課介紹的驗證標記,如 @NotBlank@NotEmpty 等,它們也是 Swagger 會掃描的地方。

然而有時因為其他考量,驗證 request body 資料的方式可能是另外處理,所以沒有加上像這樣的驗證標記。若讀者仍想將欄位必填的條件在文件表示出來,則可參考下面的做法,在 @Schema 傳入 required 參數。

@Schema(description = "The authority of user.", required = true)
private List<UserAuthority> authorities;

四、說明路徑參數與查詢字串

透過 @Parameter 標記,可以針對 API 資源路徑上的參數或後方的的查詢字串進行說明。以下對「GET /products/{id}」的 API 進行示範,其用途是透過 ID 獲取產品資料。在 Controller 中,我們對 @PathVariable 標記所接收的路徑參數 id 值進行說明。

@RestController
@RequestMapping(value = "/products", produces = MediaType.APPLICATION_JSON_VALUE)
public class ProductController {
// 其餘略過
@GetMapping("/{id}")
public ResponseEntity<ProductResponse> getProduct(
@Parameter(description = "ID of product.")
@PathVariable("id") String id) {
// 其餘略過
}
}

下圖是在 Swagger 顯示的結果。

以下對「GET /users」的 API 進行示範,其用途是查詢使用者,並可將使用者權限作為查詢條件。在 Controller 中,我們對 @RequestParam 標記所接收的查詢字串「authorities」進行說明。

@RestController
@RequestMapping(value = "/users", produces = MediaType.APPLICATION_JSON_VALUE)
public class AppUserController {
// 其餘略過
@GetMapping
public ResponseEntity<List<AppUserResponse>> getUsers(
@Parameter(description = "Define authorities that found user have at least one.", required = false, allowEmptyValue = true)
@RequestParam(name = "authorities", required = false) List<UserAuthority> authorities) {
// 其餘略過
}
}

該標記的 description 參數是用來描述用途;required 參數代表該查詢字串是否為必填;allowEmptyValue 參數代表傳送查詢字串時是否能給予空值。

下圖是在 Swagger 顯示的結果。按下「Try it out」按鈕後,會進入編輯 request 的模式,此處可看見「Send empty value」的核取方塊。

當勾選「Send empty value」再發出請求,將會送出查詢字串,但沒有值,即:
GET /api/users?authorities=

若從選單中挑選要送出的值,將會以逗號分隔的形式送出,即:
GET /api/users?authorities=ADMIN,NORMAL


五、從文件上隱藏 API 規格

有些 API 的規格我們可能不希望在文件上完全揭露出來。比方說某些 API 或 request body 的欄位是給內部人員使用的,並不希望被外部的人得知。基於安全上的考量,我們可以使用 @Hidden 標記,將 API 規格從 Swagger 文件中隱藏。

@Hidden 標記可以冠在類別、方法與類別欄位上。下面的範例是將標記冠在 MailController 類別,這會將該 Controller 中的所有 API 都隱藏。

@Hidden
@RestController
@RequestMapping(value = "/mail", produces = MediaType.APPLICATION_JSON_VALUE)
public class MailController {
// 其餘略過
@PostMapping
public ResponseEntity<Void> sendMail(@Valid @RequestBody SendMailRequest request) {
// 其餘略過
}
}

下面的範例是將標記冠在 AuthControllerparseToken 方法,這會將「POST /auth/parse」這個 API 從文件中隱藏。

@RestController
@RequestMapping(value = "/auth", produces = MediaType.APPLICATION_JSON_VALUE)
public class AuthController {
// 其餘略過
@Hidden
@PostMapping("/parse")
public ResponseEntity<Map<String, Object>> parseToken(@RequestBody Map<String, String> request) {
// 其餘略過
}
}

下面的範例是在 ProductRequest 新增 softDelete 欄位,並將標記冠在該欄位上。這會從有用到 ProductRequest 做為 request body 的 API,將 softDelete 欄位從文件中隱藏。

public class ProductRequest {
// 其餘略過
@Hidden
private boolean softDelete;

// 略過 get 與 set 方法
}

六、提供文件資訊與攜帶 Token

在 Swagger 文件頁面的最上方,可以看到預設的標題與版本號,即「OpenAPI Definition (v0)」。本節我們來提供正式一點的說明文字。請建立一個 Configuration 類別,筆者叫做 SwaggerConfig

下面的範例程式中建立了 OpenAPI 的元件,但它不會被注入到其他地方,而是會被 Swagger 掃描,並將其中的設定反映在文件上。

@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI openAPI() {
Info info = new Info()
.title("My Spring Boot API Document")
.version("Ver. 1.0.0")
.description("The document will list APIs we practice before.");
return new OpenAPI().info(info);
}
}

上面的程式設定了文件的標題、版本號與描述。下圖是在 Swagger 顯示的結果。

接著我們再做一項設定,讓發送請求時能夠在 header 攜帶 access token。

@Bean
public OpenAPI openAPI() {
// 其餘略過

String securitySchemeName = "JWT Authentication";
SecurityRequirement securityRequirement =
new SecurityRequirement().addList(securitySchemeName);
Components components = new Components()
.addSecuritySchemes(securitySchemeName,
new SecurityScheme()
.name(securitySchemeName)
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
);

return new OpenAPI()
.info(info)
.addSecurityItem(securityRequirement)
.components(components);
}

請參考以上範例程式,設置 SecurityRequirementComponents。開啟 Swagger 文件後,會在頁面上看見多出一個「Authorize」按鈕。

按下按鈕後,我們可以在彈出視窗的「Value」欄位中填入 JWT。再接續按下 Authorize 按鈕後,之後送出的請求就都會攜帶這個 JWT 了。

若之後不想攜帶 access token,只要按下「Logout」按鈕即可。

本文的完成範例:
https://github.com/ntub46010/SpringBootTutorial/tree/Ch23-fin

上一篇:【Spring Boot】第22.2課-RestTemplate 串接第三方服務實例

下一篇:【Spring Boot】第24課-使用 Controller Advice 處理例外與 query string

留言

  1. 謝謝作者所提供的內容
    跟著做完整個系列真的受益良多 謝謝

    回覆刪除
  2. 想問一下,我照著前兩步做後,頁面上跑出一堆我根本沒有做出來的api,是為什麼呢

    回覆刪除
    回覆
    1. 範例專案會包含前面文章中所實作的 API,並且就這麼累積到本文第 23 課
      你在 Swagger 文件中看到的 API 應該是來自專案中「現有」的 controller 喔!

      刪除

張貼留言