在 Spring Boot 接口开发中,如果每个 Controller 都手动 try-catch,代码会很快变得重复且不一致。更好的方式是定义统一返回结构、业务异常和全局异常处理器,让 Controller 专注业务流程。
本文介绍一个常见实践:成功响应返回统一 JSON,业务失败抛出自定义异常,参数校验和未知异常由全局处理器统一转换。
定义统一返回结构
先定义一个通用响应类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class ApiResponse<T> { private String code; private String message; private T data;
public static <T> ApiResponse<T> success(T data) { ApiResponse<T> response = new ApiResponse<>(); response.setCode("OK"); response.setMessage("success"); response.setData(data); return response; }
public static <T> ApiResponse<T> error(String code, String message) { ApiResponse<T> response = new ApiResponse<>(); response.setCode(code); response.setMessage(message); return response; }
}
|
实际项目可以使用 Lombok 简化 getter、setter,但核心结构应该清晰。
成功响应示例:
1 2 3 4 5 6 7 8
| { "code": "OK", "message": "success", "data": { "id": 1, "name": "clang" } }
|
失败响应示例:
1 2 3 4
| { "code": "USER_NOT_FOUND", "message": "用户不存在" }
|
定义业务异常
业务异常用于表达可预期的业务失败,例如用户不存在、余额不足、权限不足。
1 2 3 4 5 6 7 8 9 10 11 12
| public class BusinessException extends RuntimeException { private final String code;
public BusinessException(String code, String message) { super(message); this.code = code; }
public String getCode() { return code; } }
|
也可以定义错误码枚举:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public enum ErrorCode { USER_NOT_FOUND("USER_NOT_FOUND", "用户不存在"), PARAM_INVALID("PARAM_INVALID", "参数错误");
private final String code; private final String message;
ErrorCode(String code, String message) { this.code = code; this.message = message; }
public String getCode() { return code; }
public String getMessage() { return message; } }
|
这样错误码更集中,避免字符串散落在业务代码中。
全局异常处理器
使用 @RestControllerAdvice:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| @RestControllerAdvice public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(BusinessException.class) public ResponseEntity<ApiResponse<Void>> handleBusinessException(BusinessException ex) { return ResponseEntity .badRequest() .body(ApiResponse.error(ex.getCode(), ex.getMessage())); }
@ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<ApiResponse<Void>> handleValidationException(MethodArgumentNotValidException ex) { String message = ex.getBindingResult() .getFieldErrors() .stream() .findFirst() .map(FieldError::getDefaultMessage) .orElse("参数错误");
return ResponseEntity .badRequest() .body(ApiResponse.error("PARAM_INVALID", message)); }
@ExceptionHandler(Exception.class) public ResponseEntity<ApiResponse<Void>> handleException(Exception ex) { log.error("unhandled exception", ex); return ResponseEntity .status(HttpStatus.INTERNAL_SERVER_ERROR) .body(ApiResponse.error("INTERNAL_ERROR", "服务器内部错误")); } }
|
未知异常要记录完整堆栈,但响应中不要暴露 SQL、文件路径、服务器 IP、密钥等内部信息。
Controller 中的使用方式
Controller 不需要 try-catch:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @RestController @RequestMapping("/users") public class UserController {
private final UserService userService;
public UserController(UserService userService) { this.userService = userService; }
@GetMapping("/{id}") public ApiResponse<UserVO> getUser(@PathVariable Long id) { UserVO user = userService.getUser(id); return ApiResponse.success(user); } }
|
Service 中抛出业务异常:
1 2 3 4 5 6
| public UserVO getUser(Long id) { User user = userRepository.findById(id) .orElseThrow(() -> new BusinessException("USER_NOT_FOUND", "用户不存在"));
return convert(user); }
|
这样业务代码只表达业务逻辑,错误转换交给全局处理器。
参数校验
请求 DTO:
1 2 3 4 5 6 7 8 9 10 11 12
| public class CreateUserRequest {
@NotBlank(message = "用户名不能为空") @Size(min = 3, max = 32, message = "用户名长度必须在3到32之间") private String username;
@NotBlank(message = "邮箱不能为空") @Email(message = "邮箱格式不正确") private String email;
}
|
Controller 中启用校验:
1 2 3 4 5
| @PostMapping public ApiResponse<UserVO> createUser(@Valid @RequestBody CreateUserRequest request) { UserVO user = userService.createUser(request); return ApiResponse.success(user); }
|
如果参数不合法,会抛出 MethodArgumentNotValidException,被全局异常处理器捕获。
HTTP 状态码如何选择
统一返回结构不代表所有响应都返回 200。推荐:
- 成功:200 或 201。
- 参数错误:400。
- 未登录:401。
- 无权限:403。
- 资源不存在:404。
- 冲突:409。
- 服务器未知错误:500。
业务错误码用于前端展示和业务判断,HTTP 状态码用于表达协议层语义。二者不冲突。
常见问题
不要在 Controller 捕获所有异常后返回字符串,这会破坏统一结构,也会丢失堆栈。
不要把所有异常都当成 200 返回。网关、监控和调用方需要正确状态码判断请求是否成功。
不要把未知异常的详细信息直接返回给前端。日志中记录即可。
业务异常不要滥用。编程错误、空指针、数据库连接失败不应该包装成“业务失败”。
小结
Spring Boot 统一异常处理的核心是:定义统一响应结构,使用业务异常表达可预期失败,用 @RestControllerAdvice 统一处理业务异常、参数校验异常和未知异常。Controller 不再重复 try-catch,接口行为也更稳定,前端和调用方能得到一致的错误格式。