1. 概述
以下是兩種流行的 REST API 設計方法,常被使用:Swagger 和 HATEOAS。兩者都旨在使 API 更易於使用和理解,但遵循不同的範式。
在本教程中,我們將探討 Swagger 和 HATEOAS 之間的差異,以及一些常見的用例。
2. Swagger 是什麼?
Swagger 是一套開源工具,用於構建、文檔和消費 REST API。它允許開發者使用基於 OpenAPI Specification (OAS) 的 JSON 或 YAML 文件來描述其 API 的結構。
讓我們來看看 Swagger 的主要功能。
2.1. 代碼生成
使用 Swagger,我們可以自動生成交互式 API 文檔、代碼和客户端庫。Swagger 還可以創建各種編程語言的服務器樁和客户端 SDK,從而加速開發。它是一種 API-優先方法,定義了需求與應用程序維護者之間的合同。
開發者可以使用諸如 SwaggerHub 等工具,通過提供 Swagger 規範文件來為不同的編程語言創建樣板代碼。例如,讓我們來看一個簡單的 User 端點的 YAML 模板:
openapi: 3.0.1
info:
title: User API
version: "1.0.0"
description: API for managing users.
paths:
/users:
get:
summary: Get all users
security:
- bearerAuth: [] # Specifies security for this endpoint
responses:
'200':
description: A list of users.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
'401':
description: Unauthorized - Authentication required
'500':
description: Server error
post:
summary: Create a new user
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NewUser'
responses:
'201':
description: User created successfully.
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'400':
description: Invalid input
'401':
description: Unauthorized - Authentication required
'500':
description: Server error
/users/{id}:
get:
summary: Get user by ID
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: integer
example: 1
responses:
'200':
description: User found.
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'401':
description: Unauthorized - Authentication required
'404':
description: User not found
'500':
description: Server error
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT # JWT specifies the type of token expected
schemas:
User:
type: object
properties:
id:
type: integer
example: 1
name:
type: string
example: John Doe
email:
type: string
example: [email protected]
createdAt:
type: string
format: date-time
example: "2023-01-01T12:00:00Z"
NewUser:
type: object
properties:
name:
type: string
example: John Doe
email:
type: string
example: [email protected]
required:
- name
- email
讓我們來了解一下 YAML 文件的內容:
- 通用信息 (info): API 的標題、版本和簡短描述包含在內。
- 路徑:
- GET /users: 檢索所有用户,返回一個 200 響應,包含 User 對象數組。
- POST /users: 創建一個新的用户。它期望帶有 NewUser 模式的請求主體,並返回一個 201 響應,包含創建的用户對象。
- GET /users/{id}: 通過 ID 檢索特定用户。如果未找到 User 對象,則返回 404 響應。
- 組件:
- User 模式: 定義用户對象的結構,包括字段如 id、name、email 和 createdAt。
- NewUser 模式: 用於創建新用户的請求主體,需要 name 和 email 字段。
- SecuritySchemes: 此部分定義 API 如何處理安全。在本例中,我們指定了一個 bearerAuth 方案,該方案使用 Bearer 令牌,通常是 JWT(JSON Web Tokens),在 API 安全上下文中。
我們可以定義 API 中幾乎所有內容,並自動生成它,從而加快這一部分流程。
2.2 API 文檔
我們可以直接將 Open API 文檔標籤應用到我們項目的代碼中。無論是通過自動生成還是手動標記,讓我們來看一下用户端點在 Java Spring REST 應用中的外觀:
@RestController
@RequestMapping("/api/users")
public class UserController {
// fields and constructor
@Operation(summary = "Get all users", description = "Retrieve a list of all users")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "List of users",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = User.class))),
@ApiResponse(responseCode = "500", description = "Internal server error") })
@GetMapping
public ResponseEntity<List<User>> getAllUsers() {
return ResponseEntity.ok()
.body(userRepository.getAllUsers());
}
@Operation(summary = "Create a new user", description = "Add a new user to the system")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "User created",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = User.class))),
@ApiResponse(responseCode = "400", description = "Invalid input") })
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<User> createUser(
@RequestBody(description = "User data", required = true,
content = @Content(schema = @Schema(implementation = NewUser.class))) NewUser user) {
return new ResponseEntity<>(userRepository.createUser(user), HttpStatus.CREATED);
}
@Operation(summary = "Get user by ID", description = "Retrieve a user by their unique ID")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "User found",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = User.class))),
@ApiResponse(responseCode = "404", description = "User not found") })
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Integer id) {
return ResponseEntity.ok()
.body(userRepository.getUserById(id));
}
}讓我們來查看一些最重要的註釋:
- @Operation: 它為每個API操作添加摘要和描述,幫助説明端點的作用和用途。
- @ApiResponse: 它定義了針對HTTP狀態碼的單個響應,包括描述和預期內容類型和模式。
- @Content: 指定響應或請求體的內容類型(例如, application/json)並提供數據序列化的模式。
- @Schema: 描述請求和響應體的模型,將類(如User)與Swagger中顯示的JSON結構關聯起來。
2.3. 交互式控制枱
Swagger UI 控制枱是一個交互式、基於 Web 的界面,它能夠從 OpenAPI 規範動態生成文檔。它允許開發者和 API 消費者以可視化的方式探索和測試端點。控制枱以用户友好的佈局顯示 API 端點、請求參數、響應和錯誤代碼。
每個端點都提供輸入參數值、標頭和請求正文的字段,從而使用户能夠直接從控制枱發出實時請求。這種功能有助於開發者理解 API 行為、驗證集成並解決問題,而無需使用單獨的工具,使其成為 API 開發和測試的必備資源。例如,我們可以查看 Swagger UI 示例,該示例針對寵物商店:https://petstore.swagger.io/。
2.4. 使用 API 第一方法的好處
我們為什麼應該為文檔中使用唯一的 API 契約或模板嗎?
模板確保 API 中的所有端點都遵循統一的結構。這種一致性簡化了對 API 的理解和使用,既對內部開發團隊,也對外部消費者來説都是如此。例如,開發人員、質量保證工程師和外部利益相關者都有一個清晰、共享的 API 能力和結構理解。
此外,客户可以在文檔中直接試驗 API,從而使 API 更容易採用和集成,而無需大量的額外支持。 我們可以設置自動測試,以確保 API 的結構和響應符合規範。
3. 什麼是 HATEOAS?
HATEOAS(Hypermedia as the Engine of Application State)是 REST 應用程序架構的約束。 它屬於更廣泛的 REST 範式,強調客户端完全通過服務器動態提供的超媒體與 REST API 交互。 在 HATEOAS 中,服務器在響應中包含鏈接,指導客户端採取下一步操作。
3.1. HATEOAS 示例
讓我們來看一個 Spring HATEOAS 應用的示例。首先,我們需要定義 User 作為特定表示模型的一部分:
public class User extends RepresentationModel<User> {
private Integer id;
private String name;
private String email;
private LocalDateTime createdAt;
// Constructors, Getters, and Setters
}現在,讓我們來看一個如何為用户端點實施的示例:
@RestController
@RequestMapping("/api/hateoas/users")
public class UserHateoasController {
// fields and constructor
@GetMapping
public CollectionModel<User> getAllUsers() {
List<User> users = userService.getAllUsers();
users.forEach(user -> {
user.add(linkTo(methodOn(UserController.class).getUserById(user.getId())).withSelfRel());
});
return CollectionModel.of(users, linkTo(methodOn(UserController.class).getAllUsers())
.withSelfRel());
}
@GetMapping("/{id}")
public EntityModel<User> getUserById(@PathVariable Integer id) {
User user = userService.getUserById(id);
user.add(linkTo(methodOn(UserController.class).getUserById(id)).withSelfRel());
user.add(linkTo(methodOn(UserController.class).getAllUsers()).withRel("all-users"));
return EntityModel.of(user);
}
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<EntityModel<User>> createUser(@RequestBody NewUser user) {
User createdUser = userService.createUser(user);
createdUser.add(
linkTo(methodOn(UserController.class).getUserById(createdUser.getId())).withSelfRel());
return ResponseEntity.created(
linkTo(methodOn(UserController.class).getUserById(createdUser.getId())).toUri())
.body(EntityModel.of(createdUser));
}
}讓我們來看一個 getAllUsers 終點的示例響應,通過鏈接,我們可以動態地發現用户的行為和相關資源:
[
{
"id": 1,
"name": "John Doe",
"email": "[email protected]",
"createdAt": "2023-01-01T12:00:00",
"_links": {
"self": {
"href": "http://localhost:8080/users/1"
}
}
},
{
"id": 2,
"name": "Jane Smith",
"email": "[email protected]",
"createdAt": "2023-02-01T12:00:00",
"_links": {
"self": {
"href": "http://localhost:8080/users/2"
}
}
}
]
3.2. 測試
為了更深入地瞭解,讓我們來看一些針對控制器的集成測試。
首先,讓我們獲取所有用户:
@Test
void whenAllUsersRequested_thenReturnAllUsersWithLinks() throws Exception {
User user1 = new User(1, "John Doe", "[email protected]", LocalDateTime.now());
User user2 = new User(2, "Jane Smith", "[email protected]", LocalDateTime.now());
when(userService.getAllUsers()).thenReturn(List.of(user1, user2));
mockMvc.perform(get("/api/hateoas/users").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$._embedded.userList[0].id").value(1))
.andExpect(jsonPath("$._embedded.userList[0].name").value("John Doe"))
.andExpect(jsonPath("$._embedded.userList[0]._links.self.href").exists())
.andExpect(jsonPath("$._embedded.userList[1].id").value(2))
.andExpect(jsonPath("$._embedded.userList[1].name").value("Jane Smith"))
.andExpect(jsonPath("$._links.self.href").exists());
}在這種情況下,我們期望從檢索到的每個 User 都有一個相對路徑,通過 id 確定。
下面我們來看一下通過 id 獲取 User 的端點:
@Test
void whenUserByIdRequested_thenReturnUserByIdWithLinks() throws Exception {
User user = new User(1, "John Doe", "[email protected]", LocalDateTime.now());
when(userService.getUserById(1)).thenReturn(user);
mockMvc.perform(get("/api/hateoas/users/1").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("John Doe"))
.andExpect(jsonPath("$.email").value("[email protected]"))
.andExpect(jsonPath("$._links.self.href").exists())
.andExpect(jsonPath("$._links.all-users.href").exists());
}我們現在期望所有用户通過 引用存在於響應中。
最後,在創建新用户後,我們也期望新引用存在於響應中:
@Test
void whenUserCreationRequested_thenReturnUserByIdWithLinks() throws Exception {
User user = new User(1, "John Doe", "[email protected]", LocalDateTime.now());
when(userService.createUser(any(NewUser.class))).thenReturn(user);
mockMvc.perform(post("/api/hateoas/users").contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"John Doe\",\"email\":\"[email protected]\"}"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("John Doe"))
.andExpect(jsonPath("$._links.self.href").exists());
}3.3. 關鍵要點
正如我們所見,HATEOAS API 包含在響應中提供的鏈接,引導客户端的行為。這減少了客户端需要硬編碼端點路由的需求,並使客户端能夠更靈活地與 API 交互。
同樣,它為客户端提供了一種通過服務器提供的鏈接來動態地導航各種狀態或操作的方式,從而實現更適應性的工作流程。因此,我們可以將 HATEOAS 視為最終的步驟,使 API 更易於探索,以便客户端能夠理解其行為。
4. Swagger 和 HATEOAS 的關鍵差異
讓我們説明一下 Swagger 和 HATEOAS 的區別:
| 方面 | Swagger | HATEOAS |
|---|---|---|
| API 文檔 | Swagger 提供詳細、可讀的 API 文檔,帶有 UI,允許消費者提前瞭解可用的端點、請求參數和響應。 | HATEOAS 依賴於服務器在響應中返回的超媒體鏈接,因此文檔更具隱性。消費者通過這些鏈接動態地發現操作,而不是依賴預生成的 UI。 |
| 客户端實現 | 客户端通常基於 Swagger 規範生成或編寫。API 的結構在先,客户端可以根據預定義的路徑進行請求。 | HATEOAS 客户端通過響應中的超媒體鏈接與 API 動態交互,無需事先了解 API 的完整結構。 |
| 靈活性 | Swagger 更加僵化,期望預定義的端點和一致的 API 結構。這使得在不更新文檔或規範的情況下演化 API 變得更加困難。 | HATEOAS 提供了更大的靈活性,允許 API 通過改變超媒體驅動的響應而演化,而無需破壞現有客户端。 |
| 易用性 | 對於依賴自動生成的文檔或直接從 API 規範創建客户端代碼的消費者來説,很容易。 | 對於消費者來説,由於他們需要解釋響應並遵循超媒體鏈接以發現進一步的操作,因此更加複雜。 |
| API 演化 | API 結構中的任何更改都需要更新 Swagger 規範、重新生成客户端代碼並將其分發給用户。 | HATEOAS 允許更輕鬆的更改,因為客户端通過超媒體來發現 API,因此在 API 演化時需要更少的更新。 |
| 版本控制 | Swagger 通常需要明確的版本控制以及維護 API 的多個版本。 | HATEOAS 在客户端通過鏈接動態地發現 API 時,版本控制更加靈活,不需要在 API 演化時進行嚴格的版本控制。 |
HATEOAS 專注於使用響應中嵌入的超媒體鏈接動態地引導客户端通過 API 交互。與此同時,Swagger (或 OpenAPI) 提供靜態、可讀的、可供機器讀取的 API 文檔,描述 API 的結構、端點和操作。
5. 結論
在本文中,我們學習了 Swagger 和 HATEOAS,並通過一些應用程序示例強調了它們的主要區別。我們看到了如何從 YAML 模板生成源代碼,或者使用 Swagger 註解來裝飾我們的端點。對於 HATEOAS,我們看到了如何通過添加有價值的鏈接來改進模型定義,從而導航所有與端點相關的資源。