1. 概述
在 REST API 设计领域,Swagger 和 HATEOAS 是两种主流方案。两者都致力于提升 API 的易用性和可理解性,但遵循截然不同的设计理念。
本文将深入探讨两者的核心差异及典型应用场景。
2. 什么是 Swagger?
Swagger 是一套开源工具集,用于构建、文档化和消费 REST API。它允许开发者基于 OpenAPI 规范 (OAS) 使用 JSON/YAML 文件描述 API 结构。
2.1. 代码生成
Swagger 可自动生成交互式 API 文档、代码和客户端库。它能生成多语言的服务端存根和客户端 SDK,显著提升开发效率。
这是一种 API 优先 的设计模式,在需求与维护者之间建立契约关系。
开发者可通过 SwaggerHub 等工具,提供 Swagger 规范文件即可生成多种编程语言的模板代码。例如,下面是一个简单用户接口的 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: [] # 指定接口安全机制
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 指定预期的令牌类型
schemas:
User:
type: object
properties:
id:
type: integer
example: 1
name:
type: string
example: John Doe
email:
type: string
example: john.doe@example.com
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: john.doe@example.com
required:
- name
- email
快速解析 YAML 文件核心要素:
- 基础信息 (info):包含 API 标题、版本和简要描述
- 路径定义:
- GET /users:获取所有用户,返回状态码 200 和用户对象数组
- POST /users:创建新用户,请求体需符合 NewUser 模式,返回状态码 201 和创建的用户对象
- ***GET /users/{id}***:根据 ID 获取用户,包含 404 响应对应用户不存在的情况
- 组件定义:
- User 模式:定义用户对象结构,包含 id、name、email 和 createdAt 字段
- NewUser 模式:用于创建用户的请求体,要求必填 name 和 email 字段
- 安全方案:定义 API 安全机制,此处使用 Bearer 令牌(通常是 JWT)
通过这种方式,我们可以定义 API 的几乎所有细节,并自动生成主流语言的实现代码,大幅提升开发效率。
2.2. API 文档集成
我们也可以直接在项目代码中应用 OpenAPI 文档注解。无论是自动生成还是手动添加,下面展示 Java Spring REST 应用中用户接口的实现:
@RestController
@RequestMapping("/api/users")
public class UserController {
// 字段和构造器
@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 契约或文档模板?
✅ 统一性:模板确保所有接口遵循一致结构,简化内部团队和外部使用者的理解和使用
✅ 协作效率:开发者、QA 工程师和外部利益相关者能清晰共享对 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;
// 构造器、Getter 和 Setter
}
用户接口的实现方式:
@RestController
@RequestMapping("/api/hateoas/users")
public class UserHateoasController {
// 字段和构造器
@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": "john.doe@example.com",
"createdAt": "2023-01-01T12:00:00",
"_links": {
"self": {
"href": "http://localhost:8080/users/1"
}
}
},
{
"id": 2,
"name": "Jane Smith",
"email": "jane.smith@example.com",
"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", "john.doe@example.com", LocalDateTime.now());
User user2 = new User(2, "Jane Smith", "jane.smith@example.com", 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 获取用户:
@Test
void whenUserByIdRequested_thenReturnUserByIdWithLinks() throws Exception {
User user = new User(1, "John Doe", "john.doe@example.com", 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("john.doe@example.com"))
.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", "john.doe@example.com", 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\":\"john.doe@example.com\"}"))
.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 可探索性的终极方案,使客户端能动态理解 API 行为。
4. Swagger 与 HATEOAS 的核心差异
对比维度 | Swagger | HATEOAS |
---|---|---|
API 文档 | 提供详细的可读文档和 UI,消费者可预先了解接口、参数和响应 | 依赖响应中的超媒体链接,文档更隐式,消费者通过链接动态发现操作 |
客户端实现 | 基于规范生成或编写客户端,API 结构预知,按预定义路径请求 | 动态交互,通过响应中的超媒体链接发现操作,无需预知完整 API 结构 |
灵活性 | 较为刚性,依赖预定义接口,变更需更新文档/规范 | 更灵活,通过超媒体响应演进 API,不易破坏现有客户端 |
消费者易用性 | ✅ 易于使用,依赖自动生成文档或工具生成客户端代码 | ❌ 更复杂,需解析响应并跟踪超媒体链接发现操作 |
API 演进 | 结构变更需更新规范、重新生成客户端代码并分发 | ✅ 客户端通过超媒体发现 API,演进时更新需求较少 |
版本管理 | 需显式版本控制,维护多个独立 API 版本 | ✅ 无需严格版本控制,客户端可动态跟踪链接 |
HATEOAS 通过响应中的超媒体链接动态指导客户端交互,而 Swagger(OpenAPI)提供静态的、人机可读的 API 文档,描述 API 结构、接口和操作。
5. 总结
本文通过实际案例对比分析了 Swagger 和 HATEOAS 的核心差异。我们展示了如何从 YAML 模板生成源码,或使用 Swagger 注解装饰接口;对于 HATEOAS,则演示了如何通过添加导航链接优化模型定义。
本文代码示例可在 GitHub 获取。