1、概览 {#1概览}
设计 REST API 通常使用两种流行的方法:Swagger 和 HATEOAS。这两种方法都旨在使 API 更友好、更易懂,但遵循不同的范式。
本文将带你了解 Swagger 和 HATEOAS 的区别以及一些常见用例。
2、Swagger 是什么? {#2swagger-是什么}
Swagger 是一套用于构建、记录和使用 REST API 的开源工具。它允许开发人员使用基于 OpenAPI Specification(OAS) 的 JSON 或 YAML 文件来描述其 API 的结构。
来看看 Swagger 的主要功能。
2.1、代码生成 {#21代码生成}
有了 Swagger,我们可以自动生成交互式 API 文档、代码和客户端库。Swagger 还可以用各种编程语言创建服务器存根(Stub)和客户端 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: [] # 端点的安全设置
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: johndoe@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: johndoe@example.com
required:
- name
- email
上述 YAML 文件:
- 一般信息(info): 包括 API 标题、版本和简要说明。
- 路由(paths) :
- GET /users :检索所有用户,返回包含
User
对象数组的 200 响应。 - POST /users :创建一个新用户。它需要一个包含
NewUser
Schema 的请求体,并返回一个包含已创建用户对象的 201 响应。 - GET /users/{id} :根据 ID 检索特定用户。如果找不到
User
,则返回 404 响应
- GET /users :检索所有用户,返回包含
- 组件(components) :
- User schema :定义用户对象的结构,包括
id
、name
、email
和createdAt
等字段。 - NewUser schema :用于创建新用户的请求正文,需要
name
和email
字段。 - SecuritySchemes :定义 API 如何处理安全设置。在本例中,我们指定了一种 bearerAuth 方案,该方案在 API 安全上下文中使用 Bearer Token,通常是 JWT(JSON Web Token)。
- User schema :定义用户对象的结构,包括
我们可以定义关于 API 的几乎所有内容,并自动生成最常见语言的代码,加快这一过程
2.2、API 文档 {#22api-文档}
我们还可以在项目代码中直接应用 Open API 文档标签(自动生成或手动标记)。
来看看 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 状态码定义了一个单独的响应,包括描述、预期内容类型和 Schema。
- @Content :指定响应或请求正文的内容类型(如
application/json
),并提供数据序列化的 Schema。 - @Schema :描述了请求和响应体的数据模型,将类(如 User)与 Swagger 中显示的 JSON 结构关联起来。
2.3、互动式控制台 {#23互动式控制台}
Swagger UI 控制台是一个基于 Web 的交互式界面,可根据 OpenAPI 规范动态生成文档。它允许开发人员和 API 用户以可视化方式探索和测试端点。控制台以用户友好的布局显示 API 端点、请求参数、响应和错误码。
每个端点都提供了输入参数值、请求头和请求体的字段,使用户能够直接从控制台发出实时请求。这一功能可帮助开发人员了解 API 行为、验证集成和排除故障,而无需使用单独的工具,因此是 API 开发和测试的重要资源。例如,你可以看看一个 宠物店的 Swagger UI 示例。
2.4、API 优先的好处 {#24api-优先的好处}
为什么要使用独特的 API 约定或文档模板?
模板可确保整个 API 的所有端点都遵循统一的结构。这种一致性简化了内部开发团队和外部消费者对 API 的理解和使用。例如,开发人员、质量保证工程师和外部利益相关者对 API 的功能和结构都有清晰、共同的理解。
此外,客户可以直接在文档中试用 API,从而使 API 更易于采用和集成,而无需大量的额外支持。我们可以设置自动测试,确保 API 的结构和响应符合规范。
3、HATEOAS 是什么? {#3hateoas-是什么}
HATEOAS(超媒体作为应用状态的引擎)是 REST 应用程序架构的一个约束条件。它是更广泛的 REST 范式的一部分,强调客户端完全通过服务器动态提供的超媒体与 REST API 进行交互。在 HATEOAS 中,服务器在其响应中包含链接,引导客户端进行下一步操作。
3.1、HATEOAS 示例 {#31hateoas-示例}
来看看 Spring HATEOAS 应用。首先,将 User
定义为特定表示模型的一部分:
public class User extends RepresentationModel<User> {
private Integer id;
private String name;
private String email;
private LocalDateTime createdAt;
// 构造函数、Getter、Setter
}
现在,以 User
端点为例,来看看如何实现它:
@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
端点的响应示例,在这里我们可以通过链接(links)动态发现用户的操作和相关资源:
[
{
"id": 1,
"name": "John Doe",
"email": "johndoe@example.com",
"createdAt": "2023-01-01T12:00:00",
"_links": {
"self": {
"href": "http://localhost:8080/users/1"
}
}
},
{
"id": 2,
"name": "Jane Smith",
"email": "janesmith@example.com",
"createdAt": "2023-02-01T12:00:00",
"_links": {
"self": {
"href": "http://localhost:8080/users/2"
}
}
}
]
3.2、测试 {#32测试}
为了了解更多细节,来看看 Controller 的一些集成测试。
从获取所有用户开始:
@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());
}
现在,我们希望响应中存在所有按 id 引用的用户。
最后,在创建新用户后,我们还希望在响应中包含新引用:
@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、关键点 {#33关键点}
如你所见,HATEOAS API 在其响应中包含链接,以指导客户端的操作。这就减少了客户端硬编码端点路由的需要,使与 API 的交互更加灵活。
同样,它为客户端提供了一种方式,通过服务器提供的链接来动态地浏览各种状态或操作,实现更加适应性的工作流程。因此,我们可以认为 HATEOAS 是使我们的 API 可探索的最终步骤,以便客户端能够理解其行为。
4、Swagger 与 HATEOAS 的主要区别 {#4swagger-与-hateoas-的主要区别}
Swagger 和 HATEOAS 之间的区别:
| 比较面 | Swagger | HATEOAS | |------------|----------------------------------------------------------------|-----------------------------------------------------------------------| | API 文档 | Swagger 通过用户界面提供了详细的、人类可读的 API 文档,使消费者能够提前了解可用端点、请求参数和响应。 | HATEOAS 依靠服务器在响应中返回的超媒体链接,这意味着文档更加隐含。因此,消费者通过这些链接而不是预先生成的用户界面动态地发现操作。 | | 客户端实现 | 客户端通常是根据 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、总结 {#5总结}
本文介绍了 Swagger 和 HATEOAS,并通过一些应用示例强调了两者的主要区别。我们看到了如何从 YAML 模板生成源代码或使用 Swagger 注解来装饰我们的端点。对于 HATEOAS,我们了解了如何通过添加有价值的链接来导航与端点相关的所有资源,从而改进我们的模型定义。
Ref:https://www.baeldung.com/java-rest-swagger-vs-hateoas