在 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;
}

// getter and setter
}

实际项目可以使用 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;

// getter and setter
}

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,接口行为也更稳定,前端和调用方能得到一致的错误格式。