传统的 REST 接口通常只返回裸数据,客户端需要根据文档硬编码各种 URL 才能完成业务流程。一旦服务端调整了路由,客户端就得跟着改代码,联调成本很高。超媒体作为 REST 成熟度模型中的最高层级,要求响应中不仅包含数据本身,还要包含指向相关操作的链接,让客户端能够根据链接自主发现下一步动作。Spring HATEOAS 正是 Spring 官方提供的超媒体支持库,它与 Spring Boot 无缝集成,本文将完整演示整合过程。

一、超媒体的核心概念与 Spring HATEOAS 的作用
HATEOAS 是 Hypermedia as the Engine of Application State 的缩写,中文一般译作超媒体作为应用状态引擎。它的核心思想是:客户端与服务端的交互不依赖固定的 URL 约定,而是依赖响应中携带的链接。举个例子,客户端请求一个订单资源时,如果订单状态是待支付,响应中会出现支付链接;如果订单已发货,响应中则出现确认收货链接。客户端不需要了解业务规则,只需要判断链接是否存在即可决定行为。
Spring HATEOAS 提供了一组 API 用来构建这种带链接的响应模型,主要包括 RepresentationModel、EntityModel、CollectionModel、Link 和 WebMvcLinkBuilder 等核心类。它默认输出 HAL(Hypertext Application Language)格式的 JSON,链接统一放在 _links 字段中,这是目前使用最广泛的超媒体媒体类型。理解这些类的职责分工,是写好超媒体接口的基础。
需要注意版本对应关系。Spring Boot 2.x 对应 Spring HATEOAS 1.x,Spring Boot 3.x 对应 Spring HATEOAS 2.x。如果你使用的是 Spring Boot 3,那么核心包名已经从 org.springframework.hateoas 迁移为 org.springframework.hateoas.server 下的相关包,编码方式略有调整,下文示例会说明。
二、引入依赖并搭建基础项目
在 Spring Boot 项目中整合 Spring HATEOAS 非常简单,只需要在 pom.xml 中添加一个 starter 依赖。Spring Boot 已经帮我们管理好了版本号,不需要手动指定。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>这个 starter 会自动引入 spring-hateoas 核心库以及相关配置。引入后 Spring Boot 的自动配置会将默认的媒体类型设置为 application/hal+json,也就是说,只要你的 Controller 返回的是 RepresentationModel 的子类,输出的 JSON 就会自动带上 _links 结构,无需额外配置。
接下来准备一个简单的实体类和模拟的数据访问层,方便后续演示。这里用一个订单实体作为例子:
// 订单实体,只是一个普通 POJO
public class Order {
private Long id;
private String status; // 待支付 / 已支付 / 已发货
private Double amount;
public Order(Long id, String status, Double amount) {
this.id = id;
this.status = status;
this.amount = amount;
}
// 省略 getter 和 setter
}
// 模拟数据仓库
@Repository
public class OrderRepository {
private static final Map<Long, Order> DATA = new ConcurrentHashMap<>();
static {
DATA.put(1L, new Order(1L, "待支付", 199.0));
DATA.put(2L, new Order(2L, "已发货", 88.0));
}
public Order findById(Long id) {
return DATA.get(id);
}
}三、使用 EntityModel 与 WebMvcLinkBuilder 构建带链接的响应
EntityModel 用于包装单个资源对象,它继承自 RepresentationModel,可以在包装的同时附加若干个 Link。构建链接时强烈建议使用 WebMvcLinkBuilder 而不是手写字符串 URL,因为它基于 Controller 的方法签名生成链接,方法重命名或路径调整时链接会自动跟着变,编译期就能发现问题。
下面编写订单 Controller,重点演示如何根据订单状态动态添加不同的操作链接:
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@Autowired
private OrderRepository orderRepository;
@GetMapping("/{id}")
public EntityModel<Order> getOrder(@PathVariable Long id) {
Order order = orderRepository.findById(id);
if (order == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}
// 生成指向自身的链接,指向当前类的 getOrder 方法
Link selfLink = linkTo(methodOn(OrderController.class).getOrder(id))
.withSelfRel();
EntityModel<Order> model = EntityModel.of(order, selfLink);
// 根据订单状态动态添加操作链接
if ("待支付".equals(order.getStatus())) {
Link payLink = linkTo(methodOn(OrderController.class).payOrder(id))
.withRel("pay");
model.add(payLink);
} else if ("已发货".equals(order.getStatus())) {
Link confirmLink = linkTo(methodOn(OrderController.class)
.confirmReceipt(id)).withRel("confirm");
model.add(confirmLink);
}
return model;
}
@PutMapping("/{id}/pay")
public ResponseEntity<?> payOrder(@PathVariable Long id) {
// 支付业务逻辑
return ResponseEntity.ok().build();
}
@PutMapping("/{id}/confirm")
public ResponseEntity<?> confirmReceipt(@PathVariable Long id) {
// 确认收货业务逻辑
return ResponseEntity.ok().build();
}
}请求 GET /api/orders/1 时,返回的 JSON 大致如下:
{
"id": 1,
"status": "待支付",
"amount": 199.0,
"_links": {
"self": { "href": "http://localhost:8080/api/orders/1" },
"pay": { "href": "http://localhost:8080/api/orders/1/pay" }
}
}可以看到,客户端拿到这个响应后,只需检查 _links 中是否存在 pay 关系的链接,就能判断当前订单是否可以支付,完全不需要在客户端写死 URL 或业务判断逻辑。这就是超媒体驱动的交互方式。当订单状态流转后,同一个接口返回的可用操作也会随之变化,接口的自描述性大大增强。
四、集合资源与 RepresentationModelProcessor 进阶用法
处理列表类资源时要使用 CollectionModel。直接把 EntityModel 列表塞进 CollectionModel.of(),每个元素都会保留自己的链接,同时集合本身还可以附加 self 链接:
@GetMapping
public CollectionModel<EntityModel<Order>> allOrders() {
List<EntityModel<Order>> orders = orderRepository.findAll().stream()
.map(order -> EntityModel.of(order,
linkTo(methodOn(OrderController.class)
.getOrder(order.getId())).withSelfRel()))
.collect(Collectors.toList());
return CollectionModel.of(orders,
linkTo(methodOn(OrderController.class).allOrders()).withSelfRel());
}当链接组装逻辑比较复杂、需要在多处复用时,可以借助 RepresentationModelProcessor 把链接添加逻辑抽离成独立组件。它的工作方式类似一个后处理器,Spring HATEOAS 会在资源模型返回前自动调用所有匹配类型的 Processor,把链接注入进去:
@Component
public class OrderResourceProcessor
implements RepresentationModelProcessor<EntityModel<Order>> {
@Override
public EntityModel<Order> process(EntityModel<Order> model) {
Order order = model.getContent();
if (order != null && "待支付".equals(order.getStatus())) {
model.add(linkTo(methodOn(OrderController.class)
.payOrder(order.getId())).withRel("pay"));
}
return model;
}
}注册这个 Processor 后,Controller 里就不再需要写状态判断逻辑,只需返回基础的 EntityModel,链接注入由框架统一完成。这种模式让链接生成逻辑集中在了一处,特别适合状态机复杂的业务场景。
五、整合时的注意事项与常见问题
首先是返回类型问题。如果 Controller 方法的返回类型写的是 EntityModel 的基类 RepresentationModel,Spring HATEOAS 有时无法正确推断泛型信息,导致序列化时丢失内容或链接,建议方法返回类型尽量写具体的泛型形式。其次是版本兼容,Spring Boot 3 下 linkTo 与 methodOn 所在的包已经变化,网上大量基于 1.x 的教程代码直接照搬会编译报错,需要将导入调整为 org.springframework.hateoas.server.mvc.WebMvcLinkBuilder 下的新路径。
另外要注意媒体类型的协商。默认输出是 HAL 格式,如果客户端通过 Accept: application/prs.hal-forms+json 之类的头请求其他超媒体方言,需要额外引入对应依赖才能支持。对于前后端分离项目中前端框架的消费问题,主流的 Axios 可以直接读取 _links 字段,配合 TypeScript 定义好类型即可,并不会增加太多负担。
最后提醒一点,超媒体并不是银弹。对于纯内部服务间的调用、性能敏感的网关接口,增加链接字段会带来额外的响应体积和构建开销。HATEOAS 更适合面向外部开放、需要长期演进、消费者众多的 API,这时代价换来的是接口的稳定性和自解释能力。在动手改造之前,先评估自己的业务场景是否真的需要,再决定引入的深度。
Spring BootSpring HATEOASRESTful API修改时间:2026-09-11 21:02:45