1. 概述

本文聚焦于 REST API 的可发现性(Discoverability)和 HATEOAS,并通过测试驱动的方式探讨实际应用场景。目标是让 API 更加自描述、易于导航,减少客户端对文档或硬编码 URL 的依赖。

2. 为什么需要 API 可发现性

API 的可发现性是一个长期被低估的重要话题。现实中,大多数 API 并没有真正实现这一点。但一旦做好,不仅能提升 RESTful 的纯度,还能显著增强可用性和优雅程度。

要理解可发现性,就必须掌握 HATEOAS(Hypermedia As The Engine Of Application State)这一核心约束。✅
HATEOAS 的本质是:客户端应仅通过超媒体(通常是 Hypertext)来驱动应用状态的流转,所有操作和状态转换都应由服务端在响应中动态提供。

这意味着:

  • ❌ 客户端不应依赖外部文档猜测接口行为
  • ❌ 不应硬编码 URI 或假设资源之间的关系
  • ✅ 所有交互路径都应该在 API 响应中“自解释”

Roy Fielding 在其经典文章 REST APIs must be hypertext-driven 中明确指出:如果不能通过超文本驱动,那就不是真正的 REST。

因此,服务端必须足够“聪明”,仅通过响应内容告诉客户端“接下来能做什么”。在 HTTP 协议中,我们可以通过 Link 头、Allow 头等机制实现这一点。

3. 可发现性实践场景(测试驱动)

一个真正可发现的 REST 服务应该具备哪些特征?下面我们通过 JUnit + rest-assured + Hamcrest 编写测试用例,逐条验证。

⚠️ 由于该 REST 服务已集成 Spring Security 进行安全控制,每个测试前都需要先完成认证。认证逻辑封装在 givenAuth() 方法中(模拟用户登录),细节略过。

3.1. 发现资源支持的 HTTP 方法

当客户端对某个资源使用了不合法的 HTTP 方法时,服务端应返回 405 METHOD NOT ALLOWED,并在响应头中通过 Allow 明确列出允许的操作。

这是最基础的可发现性保障——告诉客户端:“你搞错了,但别担心,我告诉你哪些是对的”。

@Test
public void whenInvalidPOSTIsSentToValidURIOfResource_thenAllowHeaderListsTheAllowedActions(){
    // Given
    String uriOfExistingResource = restTemplate.createResource();

    // When
    Response res = givenAuth().post(uriOfExistingResource);

    // Then
    String allowHeader = res.getHeader(HttpHeaders.ALLOW);
    assertThat( allowHeader, AnyOf.anyOf(
      containsString("GET"), containsString("PUT"), containsString("DELETE") ) );
}

📌 关键点:

  • /foos/123 发送 POST 是非法操作(创建资源应 POST 到集合接口)
  • 服务端返回 405,并在 Allow: GET, PUT, DELETE 中告知合法方法
  • 客户端可据此自动调整行为,无需查文档

3.2. 获取新创建资源的 URI

每次创建资源后,服务端必须在响应中返回新资源的访问地址。这是 RESTful 设计的基本要求,使用 HTTP Location 头实现。

@Test
public void whenResourceIsCreated_thenUriOfTheNewlyCreatedResourceIsDiscoverable() {
    // When
    Foo newResource = new Foo(randomAlphabetic(6));
    Response createResp = givenAuth().contentType("application/json")
      .body(newResource).post(getFooURL());
    String uriOfNewResource = createResp.getHeader(HttpHeaders.LOCATION);

    // Then
    Response response = givenAuth().header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
      .get(uriOfNewResource);

    Foo resourceFromServer = response.body().as(Foo.class);
    assertThat(newResource, equalTo(resourceFromServer));
}

📌 流程说明:

  1. POST 到 /foos 创建资源
  2. 服务端返回 201 Created + Location: /foos/123
  3. 客户端直接从响应头拿到 URI,无需拼接或猜测
  4. 验证 GET 能正确获取刚创建的资源

⚠️ 踩坑提示:若服务端未返回 Location,客户端将无法定位新资源,尤其在分布式环境下 ID 由服务端生成时更致命。

3.3. 发现获取全部资源的接口

获取单个资源时,应能发现“获取所有同类资源”的入口。这体现了资源间的关联性,是 HATEOAS 的典型体现。

我们使用 HTTP Link 头配合 rel="collection" 语义来实现:

@Test
public void whenResourceIsRetrieved_thenUriToGetAllResourcesIsDiscoverable() {
    // Given
    String uriOfExistingResource = createAsUri();

    // When
    Response getResponse = givenAuth().get(uriOfExistingResource);

    // Then
    String uriToAllResources = HTTPLinkHeaderUtil
      .extractURIByRel(getResponse.getHeader("Link"), "collection");

    Response getAllResponse = givenAuth().get(uriToAllResources);
    assertThat(getAllResponse.getStatusCode(), is(200));
}

📌 示例响应头:

Link: <http://api.example.com/foos>; rel="collection"

📌 关键逻辑:

  • 请求 /foos/123 获取单个 Foo
  • 响应头中包含指向 /foos 的链接,关系为 collection
  • 客户端可通过 rel="collection" 提取该 URI,进而发起 GET 获取列表

🔗 HTTPLinkHeaderUtil.extractURIByRel() 的实现详见 GitHub Gist,用于解析 Link 头中的 rel 关系。

⚠️ 注意:rel="collection" 尚未成为正式标准,但已被多个微格式(microformats)采用并推动标准化(参考 RFC 5988)。使用此类非标准 relation 属于合理扩展,只要前后端达成一致即可。

4. 其他潜在可发现接口与微格式

除了上述场景,理论上还可以通过 Link 头暴露更多语义化链接,例如:

  • 创建资源的入口(rel="create"
  • 上一页/下一页(rel="next", rel="prev"
  • 搜索接口(rel="search"
  • 子资源集合(rel="items"

但目前标准 link relation 类型有限,若需更丰富语义,可考虑以下方案:

推荐路径:

❌ 不推荐:

  • 自定义冷门 rel 值导致兼容性问题
  • 完全依赖 JSON 字段传递链接(违背 HTTP 协议层设计原则)

💡 实际建议:
虽然没有标准 rel="create",但业界普遍遵循一个简单规则——创建资源的 URI 通常就是获取资源列表的 URI,区别仅在于使用 POST 方法
比如:

  • GET /foos → 获取列表
  • POST /foos → 创建新资源

这种约定既简洁又无需额外 discoverability 支持,属于“简单粗暴但有效”的实践。

5. 总结

本文通过测试用例展示了如何构建一个真正可发现的 REST API:

✅ 核心思想:客户端应能从根路径出发,仅凭响应中的超媒体信息完成全流程导航,无需预知任何 URI 结构。

✅ 实现手段:

  • 使用 Allow 头告知合法 HTTP 方法
  • 使用 Location 头返回新建资源地址
  • 使用 Link 头建立资源间语义关联(如 rel="collection"

✅ 最终效果:
API 成为一个“自描述”的状态机,客户端像浏览器访问网页一样,通过点击链接(调用接口)逐步推进业务流程。这才是 HATEOAS 的理想状态。

📌 所有示例代码均已开源:
👉 GitHub - spring-boot-rest (Discoverability Tests)

项目基于 Maven 构建,导入即可运行。适合用于学习 HATEOAS 实践或作为企业级 API 设计参考。


原始标题:REST API Discoverability and HATEOAS