这是《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) {
}
好处:
- 避免 JPA 懒加载序列化问题;
- 隐藏内部字段;
- API 契约稳定;
- 输入输出权限不同;
- 更容易编写 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());
}
}
校验要同时考虑:
- 语法校验;
- 业务规则校验;
- 权限校验;
- 幂等校验;
- 大小限制。
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");
}
}
原则:
- 业务异常返回业务码;
- 系统异常不泄露堆栈;
- 记录 traceId;
- 日志包含请求上下文;
- 客户端能看到稳定契约;
- 服务端保留完整堆栈。
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);
}
注意:
- 校验扩展名和 Content-Type;
- 校验文件头;
- 重命名存储;
- 限制大小;
- 不把文件全部读入内存;
- 下载大文件使用流;
- 病毒扫描按需接入;
- 权限校验不能只依赖 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) {
}
生产建议:
- API 变更走契约评审;
- 破坏性变更升版本;
- Swagger UI 不暴露到公网;
- 文档与代码同步生成;
- 异常模型也写入文档。
本章小结
Spring MVC 提供了注解化的 REST 开发模型,生产接口还需要统一模型、参数校验、异常处理、分页限制、幂等键、日志追踪和安全控制。Web 层应保持薄,只做协议适配和校验,业务规则下沉到 Service 或领域层,避免 Controller 膨胀。
思考题
- 为什么不建议 Controller 直接返回数据库实体?
- Filter、Interceptor、AOP 各适合什么横切逻辑?
- 如何设计创建订单接口的幂等?
- 分页接口为什么必须限制最大 size?
- 统一响应包装有哪些取舍?