重庆市网站建设_网站建设公司_在线客服_seo优化
2025/12/26 18:42:11 网站建设 项目流程

让我用一个餐厅点餐的类比来帮你理解 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就是帮你自动生成这些"指示牌"的工具,让客户端只需要跟着指示牌走,不需要记住整个地图。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询