如何在带自定义属性的RepresentationModel中嵌入资源
Spring HATEOAS构建符合HAL规范的混合结构资源问题
我了解EntityModel可包装单个POJO并添加_links,CollectionModel可包装多个POJO并放入HAL资源的_embedded部分。根据HAL规范,资源可同时包含状态、链接与_embedded内容,但我在Spring HATEOAS中无法创建如下结构的表示(暂不考虑CURIES与模板):
{ "_links": { "self": { "href": "/orders" }, "curies": [{ "name": "ea", "href": "http://example.com/docs/rels/{rel}", "templated": true }], "next": { "href": "/orders?page=2" }, "ea:find": { "href": "/orders{?id}", "templated": true }, "ea:admin": [{ "href": "/admins/2", "title": "Fred" }, { "href": "/admins/5", "title": "Kate" }] }, "currentlyProcessing": 14, "shippedToday": 20, "_embedded": { "ea:order": [{ "_links": { "self": { "href": "/orders/123" }, "ea:basket": { "href": "/baskets/98712" }, "ea:customer": { "href": "/customers/7809" } }, "total": 30.00, "currency": "USD", "status": "shipped" }, { "_links": { "self": { "href": "/orders/124" }, "ea:basket": { "href": "/baskets/97213" }, "ea:customer": { "href": "/customers/12369" } }, "total": 20.00, "currency": "USD", "status": "processing" }] } }
我知道可扩展RepresentationModel实现自定义属性,但添加CollectionModel作为属性时,它不会被放入_embedded,而是以自定义变量名存在。我想知道是否必须额外创建两个类才能实现CollectionModel原生支持的、符合HAL规范的功能?
解决方案
不需要额外创建多个类,Spring HATEOAS提供了EmbeddedWrappers工具类,结合RepresentationModel或动态Model就能构建这种包含自定义状态、链接和_embedded的混合结构。
方法一:自定义RepresentationModel子类 + EmbeddedWrappers
- 定义统计数据类:封装自定义状态属性
public class OrderStats { private int currentlyProcessing; private int shippedToday; public OrderStats(int currentlyProcessing, int shippedToday) { this.currentlyProcessing = currentlyProcessing; this.shippedToday = shippedToday; } // Getter方法省略 }
- 创建根资源模型:继承
RepresentationModel,包含统计属性和嵌入包装集合
public class OrderSummaryModel extends RepresentationModel<OrderSummaryModel> { private OrderStats stats; private List<EmbeddedWrapper> embedded; public OrderSummaryModel(OrderStats stats, List<EmbeddedWrapper> embedded) { this.stats = stats; this.embedded = embedded; } // Getter方法省略 }
- 控制器中构建响应:
@RestController @RequestMapping("/orders") public class OrderController { private final EmbeddedWrappers wrappers = new EmbeddedWrappers(false); @GetMapping public OrderSummaryModel getOrderSummary() { // 1. 模拟统计数据 OrderStats stats = new OrderStats(14, 20); // 2. 模拟订单数据并包装为EntityModel List<EntityModel<Order>> orderEntities = Arrays.asList( EntityModel.of(new Order(123, 30.00, "USD", "shipped"), linkTo(methodOn(OrderController.class).getOrder(123)).withSelfRel(), linkTo(BasketController.class).slash(98712).withRel("ea:basket"), linkTo(CustomerController.class).slash(7809).withRel("ea:customer")), EntityModel.of(new Order(124, 20.00, "USD", "processing"), linkTo(methodOn(OrderController.class).getOrder(124)).withSelfRel(), linkTo(BasketController.class).slash(97213).withRel("ea:basket"), linkTo(CustomerController.class).slash(12369).withRel("ea:customer")) ); // 3. 将订单集合包装为EmbeddedWrapper,指定关联关系为"ea:order" List<EmbeddedWrapper> embeddedWrappers = orderEntities.stream() .map(order -> wrappers.wrap(order, LinkRelation.of("ea:order"))) .collect(Collectors.toList()); // 4. 构建根模型并添加链接 OrderSummaryModel summaryModel = new OrderSummaryModel(stats, embeddedWrappers); summaryModel.add( linkTo(methodOn(OrderController.class).getOrderSummary()).withSelfRel(), linkTo(methodOn(OrderController.class).getOrderSummary()).slash("?page=2").withRel("next"), linkTo(methodOn(OrderController.class).findOrder(null)).withRel("ea:find").withTemplated(true), Link.of("/admins/2", "ea:admin").withTitle("Fred"), Link.of("/admins/5", "ea:admin").withTitle("Kate") ); return summaryModel; } // 辅助方法:模拟单个订单查询 @GetMapping("/{id}") public EntityModel<Order> getOrder(@PathVariable Long id) { return EntityModel.of(new Order(id, 0.0, "USD", "dummy")); } // 辅助方法:模拟订单查询模板 @GetMapping(params = "id") public EntityModel<Order> findOrder(@RequestParam Long id) { return EntityModel.of(new Order(id, 0.0, "USD", "dummy")); } }
方法二:动态构建Model(无需自定义模型类)
如果不想提前定义根模型类,可以使用ModelBuilder动态构建响应:
@GetMapping public Model getOrderSummary() { // 模拟订单数据并包装为EntityModel List<EntityModel<Order>> orderEntities = Arrays.asList( // 内容同方法一 ); List<EmbeddedWrapper> embeddedWrappers = orderEntities.stream() .map(order -> wrappers.wrap(order, LinkRelation.of("ea:order"))) .collect(Collectors.toList()); return ModelBuilder.empty() .addAttribute("currentlyProcessing", 14) .addAttribute("shippedToday", 20) .addAll(embeddedWrappers) .add( linkTo(methodOn(OrderController.class).getOrderSummary()).withSelfRel(), linkTo(methodOn(OrderController.class).getOrderSummary()).slash("?page=2").withRel("next"), linkTo(methodOn(OrderController.class).findOrder(null)).withRel("ea:find").withTemplated(true), Link.of("/admins/2", "ea:admin").withTitle("Fred"), Link.of("/admins/5", "ea:admin").withTitle("Kate") ) .build(); }
说明
两种方式都能让自定义状态属性直接出现在根节点,同时将集合资源正确映射到_embedded节点,完全符合HAL规范,无需额外创建冗余类。
内容的提问来源于stack exchange,提问作者Larmicon
相关产品推荐
相关产品推荐

