当前位置: 首页 > 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.jsqmd.com/news/144716/

相关文章:

  • 2025年济南做得好的翅片管公司有哪些,乏风取热箱/表冷器/翅片管/新风机组/干冷器/空调机组/空气幕/冷却器/散热器翅片管企业哪家好 - 品牌推荐师
  • 基于大数据的精品小说推荐与可视化分析系统(毕设源码+文档)
  • 2025年12月江苏徐州别墅庭院设计、屋顶花园设计、公园绿地设计、市政广场设计、生态园区设计服务商权威测评与综合推荐 - 2025年品牌推荐榜
  • 【路径规划】基于RRT快速探索随机树算法在三维环境中寻找从起点到目标点的路径,并对路径进行平滑处理附Matlab代码
  • 基于Python的购物管理系统毕设源码+文档+讲解视频
  • P3195 [HNOI2008] 玩具装箱 斜率优化
  • comsol悬浮绝缘子电场计算模型,可以得到绝缘子各个部位电势及电场分布,提供comsol详细...
  • mybatis insert后返回id
  • IRC协议:穿越时光的互联网实时聊天奠基者
  • 专科生必看!9个高效降aigc工具推荐,轻松过审不踩坑
  • Java面试:为何必须在循环中检查等待条件?避坑指南!
  • LuatOS下载不求人:完整流程与高频问题应对策略
  • 课后作业2
  • 2025年12月绵阳米粉/米线加工厂综合比较 - 2025年品牌推荐榜
  • 2025年12月江苏徐州别墅庭院设计、屋顶花园设计、公园绿地设计、市政广场设计、生态园区设计服务商排行榜 - 2025年品牌推荐榜
  • 运用 Python 将 Markdown 转换为 Word、HTML、PDF、PNG 和 JPG
  • 基于Spring Boot和Vue.js的视频点播管理系统设计与实现
  • pg_waldump 和 pg_xlogdump
  • 一个简单想法的实验随笔-胜任能力
  • 高精度光学动作捕捉如何为无人机提供飞行姿态与轨迹真值?——以IROS 2025多篇无人机学习与控制研究为例
  • 让回忆“动”起来:手把手教你制作老照片动态视频
  • 2025最新!自考党必看9个AI论文平台测评与推荐
  • CF1295F Good Contest/[APIO2016] 划艇
  • 郑州家装公司五大推荐:优质装修/别墅装修/老房翻新精选,华埔装饰砸无赦承诺引领行业新风尚 - 深度智识库
  • 基于Spring Boot和Vue.js的房屋出租管理系统设计与实现
  • 2025年12月江苏徐州变压器系列,智能变电站,新能源配套,高低压配电柜,智慧电力系统厂家选择指南 - 2025年品牌推荐榜
  • 2025年门式冲洗装置直销厂家权威推荐榜单:液压水力冲洗门/水力冲洗门/智能控制拍门源头厂家精选 - 品牌推荐官
  • 2025年蠕变持久试验机生产厂家推荐:哪家公司靠谱/国内哪家性价比高/哪个厂家品质好/哪家售后好 - 品牌推荐大师1
  • 上海策划品牌全案公司推荐:4事业部+长期陪跑(案例集) - 品牌排行榜
  • ModelEngine的Aido智能体【娱乐生涯 AI 助手】升级计划——工作流编排精确制导AI应用