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));
}
📌 流程说明:
- POST 到
/foos
创建资源 - 服务端返回
201 Created
+Location: /foos/123
- 客户端直接从响应头拿到 URI,无需拼接或猜测
- 验证 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 类型有限,若需更丰富语义,可考虑以下方案:
✅ 推荐路径:
- 使用已广泛接受的 relation,如
next
,prev
,self
,collection
- 引入 Atom Publishing Protocol 规范
- 采用 microformats 提供结构化语义
❌ 不推荐:
- 自定义冷门
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 设计参考。