You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在带自定义属性的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

  1. 定义统计数据类:封装自定义状态属性
public class OrderStats {
    private int currentlyProcessing;
    private int shippedToday;

    public OrderStats(int currentlyProcessing, int shippedToday) {
        this.currentlyProcessing = currentlyProcessing;
        this.shippedToday = shippedToday;
    }

    // Getter方法省略
}
  1. 创建根资源模型:继承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方法省略
}
  1. 控制器中构建响应:
@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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.11 10:24:52