Swagger 和 HATEOAS 之間的區別
1. 概述
經常使用兩種流行的 REST API 設計方法:Swagger 和 HATEOAS。兩者都旨在使 API 更加用戶友好和易於理解,但遵循不同的範例。
在本教程中,我們將了解 Swagger 和 HATEOAS 之間的差異以及一些常見用例。
2. 什麼是招搖?
Swagger 是一組用於建置、記錄和使用 REST API 的開源工具。它允許開發人員使用基於OpenAPI 規範(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**
:檢索所有用戶,傳回帶有User
物件陣列的200
回應。 -
**POST /users**
:它會建立一個新使用者。它需要具有NewUser
架構的請求正文,並傳回包含建立的使用者物件的201
回應。 -
**GET /users/{id}**
:透過 ID 檢索特定使用者。如果未找到User
,則包含404
回應
-
- 成分:
-
User
模式:定義使用者物件的結構,包括id
、name
、email
和createdAt
等欄位。 -
NewUser
schema :在請求正文中用於建立新用戶,需要name
和email
欄位。 - SecuritySchemes :此部分定義 API 如何處理安全性。在這種情況下,我們指定一個
bearerAuth
方案,該方案在 API 安全性上下文中使用 Bearer 令牌,通常是 JWT(JSON Web 令牌)。
-
我們可以定義幾乎所有有關 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 範例。
2.4. API 優先方法的好處
為什麼我們應該使用獨特的 API 合約或文件範本?
範本可確保 API 上的所有端點都遵循統一的結構。這種一致性簡化了內部開發團隊和外部消費者對 API 的理解和使用。例如,開發人員、QA 工程師和外部利害關係人對 API 的功能和結構有清晰、共同的理解。
此外,客戶可以直接在文件中試驗 API,從而使 API 更易於採用和集成,而無需廣泛的額外支援。我們可以設定自動化測試來確保 API 的結構和回應符合規格。
3. HATEOAS 是什麼?
HATEOAS (超媒體作為應用程式狀態引擎)是 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
端點的範例回應,我們可以在其中透過連結動態發現User's
操作和相關資源:
[
{
"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());
}
我們現在期望id
引用的所有用戶都存在於回應中。
最後,在我們建立新用戶後,我們還希望新的引用出現在回應中:
@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 之間的差異:
方面 | 昂首闊步 | 哈特奧阿斯 |
---|---|---|
API文件 | Swagger 透過 UI 提供了詳細的、人類可讀的 API 文檔,使消費者能夠提前了解可用的端點、請求參數和回應。 | HATEOAS 依賴伺服器在回應中返回的超媒體鏈接,這意味著文件更加隱式。因此,消費者透過這些連結而不是預先產生的 UI 動態發現操作。 |
客戶端實施 | 客戶端通常是根據 Swagger 規範產生或編寫的。 API的結構是事先已知的,客戶端可以根據預先定義的路徑發出請求。 | HATEOAS 用戶端與 API 動態交互,透過回應中的超媒體連結發現可用的操作。客戶端不需要事先知道完整的API結構。 |
靈活性 | Swagger 更加嚴格,需要預先定義的端點和一致的 API 結構。這使得在不更新文件或規範的情況下發展 API 變得更加困難。 | HATEOAS 提供了更大的靈活性,讓 API 透過更改超媒體驅動的回應來發展,而不會破壞現有客戶端。 |
消費者輕鬆 | 對於依賴自動產生的文件或直接根據 API 規範建立客戶端程式碼的工具的消費者來說,這很容易。 | 對於消費者來說,情況更加複雜,因為他們需要解釋回應並追蹤超媒體連結以發現進一步的操作。 |
API演進 | API 結構的任何變更都需要更新 Swagger 規範、重新產生客戶端程式碼並將其分發給使用者。 | 當客戶端透過超媒體發現 API 時,HATEOAS 允許更輕鬆地進行更改,在 API 發展時需要更少的更新。 |
版本控制 | Swagger 通常需要明確版本控制並分別維護多個版本的 API。 | HATEOAS 的發展無需嚴格的版本控制,因為用戶端可以動態地追蹤所提供的連結。 |
HATEOAS 專注於使用回應中嵌入的超媒體連結透過 API 互動動態引導用戶端。同時,Swagger(或 OpenAPI)提供靜態、人類可讀和機器可讀的 API 文檔,描述 API 的結構、端點和操作。
5. 結論
在本文中,我們了解了 Swagger 和 HATEOAS,並透過一些應用範例突出了主要差異。我們了解如何從 YAML 範本產生原始程式碼或使用 Swagger 註解來裝飾我們的端點。對於 HATEOAS,我們了解如何透過添加有價值的連結來導航與端點相關的所有資源來改進模型定義。
與往常一樣,程式碼可以在 GitHub 上取得。