SpringNotes

第 10 章:Web 与 REST

zjc 于 2026-01-10 发布

这是《Spring Boot 与 Spring Cloud 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 Web 层是系统的入口,也是参数校验、异常处理、安全、文档、监控和幂等设计的汇聚点。Spring MVC 提供了强大的注解模型,但接口是否稳定、可维护,取决于团队契约设计。

10.1 第一个 REST 接口

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

    private final OrderService orderService;

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    @GetMapping("/{id}")
    public OrderView get(@PathVariable Long id) {
        return orderService.get(id);
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public OrderView create(@Valid @RequestBody CreateOrderRequest request) {
        return orderService.create(request);
    }
}

依赖:

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

10.2 请求映射

注解 说明
@GetMapping 查询
@PostMapping 创建或非幂等动作
@PutMapping 全量更新
@PatchMapping 部分更新
@DeleteMapping 删除
@RequestMapping 通用映射

参数绑定:

注解 来源
@PathVariable 路径
@RequestParam Query
@RequestBody 请求体
@RequestHeader Header
@CookieValue Cookie
@ModelAttribute 对象绑定
@RequestPart multipart

URL 设计建议:

GET    /api/orders                 查询列表
GET    /api/orders/{id}            查询详情
POST   /api/orders                 创建
PUT    /api/orders/{id}            全量更新
PATCH  /api/orders/{id}            部分更新
POST   /api/orders/{id}/cancel     非幂等业务动作

10.3 请求与响应模型

不要直接暴露实体:

public record CreateOrderRequest(
        @NotBlank @Size(max = 64) String userId,
        @NotNull @Positive Long skuId,
        @Min(1) @Max(100) int quantity) {
}

public record OrderView(
        Long id,
        String status,
        BigDecimal amount,
        Instant createdAt) {
}

好处:

  1. 避免 JPA 懒加载序列化问题;
  2. 隐藏内部字段;
  3. API 契约稳定;
  4. 输入输出权限不同;
  5. 更容易编写 OpenAPI 文档。

金额与时间建议:

类型 建议
金额 BigDecimal 或明确的最小单位整数
时间 Instant,序列化为 ISO-8601
枚举 字符串或明确编码
ID 明确类型和语义

10.4 参数校验

public record CreateAddressRequest(
        @NotBlank String country,
        @NotBlank String city,
        @NotBlank String detail,
        @Pattern(regexp = "^1[3-9]\\d{9}$") String phone) {
}

嵌套校验:

public record CreateOrderRequest(
        @NotBlank String userId,
        @NotNull @Valid Address address) {
}

校验异常处理:

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleValidation(MethodArgumentNotValidException ex) {
        List<FieldError> errors = ex.getBindingResult().getFieldErrors();
        return new ErrorResponse("VALIDATION_ERROR", errors.get(0).getDefaultMessage());
    }
}

校验要同时考虑:

  1. 语法校验;
  2. 业务规则校验;
  3. 权限校验;
  4. 幂等校验;
  5. 大小限制。

10.5 统一响应

常见格式:

public record ApiResponse<T>(
        String code,
        String message,
        T data,
        String traceId) {

    public static <T> ApiResponse<T> ok(T data) {
        return new ApiResponse<>("OK", "success", data, TraceContext.currentId());
    }
}

是否包装统一响应要看团队契约。若客户端明确依赖 HTTP 状态码和资源模型,强制包装会破坏 REST 风格;若企业内部已有统一网关和 SDK,统一结构可能更方便。

关键是保持一致,不混用多套约定。

10.6 异常处理

@RestControllerAdvice
public class ApiExceptionHandler {

    @ExceptionHandler(OrderNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ErrorResponse notFound(OrderNotFoundException ex) {
        return new ErrorResponse("ORDER_NOT_FOUND", ex.getMessage());
    }

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ErrorResponse unexpected(Exception ex) {
        log.error("unexpected error", ex);
        return new ErrorResponse("INTERNAL_ERROR", "system busy");
    }
}

原则:

  1. 业务异常返回业务码;
  2. 系统异常不泄露堆栈;
  3. 记录 traceId;
  4. 日志包含请求上下文;
  5. 客户端能看到稳定契约;
  6. 服务端保留完整堆栈。

10.7 拦截器与过滤器

Filter 位于 Servlet 层:

@Component
public class TraceIdFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {
        String traceId = Optional.ofNullable(request.getHeader("X-Trace-Id"))
                .orElseGet(() -> UUID.randomUUID().toString());
        TraceContext.set(traceId);
        response.setHeader("X-Trace-Id", traceId);
        try {
            chain.doFilter(request, response);
        } finally {
            TraceContext.clear();
        }
    }
}

Interceptor 位于 Spring MVC 层:

@Component
public class AuthInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) {
        return checkToken(request);
    }
}

选择:

组件 适合
Filter TraceId、编码、CORS、请求日志
Interceptor 认证授权、_handler 级监控
AOP Service 方法级横切逻辑
ControllerAdvice 异常与模型绑定

10.8 分页与列表

请求:

public record PageQuery(
        @Min(1) Integer page,
        @Min(1) @Max(100) Integer size,
        String sort) {
    public int page() {
        return page == null ? 1 : page;
    }
    public int size() {
        return size == null ? 20 : size;
    }
}

响应:

public record PageResponse<T>(
        List<T> items,
        long total,
        int page,
        int size) {
}

必须限制最大 size,否则深分页或超大 pageSize 会拖垮数据库和应用。

10.9 幂等设计

创建订单需要幂等键:

POST /api/orders
Idempotency-Key: 2b0d7a1e-...

处理流程:

1. 查询幂等键
   -> 已完成:返回原结果
   -> 处理中:返回 409 或稍后重试
2. 插入幂等记录,可加唯一约束
3. 执行业务事务
4. 更新结果

并非所有接口都必须幂等。支付、扣库存、发券等写操作必须有明确幂等策略;纯查询天然幂等。

10.10 文件上传下载

限制大小:

spring.servlet.multipart.max-file-size=20MB
spring.servlet.multipart.max-request-size=25MB

上传:

@PostMapping(value = "/files", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public FileView upload(@RequestParam("file") MultipartFile file) {
    return storageService.upload(file);
}

注意:

  1. 校验扩展名和 Content-Type;
  2. 校验文件头;
  3. 重命名存储;
  4. 限制大小;
  5. 不把文件全部读入内存;
  6. 下载大文件使用流;
  7. 病毒扫描按需接入;
  8. 权限校验不能只依赖 URL。

10.11 OpenAPI

依赖:

<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>...</version>
</dependency>

注解:

@Operation(summary = "Create order")
@PostMapping
public OrderView create(@RequestBody CreateOrderRequest request) {
}

生产建议:

  1. API 变更走契约评审;
  2. 破坏性变更升版本;
  3. Swagger UI 不暴露到公网;
  4. 文档与代码同步生成;
  5. 异常模型也写入文档。

本章小结

Spring MVC 提供了注解化的 REST 开发模型,生产接口还需要统一模型、参数校验、异常处理、分页限制、幂等键、日志追踪和安全控制。Web 层应保持薄,只做协议适配和校验,业务规则下沉到 Service 或领域层,避免 Controller 膨胀。

思考题

  1. 为什么不建议 Controller 直接返回数据库实体?
  2. Filter、Interceptor、AOP 各适合什么横切逻辑?
  3. 如何设计创建订单接口的幂等?
  4. 分页接口为什么必须限制最大 size?
  5. 统一响应包装有哪些取舍?