2. @ControllerAdvice 与 @ExceptionHandler:基础用法、优先级规则、返回自定义错误响应体
在 Spring Boot 错误处理体系中,@ControllerAdvice 与 @ExceptionHandler 是构建全局异常拦截器的核心组合。它们允许你集中定义异常处理逻辑,避免在每个 Controller 中重复编写 try-catch 代码,同时保证错误响应格式的统一性。
2.1 基础用法:全局异常拦截器
@ControllerAdvice 是一个组件注解,用于声明一个“全局控制器增强器”。它默认会拦截所有 @RequestMapping 方法抛出的异常,配合 @ExceptionHandler 指定具体处理的异常类型。
核心机制:
@ControllerAdvice 内部通过 AOP 原理,在控制器方法执行前后织入增强逻辑。当方法抛出异常时,Spring 会遍历所有注册的 @ControllerAdvice 类,找到匹配的 @ExceptionHandler 方法进行处理。
标准实现步骤:
- 创建一个类,标注
@ControllerAdvice。 - 在类中定义方法,标注
@ExceptionHandler,参数为要捕获的异常类型。 - 方法返回
ResponseEntity或自定义响应体,并设置 HTTP 状态码。
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleResourceNotFound(ResourceNotFoundException ex) {
ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
ex.getMessage(),
System.currentTimeMillis()
);
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ErrorResponse> handleIllegalArgument(IllegalArgumentException ex) {
ErrorResponse error = new ErrorResponse(
HttpStatus.BAD_REQUEST.value(),
"参数校验失败: " + ex.getMessage(),
System.currentTimeMillis()
);
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneric(Exception ex) {
ErrorResponse error = new ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"服务器内部错误,请稍后重试",
System.currentTimeMillis()
);
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
最佳实践: 始终在全局处理器中保留一个
@ExceptionHandler(Exception.class) 兜底方法,确保所有未明确处理的异常都能返回友好的错误信息,避免直接暴露堆栈跟踪给客户端。
2.2 优先级规则:谁先处理?
当多个 @ExceptionHandler 方法都能处理同一个异常时,Spring 遵循明确的优先级规则。理解这些规则是避免“异常被错误拦截”的关键。
| 优先级 | 规则 | 说明 |
|---|---|---|
| 1(最高) | 异常类型精确匹配 | 优先选择参数类型与抛出异常类型完全一致的方法 |
| 2 | 异常类型继承链最近 | 若无精确匹配,选择继承层次中最近的父类异常处理方法 |
| 3 | Controller 局部 > 全局 | Controller 类内部的 @ExceptionHandler 优先级高于 @ControllerAdvice 全局处理器 |
| 4(最低) | 兜底 Exception.class | 所有异常最终可被 Exception.class 捕获,作为最后防线 |
常见陷阱: 如果同时定义了
@ExceptionHandler(IllegalArgumentException.class) 和 @ExceptionHandler(RuntimeException.class),当抛出 IllegalArgumentException 时,Spring 会优先选择精确匹配的 IllegalArgumentException 方法,而不是父类 RuntimeException 的方法。不要依赖方法定义的顺序来决定优先级。
优先级决策流程:
2.3 返回自定义错误响应体
为了给客户端提供一致的错误结构,建议定义统一的错误响应 DTO。这有助于前端解析错误信息,也便于 API 文档的生成。
标准错误响应体设计:
public class ErrorResponse {
private int status; // HTTP 状态码
private String message; // 错误描述
private long timestamp; // 错误发生时间戳
private String path; // 请求路径(可选)
private String errorCode; // 业务错误码(可选)
// 全参构造方法
public ErrorResponse(int status, String message, long timestamp) {
this.status = status;
this.message = message;
this.timestamp = timestamp;
}
// 带路径的构造方法
public ErrorResponse(int status, String message, long timestamp, String path) {
this.status = status;
this.message = message;
this.timestamp = timestamp;
this.path = path;
}
// Getters and Setters
public int getStatus() { return status; }
public String getMessage() { return message; }
public long getTimestamp() { return timestamp; }
public String getPath() { return path; }
public String getErrorCode() { return errorCode; }
public void setErrorCode(String errorCode) { this.errorCode = errorCode; }
}
在全局处理器中注入请求路径:
@ControllerAdvice
public class EnhancedGlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(
ResourceNotFoundException ex,
HttpServletRequest request) {
ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
ex.getMessage(),
System.currentTimeMillis(),
request.getRequestURI()
);
error.setErrorCode("RESOURCE_NOT_FOUND");
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(
MethodArgumentNotValidException ex,
HttpServletRequest request) {
String message = ex.getBindingResult().getFieldErrors().stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage())
.collect(Collectors.joining(", "));
ErrorResponse error = new ErrorResponse(
HttpStatus.BAD_REQUEST.value(),
message,
System.currentTimeMillis(),
request.getRequestURI()
);
error.setErrorCode("VALIDATION_FAILED");
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
}
增强建议: 在错误响应中加入
errorCode 字段,用于标识业务层面的错误类型(如 INVALID_PARAM、USER_NOT_FOUND)。这比仅依赖 HTTP 状态码更精确,便于客户端做差异化处理。
2.4 多 ControllerAdvice 共存时的优先级
当项目中存在多个 @ControllerAdvice 类时,Spring 通过 @Order 注解或 Ordered 接口控制执行顺序。
| 排序方式 | 示例 | 优先级 |
|---|---|---|
@Order(1) |
特定模块的异常处理器 | 高(数值越小优先级越高) |
@Order(2) |
通用异常处理器 | 中 |
无 @Order |
兜底处理器 | 低(默认最低优先级) |
@Order(1)
@ControllerAdvice(basePackages = "com.example.order")
public class OrderModuleExceptionHandler {
@ExceptionHandler(OrderNotFoundException.class)
public ResponseEntity<ErrorResponse> handleOrderNotFound(OrderNotFoundException ex) {
// 订单模块专属处理
}
}
@Order(2)
@ControllerAdvice
public class CommonExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneric(Exception ex) {
// 通用兜底处理
}
}
注意: 当多个
@ControllerAdvice 都定义了处理同一异常类型的方法时,只有 @Order 值最小的那个方法会被执行。其他方法会被忽略,不会形成“链式调用”。因此,建议将精确的异常处理放在高优先级的 Advice 中,将兜底处理放在低优先级的 Advice 中。
2.5 实战对比:局部 vs 全局异常处理
下表总结了 @ControllerAdvice 全局处理与 Controller 内部局部处理的适用场景:
| 维度 | 全局 @ControllerAdvice | 局部 @ExceptionHandler |
|---|---|---|
| 适用范围 | 所有 Controller(或指定包) | 仅当前 Controller 类 |
| 代码复用 | 高,一处定义全局生效 | 低,每个 Controller 需重复定义 |
| 优先级 | 低(被局部覆盖) | 高(优先于全局) |
| 推荐场景 | 通用异常、统一响应格式 | 特殊业务逻辑、需要特殊响应的场景 |
| 维护成本 | 低,集中管理 | 高,分散在各 Controller |
推荐策略: 以全局 @ControllerAdvice 为主,处理 90% 的通用异常;仅在极少数需要特殊响应格式的 Controller 中使用局部 @ExceptionHandler 覆盖默认行为。