返回 Skill 列表
extension
分类: 开发与工程无需 API Key

java-api-endpoint

按照分层架构约定为Spring Boot项目添加RESTful API端点。涵盖控制器、外观模式、分页、批处理操作以及OpenAPI注解。在添加API端点、创建REST接口或实现查询/变更操作时使用。

person作者: jakexiaohubgithub

API 端点开发

在 Spring Boot 分层架构项目中添加 RESTful API 端点。

前置: 遵守 java-architecture-guide 中的分层原则。

适用场景

  • 新增 RESTful 接口
  • 为现有资源补充分页、详情、启停、批量操作等端点
  • 统一 Controller / Facade 层的接口风格

不适用

  • RPC、消息消费、定时任务等非 HTTP 入口
  • 仅修改 Service / Repository 内部逻辑而不暴露新接口
  • 不遵循该仓库 Java 分层约定的项目

快速工作流

  1. 先确认资源名、路径和返回模型,再决定端点形态
  2. Controller 只做参数接收、校验和响应封装
  3. 读写编排放 Facade,数据查询和落库逻辑放 Service
  4. 最后补齐 @Tag@Operation@Parameter 等 OpenAPI 注解

RESTful 规范

路径格式

/api/v{version}/{resources}
  • 资源名: 复数名词, kebab-case
  • 示例: /api/v1/alert-policies, /api/v1/video-sources

标准 CRUD 映射

| 操作 | 方法 | 路径 | 返回 | |------|------|------|------| | 创建 | POST | /api/v1/policies | ApiResponse<IdResp> | | 更新 | PUT | /api/v1/policies/{id} | ApiResponse<Void> | | 删除 | DELETE | /api/v1/policies/{id} | ApiResponse<Void> | | 详情 | GET | /api/v1/policies/{id} | ApiResponse<DetailDTO> | | 分页列表 | GET | /api/v1/policies?page=1&size=20 | ApiResponse<PageResult<Card>> | | 游标分页 | GET | /api/v1/policies/stream?cursor=&size=20 | ApiResponse<CursorPageResult<Card>> | | 批量操作 | POST | /api/v1/policies/batch/{action} | ApiResponse<BatchResp> |

Controller 模式

类声明

@Slf4j
@RestController
@RequestMapping(path = "/api/v1/policies", produces = MediaType.APPLICATION_JSON_VALUE)
@Tag(name = "预警策略", description = "预警策略管理接口")
@RequiredArgsConstructor
public class PolicyController {
    private final PolicyFacade facade;  // 只注入 Facade
}

必需注解:

  • @Tag(name, description) — OpenAPI 分组
  • @RequestMapping(path, produces=JSON) — 统一 JSON 响应
  • @RequiredArgsConstructor — 构造器注入

统一响应

所有方法返回 ApiResponse<T>:

ApiResponse.ok()              // 无数据成功
ApiResponse.ok(data)          // 带数据成功
ApiResponse.error(code, msg)  // 错误

参数校验

// Request Body 校验
@PostMapping
public ApiResponse<IdResp> create(@Valid @RequestBody PolicyCreateReq req)

// Path Variable 校验 (类上需加 @Validated)
@Validated
@RestController
public class PolicyController {
    @DeleteMapping("/{id}")
    public ApiResponse<Void> delete(
        @NotNull(message = "ID不能为空")
        @Positive(message = "ID必须为正数")
        @PathVariable Long id)
}

5 种端点模板

1. 分页查询 (偏移分页)

适用于小数据量,前端需要页码跳转。

@Operation(summary = "分页查询策略列表")
@GetMapping
public ApiResponse<PageResult<PolicyCard>> getPage(
        @Parameter(description = "页码", example = "1")
        @RequestParam(defaultValue = "1") int page,
        @Parameter(description = "每页大小", example = "20")
        @RequestParam(defaultValue = "20") int size,
        @Parameter(description = "策略名称")
        @RequestParam(required = false) String name,
        @Parameter(description = "状态")
        @RequestParam(required = false) String status) {
    return ApiResponse.ok(facade.getPage(page, size, name, status));
}

2. 游标分页 (大数据量)

适用于大数据量,避免深分页性能问题。

@Operation(summary = "游标分页查询")
@GetMapping("/stream")
public ApiResponse<CursorPageResult<PolicyCard>> getStreamPage(
        @Parameter(description = "游标(上页最后一条ID)")
        @RequestParam(required = false) Long cursor,
        @Parameter(description = "每页大小")
        @RequestParam(defaultValue = "20") int size,
        @Parameter(description = "策略名称")
        @RequestParam(required = false) String name) {
    if (cursor != null && cursor < 0L) {
        cursor = null;
    }
    return ApiResponse.ok(facade.getStreamPage(cursor, size, name));
}

Service 层游标分页实现要点:

LambdaQueryWrapper<Entity> wrapper = new LambdaQueryWrapper<>();
if (cursor != null) {
    wrapper.lt(Entity::getId, cursor);  // ID < cursor
}
wrapper.orderByDesc(Entity::getId)
       .last("LIMIT " + (size + 1));   // 多查一条判断 hasMore

List<Entity> list = mapper.selectList(wrapper);
boolean hasMore = list.size() > size;
if (hasMore) list = list.subList(0, size);

3. 详情查询

@Operation(summary = "查询策略详情")
@GetMapping("/{id}")
public ApiResponse<PolicyDetail> getDetail(
        @Parameter(description = "策略ID", example = "1")
        @PathVariable Long id) {
    return ApiResponse.ok(facade.getDetail(id));
}

4. 创建 / 更新 / 删除

@Operation(summary = "创建策略")
@PostMapping
public ApiResponse<IdResp> create(@Valid @RequestBody PolicyCreateReq req) {
    Long id = facade.create(req);
    return ApiResponse.ok(new IdResp(id));
}

@Operation(summary = "更新策略")
@PutMapping("/{id}")
public ApiResponse<Void> update(
        @Parameter(description = "策略ID") @PathVariable Long id,
        @Valid @RequestBody PolicyUpdateReq req) {
    facade.update(id, req);
    return ApiResponse.ok();
}

@Operation(summary = "删除策略")
@DeleteMapping("/{id}")
public ApiResponse<Void> delete(
        @Parameter(description = "策略ID") @PathVariable Long id) {
    facade.delete(id);
    return ApiResponse.ok();
}

5. 批量操作

@Operation(summary = "批量启动任务")
@PostMapping("/batch/start")
public ApiResponse<TaskBatchResp> batchStart(
        @Valid @RequestBody TaskBatchReq req) {
    return ApiResponse.ok(facade.batchStart(req));
}

Facade 层批量操作模式 (部分成功):

public TaskBatchResp batchStart(TaskBatchReq req) {
    TaskBatchResp resp = new TaskBatchResp();
    for (Long taskId : req.getTaskIds()) {
        try {
            startTask(taskId);
            resp.addSuccess(taskId);
        } catch (Exception e) {
            resp.addFailure(taskId, e.getMessage());
        }
    }
    return resp;
}

Facade 编排模式

写操作: 验证 → 执行 → 返回

@Transactional(rollbackFor = Exception.class)
public Long create(PolicyCreateReq req) {
    // 1. 验证关联数据有效性
    validateRelatedData(req);
    // 2. 调用 Service 执行
    Long id = policyService.create(req);
    // 3. 创建关联数据
    createTimePlans(id, req.getTimePlans());
    return id;
}

读操作: 查询 → 批量关联 → 丰富结果

public PageResult<PolicyCard> getPage(int page, int size, String name, String status) {
    // 1. 查询主数据
    PageResult<PolicyCard> result = policyService.getPage(page, size, name, status);
    // 2. 提取 ID 批量查询关联数据
    List<Long> policyIds = result.getRecords().stream()
            .map(PolicyCard::getId).toList();
    Map<Long, List<TimePlan>> planMap = timePlanService.batchGetByPolicyIds(policyIds);
    // 3. 丰富结果
    result.getRecords().forEach(card ->
            card.setTimePlans(planMap.getOrDefault(card.getId(), List.of())));
    return result;
}

OpenAPI 注解速查

| 注解 | 位置 | 用途 | |------|------|------| | @Tag(name, description) | Controller 类 | API 分组 | | @Operation(summary, description) | 方法 | 操作说明 | | @Parameter(description, example) | 参数 | 参数说明 | | @Schema(description, example) | DTO 字段 | 字段说明 |

启用/禁用端点模式

@Operation(summary = "切换启用状态")
@PatchMapping("/{id}/enabled")
public ApiResponse<Void> switchEnabled(
        @PathVariable Long id,
        @RequestParam boolean enabled) {
    facade.switchEnabled(id, enabled);
    return ApiResponse.ok();
}

Facade 层:

@Transactional(rollbackFor = Exception.class)
public void switchEnabled(Long id, boolean enabled) {
    if (enabled) {
        // 启用前检查依赖是否就绪
        validateDependenciesReady(id);
    } else {
        // 禁用前检查是否被其他模块使用
        checkNotInUse(id);
    }
    policyService.switchEnabled(id, enabled);
}

Checklist

实现前:

  • [ ] 已确认资源名使用复数 kebab-case,且路径与语义匹配
  • [ ] 已确认返回 DTO / 分页模型 / 批量响应模型
  • [ ] 已确认该接口是读操作、写操作还是批量操作

实现后:

  • [ ] Controller 只注入 Facade,没有直接调用 Service
  • [ ] 所有接口统一返回 ApiResponse<T>
  • [ ] 请求参数包含必要的校验注解和 OpenAPI 注解
  • [ ] 写操作的事务边界放在 Facade / Service 合适位置

常见错误

| 错误做法 | 正确做法 | |----------|----------| | 用单数路径如 /api/v1/policy | 使用复数资源路径,如 /api/v1/policies | | Controller 里直接编排多个 Service | 把跨服务编排上推到 Facade | | 为列表查询直接返回 Entity | 返回 Card / Detail 等 DTO | | 批量接口逐条抛异常中断 | 明确设计成全成功或部分成功,并返回结构化结果 |