mcpField
H
hateoas

hateoas

github.com/rrezartprebreza开发32887 次安装6 天前更新
代码仓库

关于 hateoas

用于构建超媒体 API 响应的 Spring HATEOAS 指南,通过链接呈现资源和可用操作。面向 Spring API 开发者,涵盖可复用的组装器和分页链接。

主要功能

  1. 01带链接的资源表示
  2. 02按状态提供操作链接
  3. 03可复用的表示组装器
  4. 04自动生成分页链接
  5. 05类型安全的链接生成

适用场景

  1. 01为 API 资源添加导航链接
  2. 02仅呈现当前有效的操作
  3. 03返回带导航链接的分页集合

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/rrezartprebreza/spring-boot-skills --skill hateoas

安装后在 Claude Code 中输入/hateoas确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

agents/
openai.yaml
examples/
bad-hateoas-controller.java
good-hateoas-controller.java
templates/
OrderModelAssembler.java

详细介绍

Spring HATEOAS

依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>

何时添加链接

  • self — 每个资源响应中始终包含
  • collection — 链接回列表端点
  • related resources — 当客户端通常需要跳转到这些资源时
  • actions — 指向状态转换的链接(例如,cancel、ship),仅当当前状态允许时添加

资源模型

@Getter // Jackson serializes through getters — without them the body is only "_links"
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class OrderModel extends RepresentationModel<OrderModel> {
    private final UUID id;
    private final String status;
    private final String customerEmail;
    private final Instant createdAt;

    // Static factory with links
    public static OrderModel from(Order order) {
        OrderModel model = new OrderModel(
            order.getId(), order.getStatus().name(),
            order.getCustomerEmail(), order.getCreatedAt()
        );

        // Self link — always
        model.add(linkTo(methodOn(OrderController.class).getById(order.getId())).withSelfRel());

        // Collection link
        model.add(linkTo(methodOn(OrderController.class).list(null)).withRel("orders"));

        // Conditional action links based on state
        if (order.getStatus() == OrderStatus.PENDING) {
            model.add(linkTo(methodOn(OrderController.class)
                .cancelOrder(order.getId())).withRel("cancel"));
        }
        if (order.getStatus() == OrderStatus.PROCESSING) {
            model.add(linkTo(methodOn(OrderController.class)
                .shipOrder(order.getId())).withRel("ship"));
        }

        return model;
    }
}

控制器

@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {

    @GetMapping("/{id}")
    public ResponseEntity<OrderModel> getById(@PathVariable UUID id) {
        Order order = orderService.findById(id);
        return ResponseEntity.ok(OrderModel.from(order));
    }

    @GetMapping
    public ResponseEntity<CollectionModel<OrderModel>> list(Pageable pageable) {
        Page<Order> orders = orderService.findAll(pageable);

        List<OrderModel> models = orders.getContent().stream()
            .map(OrderModel::from)
            .toList();

        CollectionModel<OrderModel> collection = CollectionModel.of(models,
            linkTo(methodOn(OrderController.class).list(pageable)).withSelfRel()
        );

        // Pagination links
        if (orders.hasNext()) {
            collection.add(linkTo(methodOn(OrderController.class)
                .list(pageable.next())).withRel(IanaLinkRelations.NEXT));
        }
        if (orders.hasPrevious()) {
            collection.add(linkTo(methodOn(OrderController.class)
                .list(pageable.previousOrFirst())).withRel(IanaLinkRelations.PREV));
        }

        return ResponseEntity.ok(collection);
    }
}

响应结构

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "PENDING",
  "customerEmail": "[email protected]",
  "_links": {
    "self": { "href": "http://api.example.com/api/v1/orders/550e8400" },
    "orders": { "href": "http://api.example.com/api/v1/orders" },
    "cancel": { "href": "http://api.example.com/api/v1/orders/550e8400/cancel" }
  }
}

RepresentationModelAssembler 模式

  • Spring 推荐使用此方式从实体构建 HATEOAS 模型
  • 实现 RepresentationModelAssembler<Entity, Model> — 可在多个控制器之间复用
  • 将 assembler 注入控制器,而不是直接调用 Model.from()
@Component
public class OrderModelAssembler implements RepresentationModelAssembler<Order, EntityModel<OrderResponse>> {

    @Override
    public EntityModel<OrderResponse> toModel(Order order) {
        EntityModel<OrderResponse> model = EntityModel.of(OrderResponse.from(order),
            linkTo(methodOn(OrderController.class).getById(order.getId())).withSelfRel(),
            linkTo(methodOn(OrderController.class).list(null)).withRel("orders"));

        if (order.getStatus() == OrderStatus.PENDING) {
            model.add(linkTo(methodOn(OrderController.class)
                .cancelOrder(order.getId())).withRel("cancel"));
        }
        return model;
    }
}

用于分页集合的 PagedModel

  • 使用 PagedResourcesAssembler 自动生成分页链接(first、prev、next、last)
  • 将 PagedResourcesAssembler<Order> 注入控制器 — Spring 会自动创建它
@GetMapping
public ResponseEntity<PagedModel<EntityModel<OrderResponse>>> list(
        Pageable pageable, PagedResourcesAssembler<Order> pagedAssembler) {
    Page<Order> orders = orderService.findAll(pageable);
    PagedModel<EntityModel<OrderResponse>> pagedModel =
        pagedAssembler.toModel(orders, orderModelAssembler);
    return ResponseEntity.ok(pagedModel);
}

注意事项

  • Agent 不论状态如何都会添加所有链接 — 仅当操作有效时才添加操作链接
  • Agent 在链接中硬编码 URL — 始终使用 linkTo(methodOn(...)) 来生成类型安全的链接
  • Agent 返回普通 DTO — 使用 EntityModel.of(dto, links...) 包装,或继承 RepresentationModel
  • Agent 将链接逻辑放在控制器中 — 提取到 RepresentationModelAssembler
  • Agent 手动构建分页链接 — 改用 PagedResourcesAssembler
  • Agent 忘记 self 链接 — 每个资源都必须包含 self 链接

评论

支持 Markdown