当前位置: 首页 > news >正文

Spring HATEOAS 详细介绍

让我用一个餐厅点餐的类比来帮你理解 HATEOAS 和 Spring HATEOAS。

一、先理解 HATEOAS 的核心思想

场景:在餐厅点餐

传统 API(没有 HATEOAS):

  • 你去餐厅,服务员给你一份菜单

  • 必须知道要喊:"服务员,我要点菜!"

  • 必须知道要说:"给我一份意大利面"

  • 必须知道吃完后要说:"结账!"

  • 必须记住所有可能的操作和对应的指令

HATEOAS API(有 HATEOAS):

  • 你进餐厅,服务员说:

    • "欢迎!这是菜单(包含菜品和'点菜'按钮)"

    • 你点菜后,服务员返回:

      • "已收到您的订单(包含'查看订单'、'修改订单'、'付款'按钮)"

    • 你付款后,服务员返回:

      • "付款成功(包含'开发票'、'评价'、'再来一单'按钮)"

  • 你不需要记住任何固定指令,服务员每次都会告诉你下一步能做什么

二、Spring HATEOAS 解决什么问题

传统 REST API 的问题

// 客户端需要硬编码这些 URL String getOrdersUrl = "http://api.example.com/orders"; // 如果这个URL改变,客户端就坏了 String createOrderUrl = "http://api.example.com/orders"; // 客户端必须知道这里是POST String cancelOrderUrl = "http://api.example.com/orders/{id}/cancel"; // 客户端必须知道这个模式

Spring HATEOAS 的解决方案

// 客户端不关心具体URL,只关心链接关系 Link selfLink = response.getLink("self"); // 获取"查看自己"的链接 Link cancelLink = response.getLink("cancel"); // 获取"取消"的链接 // URL可以任意变化,只要关系名称不变

三、实际代码示例详解

示例1:简单的订单系统

1. 实体类(Order.java)

// 普通的Java对象 public class Order { private Long id; private String customerName; private BigDecimal total; private OrderStatus status; // PENDING, PAID, CANCELLED // 构造器、getter、setter public boolean canBeCancelled() { return status == OrderStatus.PENDING; } }

2. 资源表示类(OrderResource.java)

// 继承 EntityModel,这样就能添加链接 // 这就像给"订单"这个普通对象穿上"超链接"的外套 public class OrderResource extends EntityModel<Order> { // 可以有额外的属性 private String message; public OrderResource(Order order) { super(order); // 把订单对象放进去 this.message = "订单详情"; } // 也可以不继承,直接用EntityModel.of()包装 }

3. 控制器(OrderController.java) - 详细解释

@RestController @RequestMapping("/api/orders") public class OrderController { @Autowired private OrderService orderService; /** * 获取单个订单 * 返回的不仅是一个订单对象,还包含它能做什么操作的链接 */ @GetMapping("/{id}") public EntityModel<Order> getOrder(@PathVariable Long id) { // 1. 获取订单数据 Order order = orderService.findById(id); // 2. 创建资源模型(订单+链接) EntityModel<Order> resource = EntityModel.of(order); // 3. 添加"自链接"(查看自己) // linkTo: 创建链接 // methodOn: 指向哪个控制器方法 // withSelfRel(): 关系名为"self" resource.add( linkTo(methodOn(OrderController.class).getOrder(id)) .withSelfRel() ); // 4. 添加"返回列表"链接 resource.add( linkTo(methodOn(OrderController.class).getAllOrders()) .withRel("collection") // 关系名"collection" ); // 5. 根据状态动态添加链接 if (order.canBeCancelled()) { // 只有待处理的订单才能取消 resource.add( linkTo(methodOn(OrderController.class).cancelOrder(id, null)) .withRel("cancel") // 关系名"cancel" ); } if (order.getStatus() == OrderStatus.PAID) { // 已支付的订单可以开发票 resource.add( linkTo(methodOn(InvoiceController.class).createInvoice(order.getId())) .withRel("invoice") ); } return resource; } }

4. 查看返回的JSON(HAL格式)

{ "id": 123, "customerName": "张三", "total": 100.00, "status": "PENDING", // 这是Spring HATEOAS添加的链接部分 "_links": { "self": { "href": "http://localhost:8080/api/orders/123" }, "collection": { "href": "http://localhost:8080/api/orders" }, "cancel": { "href": "http://localhost:8080/api/orders/123/cancel" } } }

四、linkTo 和 methodOn 的工作原理

这两个方法是Spring HATEOAS的魔法所在:

// 这行代码做了什么? linkTo(methodOn(OrderController.class).getOrder(id)).withSelfRel() // 分解: // 1. methodOn(OrderController.class) 创建一个Controller的代理 // 2. .getOrder(id) 调用代理的方法,Spring HATEOAS会记录:调用的是getOrder方法,参数是id // 3. linkTo() 根据上一步的记录,查找@RequestMapping注解,生成URL // 4. withSelfRel() 给这个链接命名"self"

等价于:

// 手动构建URL(不推荐,容易出错) String url = "/api/orders/" + id; Link link = new Link(url, "self"); // 使用ControllerLinkBuilder(简化版) Link link = ControllerLinkBuilder .linkTo(OrderController.class) // 指定Controller .slash("orders") // 添加路径 .slash(id) // 添加ID .withSelfRel();

五、完整的增删改查示例

OrderController.java 完整版

@RestController @RequestMapping("/api/orders") public class OrderController { // 获取所有订单 @GetMapping public CollectionModel<EntityModel<Order>> getAllOrders() { List<Order> orders = orderService.findAll(); // 将每个订单转换为资源模型 List<EntityModel<Order>> orderResources = orders.stream() .map(order -> EntityModel.of(order, linkTo(methodOn(OrderController.class) .getOrder(order.getId())).withSelfRel() )) .collect(Collectors.toList()); // 包装成集合资源 return CollectionModel.of(orderResources, linkTo(methodOn(OrderController.class).getAllOrders()) .withSelfRel(), linkTo(methodOn(OrderController.class).createOrder(null)) .withRel("create") // 如何创建新订单 ); } // 创建订单 @PostMapping public ResponseEntity<EntityModel<Order>> createOrder(@RequestBody Order order) { Order savedOrder = orderService.save(order); // 创建资源 EntityModel<Order> resource = EntityModel.of(savedOrder, linkTo(methodOn(OrderController.class) .getOrder(savedOrder.getId())).withSelfRel() ); // 返回201 Created,包含Location头 return ResponseEntity.created( linkTo(methodOn(OrderController.class) .getOrder(savedOrder.getId())).toUri() ).body(resource); } // 取消订单 @PostMapping("/{id}/cancel") public ResponseEntity<?> cancelOrder(@PathVariable Long id, @RequestBody CancelRequest request) { orderService.cancel(id, request.getReason()); // 取消后返回订单详情 return ResponseEntity.ok(getOrder(id)); } }

六、RepresentationModelAssembler 的作用

这是一个转换器,把普通对象转换成带链接的资源对象:

@Component public class OrderModelAssembler implements RepresentationModelAssembler<Order, EntityModel<Order>> { // 单个对象转换 @Override public EntityModel<Order> toModel(Order order) { return EntityModel.of(order, linkTo(methodOn(OrderController.class) .getOrder(order.getId())).withSelfRel(), linkTo(methodOn(OrderController.class) .cancelOrder(order.getId(), null)) .withRel("cancel"), linkTo(methodOn(PaymentController.class) .getOrderPayments(order.getId())) .withRel("payments") ); } // 集合转换 public CollectionModel<EntityModel<Order>> toCollectionModel( List<Order> orders, boolean includeCreateLink) { // 先调用父类方法转换每个订单 CollectionModel<EntityModel<Order>> collectionModel = RepresentationModelAssembler.super.toCollectionModel(orders); // 添加集合级别的链接 collectionModel.add( linkTo(methodOn(OrderController.class) .getAllOrders()).withSelfRel() ); if (includeCreateLink) { collectionModel.add( linkTo(methodOn(OrderController.class) .createOrder(null)).withRel("create") ); } return collectionModel; } }

在Controller中使用:

@GetMapping("/{id}") public EntityModel<Order> getOrder(@PathVariable Long id) { Order order = orderService.findById(id); return assembler.toModel(order); // 一行代码搞定! }

七、客户端如何使用这样的API

传统客户端调用:

// 硬编码的URL String apiBase = "http://api.example.com"; String ordersUrl = apiBase + "/api/orders"; // 1. 获取订单列表 Response ordersResponse = restTemplate.getForEntity(ordersUrl, String.class); // 2. 从响应中提取订单ID Long orderId = parseOrderId(ordersResponse); // 3. 硬编码取消URL String cancelUrl = apiBase + "/api/orders/" + orderId + "/cancel"; restTemplate.postForEntity(cancelUrl, null, Void.class);

使用Spring HATEOAS客户端:

// 1. 发现入口点 String apiRoot = "http://api.example.com/api"; ResponseEntity<EntityModel<Object>> rootResponse = restTemplate.exchange(apiRoot, HttpMethod.GET, null, new ParameterizedTypeReference<EntityModel<Object>>() {}); // 2. 提取"orders"链接 Link ordersLink = rootResponse.getBody().getLink("orders").orElseThrow(); // 3. 获取订单列表 ResponseEntity<CollectionModel<EntityModel<Order>>> ordersResponse = restTemplate.exchange(ordersLink.toUri(), HttpMethod.GET, null, new ParameterizedTypeReference<CollectionModel<EntityModel<Order>>>() {}); // 4. 获取第一个订单 EntityModel<Order> firstOrder = ordersResponse.getBody().getContent().iterator().next(); // 5. 从订单中提取"cancel"链接 Link cancelLink = firstOrder.getLink("cancel").orElseThrow(); // 6. 取消订单(不需要知道具体URL!) restTemplate.postForEntity(cancelLink.toUri(), null, Void.class);

八、实际好处

1.API演进更容易

// 旧URL:/api/v1/orders/{id}/cancel // 新URL:/api/v2/orders/{id}/actions/cancel // 客户端代码完全不变!因为客户端只关心"cancel"这个关系名 // 服务器返回什么URL,客户端就用什么URL

2.权限控制更灵活

// 管理员看到更多链接 if (user.hasRole("ADMIN")) { resource.add(linkTo(methodOn(AdminController.class) .refundOrder(order.getId())).withRel("refund")); }

3.状态控制

// 只有特定状态的订单才有某些操作 if (order.getStatus() == OrderStatus.SHIPPED) { resource.add(linkTo(methodOn(TrackingController.class) .getTracking(order.getId())).withRel("tracking")); }

九、常见问题解答

Q: 为什么用EntityModel.of()而不是new EntityModel<>()

A:EntityModel.of()是工厂方法,可以确保对象正确初始化。它内部会设置一些必要的属性。

Q: 链接关系名(rel)有什么规范?

A: 有三种:

  1. IANA标准关系:selfnextprevfirstlastcollection

  2. Web Linking关系:stylesheeticon

  3. 自定义关系:orderpaymentinvoice

Q: 如何测试HATEOAS API?

@Test void shouldReturnOrderWithLinks() throws Exception { mockMvc.perform(get("/api/orders/1")) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(1)) .andExpect(jsonPath("$._links.self.href").exists()) .andExpect(jsonPath("$._links.cancel.href").exists()) .andExpect(jsonPath("$._links.collection.href").exists()); }

十、总结比喻

把API想象成一个网站导航

  • 传统API:给你一张地图,告诉你"书店在这里,餐厅在那里",地图变了就得重印

  • HATEOAS API:每个地方都有指示牌

    • 在首页:"想去书店?点这里"

    • 在书店:"想买书?点这里"、"想结账?点这里"

    • 在收银台:"要发票?点这里"、"要袋子?点这里"

Spring HATEOAS就是帮你自动生成这些"指示牌"的工具,让客户端只需要跟着指示牌走,不需要记住整个地图。

http://www.gsyq.cn/news/159129.html

相关文章:

  • LuatOS下载不求人:完整流程与高频问题应对策略
  • 024.二叉树层序遍历
  • mybatis insert后返回id
  • Java面试:为何必须在循环中检查等待条件?避坑指南!
  • Android 12 RK3588平台电源菜单深度定制指南
  • 千万不能忽视!选择口碑好的实验室净化机构有多重要
  • 实验室净化?选这家供应商就对了
  • 2025年12月江苏徐州变压器系列、智能变电站、新能源配套、高低压配电柜、智慧电力系统推荐榜单:顶尖企业综合评估 - 2025年品牌推荐榜
  • comsol悬浮绝缘子电场计算模型,可以得到绝缘子各个部位电势及电场分布,提供comsol详细...
  • !AI领域火爆!求职人数激增33.4%,AI工程师月薪高达3.5万元,你还在等什么?
  • 2025年封切收缩机厂家实力推荐:套袋机/包装机/码垛机源头厂家精选 - 品牌推荐官
  • 凌晨兩點的覺悟:當AttributeError成為我擁抱Type Hints的轉折點
  • 2025年12月金属熔剂/合金金属熔剂/金属添加剂/厂家综合评测 - 2025年品牌推荐榜
  • ModelEngine的Nexent智能体【娱乐生涯 AI 助手】落地实施测试——看看你35岁能否成为天王巨星
  • 2025年12月成都米粉/米线/绵阳米粉加工厂口碑榜单 - 2025年品牌推荐榜
  • 基于Spring Boot和Vue.js的视频点播管理系统设计与实现
  • 运用 Python 将 Markdown 转换为 Word、HTML、PDF、PNG 和 JPG
  • CF1295F Good Contest/[APIO2016] 划艇
  • 2025最新!自考党必看9个AI论文平台测评与推荐
  • 2025年图书档案标签厂家实力推荐:超高频抗金属标签/耐高温电子标签/rfid标签定制厂家精选 - 品牌推荐官
  • 上海策划品牌全案公司推荐:4事业部+长期陪跑(案例集) - 品牌排行榜
  • 告别传统低效!AgentRun 如何用 Serverless + Agent 打造现代化的舆情分析系统?
  • 2025年蠕变持久试验机生产厂家推荐:哪家公司靠谱/国内哪家性价比高/哪个厂家品质好/哪家售后好 - 品牌推荐大师1
  • 学长亲荐9个AI论文工具,研究生轻松搞定开题报告!
  • 西门子模拟量处理程序块:滤波峰值,便捷调用报警功能,适用于博图V15及以上版本
  • 模型没挂,是我自己把系统搞死的
  • c# 递归算法
  • 12.24模拟赛
  • 【golang】goland使用多版本go sdk的方法
  • 2025年12月三圣乡团建/宴席/婚宴/团建聚会/寿宴场地推荐排行榜单 - 2025年品牌推荐榜