下载工作台
Spring Boot Web 开发

RESTful API 与统一响应

试读上半部分 · 解锁后可读全文

第 7 章 · RESTful API 与统一响应

本章目标:使用 ResponseEntity 精确控制 HTTP 状态码与响应头;实现 Pageable 分页与排序;设计 Result<T> 统一包装(code/message/data);完成商品 CRUD REST API;处理 @Valid 校验与全局异常映射;对照 shop-demo DRF 的 Response、Pagination、Serializer 说明异同;让 shop-spring-demo 的 JSON API 达到可对接虚构 api.example.com 前端 SDK 的契约水平。

学时建议:6~7 小时(含 3 小时跟练)

前置:完成 spring-boot-web ch06java-dev ch08 异常;了解 HTTP 状态码语义。


7.1 场景说明:契约稳定的 JSON API

移动端与 SPA 从 https://api.example.com/api/v1/products 拉取分页商品;创建/更新需 Session 或 Token(ch06 Session / ch15 JWT)。响应格式需统一,避免有的接口返回裸数组、有的返回 {error: ...}

需求ch03 现状ch07 目标
列表List<ProductDto>Result<PageResult<ProductDto>>
创建未实现201 + Location 头
错误500 堆栈400/404/409 + 统一 code
分页limit 参数Pageable page/size/sort

shop-demo DRF 常用:

{"count": 100, "next": "...", "previous": null, "results": [...]}

shop-spring-demo 采用更贴近国内团队的 {code, message, data} 包装,并在 data 内放分页元信息。


7.2 统一 Result 模型

package com.example.shop.common.api;

public record Result<T>(
        int code,
        String message,
        T data
) {
    public static <T> Result<T> ok(T data) {
        return new Result<>(0, "success", data);
    }

    public static <T> Result<T> ok() {
        return ok(null);
    }

    public static <T> Result<T> fail(int code, String message) {
        return new Result<>(code, message, null);
    }
}
public record PageResult<T>(
        List<T> items,
        long total,
        int page,
        int size,
        int totalPages
) {}
字段含义
code业务码,0 成功,非 0 失败
message人类可读说明
data载荷,可为 PageResult、单对象、null

Django DRF:HTTP 状态 + body 内 detail;Spring 本课程同时规范 HTTP 状态与 body code


7.3 分页 Pageable

7.3.1 Controller

@GetMapping
public Result<PageResult<ProductDto>> list(
        @RequestParam(required = false) String q,
        @PageableDefault(size = 20, sort = "id", direction = Sort.Direction.DESC)
        Pageable pageable) {

    Page<ProductDto> page = catalogService.search(q, pageable);
    PageResult<ProductDto> body = new PageResult<>(
            page.getContent(),
            page.getTotalElements(),
            page.getNumber(),
            page.getSize(),
            page.getTotalPages()
    );
    return Result.ok(body);
}

7.3.2 Service

public Page<ProductDto> search(String q, Pageable pageable) {
    Page<Product> page;
    if (q == null || q.isBlank()) {
        page = productRepository.findByActiveTrue(pageable);
    } else {
        page = productRepository.findByActiveTrueAndNameContainingIgnoreCase(q.trim(), pageable);
    }
    return page.map(this::toDto);
}

7.3.3 请求示例

GET /api/v1/products?page=0&size=10&sort=price,desc
参数默认DRF 对照
page0page(DRF 常从 1 起,需文档说明)
size20page_size
sortid,descordering=-price

文档中写清 page 从 0 开始,避免前端 off-by-one。


7.4 ResponseEntity 精细控制

7.4.1 创建 201

@PostMapping
@PreAuthorize("hasRole('STAFF')")
public ResponseEntity<Result<ProductDto>> create(
        @Valid @RequestBody ProductCreateRequest req) {

    ProductDto created = catalogService.create(req);
    URI location = ServletUriComponentsBuilder.fromCurrentRequest()
            .path("/{id}")
            .buildAndExpand(created.id())
            .toUri();

    return ResponseEntity.created(location).body(Result.ok(created));
}

7.4.2 更新与删除

@PutMapping("/{id}")
@PreAuthorize("hasRole('STAFF')")
public Result<ProductDto> update(@PathVariable Long id,
                                 @Valid @RequestBody ProductUpdateRequest req) {
    return Result.ok(catalogService.update(id, req));
}

@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Result<Void>> delete(@PathVariable Long id) {
    catalogService.delete(id);
    return ResponseEntity.ok(Result.ok());
}
操作HTTPBody
创建201Result<ProductDto>
更新200Result<ProductDto>
删除200 或 204可选空 body
查询200Result<PageResult> / Result<ProductDto>

7.5 请求 DTO 与校验

public record ProductCreateRequest(
        @NotBlank @Size(max = 32) String sku,
        @NotBlank @Size(max = 128) String name,
        @NotNull @DecimalMin("0.01") BigDecimal price,
        @NotNull @Min(1) Long categoryId,
        @Min(0) Integer stock
) {}

Controller 类加 @Validated;方法参数 @Valid @RequestBody

校验失败由全局异常处理转换为:

{"code": 40001, "message": "sku: SKU 不能为空", "data": null}

Django DRF:serializer.is_valid(raise_exception=True) → 400 field errors。


7.6 全局异常处理 @RestControllerAdvice

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Result<Void>> handleValidation(MethodArgumentNotValidException ex) {

以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。