Chapter 02

REST API 设计

掌握 @RestController、请求参数绑定、Bean Validation 校验,以及全局异常处理的最佳实践。

REST 是什么

REST(Representational State Transfer,表述性状态转移)不是一种协议或标准,而是 Roy Fielding 在其 2000 年博士论文中提出的软件架构风格。RESTful API 利用 HTTP 协议本身的能力(方法、状态码、URL)来表达资源操作,而无需额外的协议层。

Richardson 成熟度模型将 REST API 分为四个级别:

级别特征示例
Level 0 — 沼泽 POX单一 URI,所有操作 POSTPOST /api 发送 XML/JSON 消息体
Level 1 — 资源多 URI,每种资源有独立地址POST /users、POST /orders
Level 2 — HTTP 动词使用 HTTP 方法区分操作(最常见)GET /users/1、DELETE /users/1
Level 3 — 超媒体响应中包含下一步操作的链接(HATEOAS)响应含 _links: { self, next }

实际项目中绝大多数团队实现到 Level 2 即认为是"RESTful",Level 3 的 HATEOAS 因实现成本高而较少使用。

核心注解名词解释

@RestController
复合注解,等价于 @Controller + @ResponseBody。标注后该类的所有方法返回值都会被 Jackson 序列化为 JSON 写入响应体,而不是解析为视图名称。
@RequestMapping
映射 HTTP 请求到处理方法。可指定 path(URL 路径)、method(HTTP 方法)、consumes(接受的 Content-Type)、produces(返回的 Content-Type)。@GetMapping、@PostMapping 等是其快捷方式。
@PathVariable
从 URL 路径中提取变量。例如 @GetMapping("/{id}") 中的 {id} 可通过 @PathVariable Long id 获取。URL 路径变量名与方法参数名相同时可省略注解内的名称。
@RequestBody
将 HTTP 请求体的 JSON 数据反序列化为 Java 对象。通常用于 POST/PUT 请求,配合 @Valid 触发参数校验。
@Valid / @Validated
触发 Bean Validation(JSR-380)校验。@Valid 来自 javax/jakarta.validation,@Validated 是 Spring 扩展,额外支持分组校验。校验失败时抛出 MethodArgumentNotValidException。
ResponseEntity<T>
表示完整的 HTTP 响应(状态码 + 响应头 + 响应体)。当需要自定义状态码或响应头时使用,如 ResponseEntity.created(uri).body(dto) 返回 201 Created。
@ControllerAdvice
全局控制器增强,配合 @ExceptionHandler 实现统一异常处理。Spring MVC 会将所有 Controller 抛出的异常路由到这里集中处理,避免在每个 Controller 中重复 try-catch。

HTTP 方法语义

正确使用 HTTP 方法是 RESTful 设计的基础,每种方法有明确的语义和幂等性要求:

方法语义幂等请求体典型用途
GET读取资源查询用户、获取列表
POST创建资源新建用户、提交订单
PUT全量更新完整替换用户信息
PATCH部分更新只修改邮箱或昵称
DELETE删除资源删除用户

请求处理链(架构图)

HTTP 请求 │ ▼ ┌─────────────────────────────────────────────────────┐ │ Filter Chain(安全过滤、日志、跨域等) │ └─────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────┐ │ DispatcherServlet(Spring MVC 前端控制器) │ └─────────────────────────────────────────────────────┘ │ ├─── HandlerMapping(查找匹配的 @RequestMapping) │ ├─── HandlerAdapter(调用 Controller 方法) │ │ │ ├── ArgumentResolver(解析 @PathVariable/@RequestBody 等) │ │ │ └── 执行 Controller 方法 │ │ │ ▼ │ Service 层(业务逻辑) │ │ │ ▼ │ Repository 层(数据访问) │ ├─── ReturnValueHandler(序列化返回值为 JSON) │ └─── ExceptionHandlerExceptionResolver(异常统一处理) │ ▼ @ControllerAdvice → @ExceptionHandler │ ▼ HTTP 响应(JSON + 状态码)

完整用户 CRUD API 实现

请求/响应 DTO

// 创建用户请求 DTO(包含校验注解)
@Data  // Lombok:自动生成 getter/setter/toString/equals
public class UserCreateDTO {
    @NotBlank(message = "用户名不能为空")
    @Size(min = 2, max = 20, message = "用户名长度 2-20 位")
    private String username;

    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;

    @NotBlank
    @Size(min = 8, message = "密码至少 8 位")
    @Pattern(regexp = ".*[A-Z].*", message = "密码须含大写字母")
    private String password;
}

// 响应 DTO(不包含密码等敏感字段)
@Data
@Builder
public class UserResponseDTO {
    private Long id;
    private String username;
    private String email;
    private LocalDateTime createdAt;
}

Controller 层

@RestController
@RequestMapping("/users")
@RequiredArgsConstructor  // Lombok:生成包含 final 字段的构造器
public class UserController {

    private final UserService userService;

    // 查询所有用户(支持分页)
    @GetMapping
    public ApiResponse<Page<UserResponseDTO>> listUsers(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size) {
        return ApiResponse.ok(userService.findAll(PageRequest.of(page, size)));
    }

    // 查询单个用户
    @GetMapping("/{id}")
    public ApiResponse<UserResponseDTO> getUser(@PathVariable Long id) {
        return ApiResponse.ok(userService.findById(id));
    }

    // 创建用户(@Valid 触发校验)
    @PostMapping
    public ResponseEntity<ApiResponse<UserResponseDTO>> createUser(
            @Valid @RequestBody UserCreateDTO dto) {
        UserResponseDTO created = userService.create(dto);
        URI location = URI.create("/users/" + created.getId());
        return ResponseEntity.created(location).body(ApiResponse.ok(created));
    }

    // 删除用户
    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void deleteUser(@PathVariable Long id) {
        userService.delete(id);
    }
}

统一响应格式封装

生产级 API 通常会定义一个统一的响应包装类,包含 code(业务状态码)、message(提示信息)和 data(实际数据),方便前端统一处理。

@Data
@AllArgsConstructor
public class ApiResponse<T> {
    private int code;
    private String message;
    private T data;

    public static <T> ApiResponse<T> ok(T data) {
        return new ApiResponse<>(200, "success", data);
    }

    public static <T> ApiResponse<T> error(int code, String message) {
        return new ApiResponse<>(code, message, null);
    }
}

全局异常处理

@RestControllerAdvice  // = @ControllerAdvice + @ResponseBody
public class GlobalExceptionHandler {

    // 处理 @Valid 校验失败
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiResponse<Map<String, String>> handleValidation(
            MethodArgumentNotValidException ex) {
        Map<String, String> errors = new LinkedHashMap<>();
        ex.getBindingResult().getFieldErrors()
          .forEach(e -> errors.put(e.getField(), e.getDefaultMessage()));
        return ApiResponse.error(400, "参数校验失败");
    }

    // 处理资源不存在
    @ExceptionHandler(EntityNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ApiResponse<Void> handleNotFound(EntityNotFoundException ex) {
        return ApiResponse.error(404, ex.getMessage());
    }

    // 处理业务异常
    @ExceptionHandler(BusinessException.class)
    @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
    public ApiResponse<Void> handleBusiness(BusinessException ex) {
        return ApiResponse.error(ex.getCode(), ex.getMessage());
    }

    // 兜底:处理所有未预期异常
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ApiResponse<Void> handleGeneral(Exception ex) {
        // 内部异常不暴露给前端,记录日志
        log.error("Unhandled exception", ex);
        return ApiResponse.error(500, "服务器内部错误");
    }
}
Warning 不要直接将 JPA Entity 作为 API 响应体返回。Entity 可能包含密码哈希、内部 ID 等敏感字段,也可能因为懒加载触发额外 SQL 查询(LazyInitializationException)。始终使用 DTO 作为 API 的输入/输出边界。
Tip 使用 MapStruct 库可以通过注解自动生成 Entity ↔ DTO 的转换代码,比手写 converter 或使用 BeanUtils.copyProperties 更高效、类型安全。