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 模式:定义用户对象结构,包含 idnameemailcreatedAt 字段
    • NewUser 模式:用于创建用户的请求体,要求必填 nameemail 字段
    • 安全方案:定义 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 获取。


原始标题:Difference Between Swagger and HATEOAS | Baeldung