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 更高效、类型安全。
HTTP 状态码最佳实践
正确的 HTTP 状态码是 RESTful API 契约的重要组成部分。以下是常见场景与对应状态码的规范使用:
| 场景 | 状态码 | 说明 |
|---|---|---|
| 创建资源成功 | 201 Created | 响应头应包含 Location: /users/123 |
| 删除资源成功 | 204 No Content | 响应体为空 |
| 请求参数校验失败 | 400 Bad Request | 响应体包含具体字段错误信息 |
| 未登录/Token 无效 | 401 Unauthorized | 需要认证 |
| 已登录但无权限 | 403 Forbidden | 有认证但没有授权 |
| 资源不存在 | 404 Not Found | -- |
| 邮箱/手机号冲突 | 409 Conflict | 业务唯一约束违反 |
| 未预期服务端错误 | 500 Internal Server Error | 不要泄露内部堆栈给客户端 |
Bean Validation 进阶:自定义校验注解
标准的 @NotBlank、@Email 无法满足复杂业务校验时,可以自定义注解:
// 1. 定义校验注解
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneNumberValidator.class)
public @interface ChinesePhone {
String message() default "手机号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// 2. 实现校验逻辑
public class PhoneNumberValidator
implements ConstraintValidator<ChinesePhone, String> {
// 中国大陆手机号正则:1 开头,第二位 3-9,共 11 位
private static final Pattern PATTERN =
Pattern.compile("^1[3-9]\\d{9}$");
@Override
public boolean isValid(String value, ConstraintValidatorContext ctx) {
if (value == null) return true; // null 由 @NotBlank 处理
return PATTERN.matcher(value).matches();
}
}
// 3. 在 DTO 中使用
@Data
public class RegisterDTO {
@NotBlank
@ChinesePhone // 自定义校验注解
private String phone;
}
分页查询的标准实现
Spring Data 提供 Pageable 接口和 Page<T> 结果类,实现分页无需手写 LIMIT/OFFSET SQL:
// Controller:接收分页参数
@GetMapping
public ApiResponse<PageResult<UserResponseDTO>> listUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "createdAt") String sortBy) {
// 构建分页和排序对象
Pageable pageable = PageRequest.of(page, size,
Sort.by(Sort.Direction.DESC, sortBy));
Page<UserResponseDTO> result = userService.findAll(pageable);
// 包装分页元信息
return ApiResponse.ok(PageResult.of(result));
}
// 自定义分页结果封装(比直接返回 Page<T> 更友好)
@Data
@AllArgsConstructor
public class PageResult<T> {
private List<T> content;
private long total; // 总记录数
private int page; // 当前页码
private int size; // 每页大小
private int totalPages; // 总页数
public static <T> PageResult<T> of(Page<T> page) {
return new PageResult<>(
page.getContent(),
page.getTotalElements(),
page.getNumber(),
page.getSize(),
page.getTotalPages()
);
}
}
Warning
分页参数 size 应设置合理的上限(如最大 100),防止客户端传入 size=999999 导致单次查询返回大量数据压垮数据库和网络。在 Controller 层做入参校验:
@RequestParam @Max(100) int size。
本章小结
REST API 设计的核心:正确使用 HTTP 方法(GET/POST/PUT/PATCH/DELETE)和状态码(201/204/400/404/409/500)表达语义;@RestController + @Valid + 全局异常处理构成 Spring MVC 的完整请求处理链;DTO 模式隔离 API 层与数据库层,是安全和可维护性的基础;Bean Validation 支持自定义注解满足复杂业务校验;分页查询应封装统一的 PageResult 结构,并限制最大 size 参数。