REST 是什么
REST(Representational State Transfer,表述性状态转移)不是一种协议或标准,而是 Roy Fielding 在其 2000 年博士论文中提出的软件架构风格。RESTful API 利用 HTTP 协议本身的能力(方法、状态码、URL)来表达资源操作,而无需额外的协议层。
Richardson 成熟度模型将 REST API 分为四个级别:
| 级别 | 特征 | 示例 |
|---|---|---|
| Level 0 — 沼泽 POX | 单一 URI,所有操作 POST | POST /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 更高效、类型安全。