HATEOAS 要解决什么问题以及依赖引入
HATEOAS(Hypermedia as the Engine of Application State)的核心思路是让服务端在响应中附带链接,客户端通过链接发现下一步可以执行的操作,而不是把各个接口地址硬编码在前端代码里。传统 REST 接口返回一个 JSON 对象,里面只有数据字段,客户端要更新商品、查看订单、翻页时,通常需要根据接口文档手动拼接 URL。一旦服务端调整路由,前端、移动端或第三方调用方都要跟着改。HATEOAS 在响应中加入 _links 字段,把 self、update、delete、next 等关系对应的 URI 直接返回,客户端只需根据关系名解析链接即可完成跳转,服务端可以独立演进 URL 结构。

Spring HATEOAS 是 Spring 生态对 HATEOAS 约束的落地实现,提供 RepresentationModel、EntityModel、CollectionModel 等模型,以及 WebMvcLinkBuilder、WebFluxLinkBuilder 等链接构建器。在 Spring Boot 项目中集成非常简单,只需引入 spring-boot-starter-hateoas 依赖。该起步依赖会自动加入 spring-hateoas 核心库,并配置 Jackson 对 HAL 格式的序列化支持,默认响应 Content-Type 为 application/hal+json。Maven 坐标如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
如果使用 Gradle,可以在 build.gradle 中加入 implementation 'org.springframework.boot:spring-boot-starter-hateoas'。引入后通常不需要额外配置类,只需要在控制器和实体上做适配。需要注意,Spring HATEOAS 对 Spring Framework 和 Jackson 版本有配套要求,Spring Boot 的依赖管理会自动处理版本冲突;但如果手动排除依赖,可能会遇到 MediaType 不识别、HAL 序列化失败等问题。
资源模型与链接构建:从 RepresentationModel 到 EntityModel
在 Spring HATEOAS 中,最基础的做法是让资源类继承 RepresentationModel<T>,这样资源对象本身就能持有 Link 列表。例如有一个商品实体 Product,包含 id、name、price 三个字段。为了不让持久化实体与表现层耦合,通常会创建一个专门的资源类 ProductResource 继承 RepresentationModel<ProductResource>,并在构造函数中复制业务字段。
public class Product {
private Long id;
private String name;
private double price;
public Product(Long id, String name, double price) {
this.id = id;
this.name = name;
this.price = price;
}
// 省略 getter/setter
}
public class ProductResource extends RepresentationModel<ProductResource> {
private final Long id;
private final String name;
private final double price;
public ProductResource(Product product) {
this.id = product.getId();
this.name = product.getName();
this.price = product.getPrice();
}
// 省略 getter
}
继承 RepresentationModel 的优点是可以在服务层或控制器组装时直接调用 add(Link) 方法添加链接,资源类与链接绑定在一起。但缺点也很明显:每个业务实体都要额外编写资源类,字段多时容易重复,而且持久化实体如果直接继承 RepresentationModel 会把链接字段污染到数据库层。为了解决这个问题,Spring HATEOAS 提供了 EntityModel<T> 包装器,它不需要修改实体类本身,只需在控制器返回时用 EntityModel.of(entity, links) 把实体和链接包装起来。
EntityModel 本质上是一种通用容器,里面保存 domain object 和链接列表。使用 EntityModel 的另一个好处是序列化时会把实体的字段和 _links 平级输出,而不是嵌套在 data 字段里。对于集合资源,则使用 CollectionModel<T>,它可以包含多个 EntityModel 以及整个集合的 self 链接。下面会结合 Controller 演示两种模型的返回方式。
Controller 实战:生成 self 链接与集合链接
链接的构建通常使用 WebMvcLinkBuilder,它可以基于控制器方法映射自动生成 URI,避免在代码里硬编码路径。核心方法是 linkTo(methodOn(ProductController.class).getProduct(id)),其中 methodOn 创建控制器代理,linkTo 读取方法上的 @GetMapping 注解并拼出完整地址。然后调用 withSelfRel() 表示该链接的关系是 self,或用 withRel("products") 自定义关系名。
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*;
@RestController
@RequestMapping("/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping("/{id}")
public EntityModel<Product> getProduct(@PathVariable Long id) {
Product product = productService.findById(id);
return EntityModel.of(product,
linkTo(methodOn(ProductController.class).getProduct(id)).withSelfRel(),
linkTo(methodOn(ProductController.class).listProducts()).withRel("products"));
}
@GetMapping
public CollectionModel<EntityModel<Product>> listProducts() {
List<EntityModel<Product>> products = productService.findAll().stream()
.map(product -> EntityModel.of(product,
linkTo(methodOn(ProductController.class).getProduct(product.getId())).withSelfRel()))
.collect(Collectors.toList());
return CollectionModel.of(products,
linkTo(methodOn(ProductController.class).listProducts()).withSelfRel());
}
}
getProduct 方法返回单个商品时,除了 self 链接,还额外添加了一个名为 products 的链接指向商品列表,客户端可以通过该关系返回列表。listProducts 方法中,每个商品都被包装成 EntityModel 并添加各自的 self 链接,最后用 CollectionModel.of 添加整体集合的 self 链接。序列化后,单个商品接口返回的内容大致如下:
{
"id": 1,
"name": "机械键盘",
"price": 399.0,
"_links": {
"self": {
"href": "http://localhost:8080/products/1"
},
"products": {
"href": "http://localhost:8080/products"
}
}
}
可以看到,链接位于 _links 对象中,每个关系名对应一个包含 href 的对象。集合接口则会返回 _embedded 和 _links 两部分,_embedded 里是商品数组,每个元素也都携带 self 链接。这种结构让客户端不用查阅接口文档就能知道如何获取详情、返回列表或执行后续动作。
需要注意的是,linkTo(methodOn(...)) 的参数不能传 null,否则构建链接时会因为无法确定路径变量而抛出异常。对于路径变量,必须传入实际的值;对于多个路径变量,需要按顺序传入。另外,如果控制器方法有 @RequestParam 或 @RequestBody 参数,linkTo 不会构造查询串,需要在 Link 上额外处理。
响应格式、配置与常见坑点
Spring Boot 自动配置后,接口默认返回 HAL 格式,Content-Type 为 application/hal+json。如果想关闭 HAL 而使用普通 JSON,可以通过 spring.hateoas.use-hal-as-default-json-media-type=false 配置,但一般不建议,因为 HAL 已经是 Spring HATEOAS 最稳定的表现格式。如果你的项目里还有 Spring Data REST,可以共用同一套链接规范。
常见问题之一是启动时找不到 RepresentationModel 类。这往往是因为只引入了 spring-boot-starter-web,没有引入 spring-boot-starter-hateoas。另一个坑是手动创建 ObjectMapper 覆盖了 Spring Boot 的 Jackson 配置,导致 HAL 模块不生效,返回 406 或链接不输出。这时需要确认 Jackson2ObjectMapperBuilder 是否注入了 Spring HATEOAS 模块,或者直接使用自动配置的 ObjectMapper。
在实体设计上,尽量避免让 JPA 实体直接继承 RepresentationModel。虽然这样可以少写一个包装类,但会把链接数据混入数据库实体,缓存序列化和数据访问对象都要额外处理。更推荐使用 EntityModel 或自定义 Resource 类分离关注点。对于大型项目,还可以创建统一的 Assembler 类把实体转换为 EntityModel,把链接构建逻辑集中管理,避免控制器臃肿。
最后,HATEOAS 的价值不是让响应变大,而是通过自描述降低客户端与服务端的协作成本。如果 API 只被内部一个前端调用且变化不频繁,引入 HATEOAS 可能收益有限;但当 API 面向多个团队、多个终端,并且需要长期演进时,超媒体约束能显著减少因 URL 调整造成的联调问题。Spring HATEOAS 提供了从简单包装到完整 Assembler 的渐进式方案,开发者可以根据项目阶段逐步采用。
Spring BootSpring Boot HATEOASHATEOAS修改时间:2026-10-03 16:16:04