导读:本期聚焦于IT柏拉图创作的《如何用 Spring Boot 整合 HATEOAS 实现可发现的 REST 超媒体 API?》,敬请观看详情。REST 接口返回纯数据时,客户端需要硬编码拼接后续请求路径,路由一变前端逻辑就会失效。超媒体驱动(HATEOAS)把可用操作和链接直接放进响应里,客户端根据关系名动态发现下一步调用,这是 REST 成熟度模型中的关键一环。本文以 Spring Boot 为基础,展示如何通过 spring-boot-starter-hateoas 引入 Spring HATEOAS,使用 RepresentationModel 或 EntityModel 包装资源,并借助 WebMvcLinkBuilder 生成 self、update 等链接。文章会说明实体建模、控制器返回结构、链接构建的常见写法,分析 HAL 响应格式、自动配置过程和常见坑点。读完可以快速在项目里落地 HATEOAS,让 API 从普通资源接口升级为可发现、可演进、客户端耦合度更低的超媒体服务。

HATEOAS 要解决什么问题以及依赖引入

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

如何用 Spring Boot 整合 HATEOAS 实现可发现的 REST 超媒体 API?

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

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/1003/65172.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。