一、核心概念
在 Spring Boot 应用中,当控制器方法执行过程中发生异常时,需要一种集中、统一、优雅的方式来处理这些异常,并向客户端返回结构化的错误信息(如 JSON),而不是默认的错误页面或堆栈信息。
1. 核心注解
注解 | 作用 |
---|---|
@ControllerAdvice |
全局异常处理器。一个带有此注解的类,其内部的 @ExceptionHandler 方法可以处理整个应用中所有控制器抛出的异常。它是 @Component 的特殊形式,会被 Spring 扫描并注册为全局的异常处理切面。 |
@ExceptionHandler |
异常处理方法。标注在方法上,指定该方法用于处理哪些类型的异常(通过方法参数声明)。它可以出现在 @Controller 类中(处理该控制器的异常),但更常见的是与 @ControllerAdvice 结合使用,实现全局处理。 |
2. 处理流程
- 控制器方法执行,抛出异常。
- Spring MVC 的
DispatcherServlet
捕获异常。 - 查找匹配的
@ExceptionHandler
方法:- 首先在抛出异常的控制器类中查找。
- 如果未找到,则在所有被
@ControllerAdvice
标注的类中查找。
- 执行找到的
@ExceptionHandler
方法。 - 该方法的返回值(通常是
ResponseEntity
或直接对象)被序列化并作为 HTTP 响应返回给客户端。
3. 常见异常类型
- 自定义业务异常:
UserNotFoundException
,OrderAlreadyPaidException
等。 - Spring MVC 异常:
MissingServletRequestParameterException
,HttpMessageNotReadableException
,MethodArgumentNotValidException
(参数校验失败)。 - HTTP 状态码异常:
HttpRequestMethodNotSupportedException
,HttpMediaTypeNotSupportedException
。 - 通用异常:
Exception
(捕获所有未被处理的异常)。
二、操作步骤(非常详细)
步骤 1:创建自定义业务异常(可选但推荐)
// 定义一个自定义异常类
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
// 可以添加更多字段,如 errorCode, timestamp 等
}
步骤 2:创建全局异常处理类
2.1 创建 @ControllerAdvice
类
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.Map;
// 标记为全局异常处理器组件
@ControllerAdvice
public class GlobalExceptionHandler {
// 在这里添加 @ExceptionHandler 方法
}
2.2 处理自定义业务异常
@ControllerAdvice
public class GlobalExceptionHandler {
// 处理 UserNotFoundException
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<Map<String, Object>> handleUserNotFound(UserNotFoundException ex) {
Map<String, Object> errorDetails = new LinkedHashMap<>();
errorDetails.put("timestamp", LocalDateTime.now());
errorDetails.put("status", HttpStatus.NOT_FOUND.value());
errorDetails.put("error", "Not Found");
errorDetails.put("message", ex.getMessage());
errorDetails.put("path", "/api/users"); // 可以从请求中获取实际路径
return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND);
}
}
2.3 处理 Spring MVC 参数校验异常
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
@ControllerAdvice
public class GlobalExceptionHandler {
// ... handleUserNotFound 方法
// 处理 @Valid 校验失败抛出的 MethodArgumentNotValidException
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> handleValidationExceptions(
MethodArgumentNotValidException ex) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("timestamp", LocalDateTime.now());
body.put("status", HttpStatus.BAD_REQUEST.value());
body.put("error", "Validation Failed");
// 收集所有字段错误信息
Map<String, String> errors = new LinkedHashMap<>();
for (FieldError error : ex.getBindingResult().getFieldErrors()) {
errors.put(error.getField(), error.getDefaultMessage());
}
body.put("errors", errors);
return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST);
}
}
2.4 处理缺少请求参数异常
import org.springframework.web.bind.MissingServletRequestParameterException;
@ControllerAdvice
public class GlobalExceptionHandler {
// ... 其他方法
// 处理缺少必需的 @RequestParam 参数
@ExceptionHandler(MissingServletRequestParameterException.class)
public ResponseEntity<Map<String, Object>> handleMissingParams(
MissingServletRequestParameterException ex) {
Map<String, Object> errorDetails = new LinkedHashMap<>();
errorDetails.put("timestamp", LocalDateTime.now());
errorDetails.put("status", HttpStatus.BAD_REQUEST.value());
errorDetails.put("error", "Bad Request");
errorDetails.put("message", "Required parameter '" + ex.getParameterName() + "' is missing");
errorDetails.put("parameter", ex.getParameterName());
return new ResponseEntity<>(errorDetails, HttpStatus.BAD_REQUEST);
}
}
2.5 处理请求体反序列化异常
import org.springframework.http.converter.HttpMessageNotReadableException;
@ControllerAdvice
public class GlobalExceptionHandler {
// ... 其他方法
// 处理 JSON 格式错误或无法反序列化
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<Map<String, Object>> handleHttpMessageNotReadable(
HttpMessageNotReadableException ex) {
Map<String, Object> errorDetails = new LinkedHashMap<>();
errorDetails.put("timestamp", LocalDateTime.now());
errorDetails.put("status", HttpStatus.BAD_REQUEST.value());
errorDetails.put("error", "Bad Request");
errorDetails.put("message", "Malformed JSON request or invalid data format");
// 注意:ex.getCause().getMessage() 可能包含敏感信息,生产环境需谨慎处理
errorDetails.put("detail", ex.getMessage());
return new ResponseEntity<>(errorDetails, HttpStatus.BAD_REQUEST);
}
}
2.6 处理所有未被捕获的异常(兜底)
@ControllerAdvice
public class GlobalExceptionHandler {
// ... 其他更具体的异常处理方法
// 处理所有其他未被上述方法捕获的异常
// 通常放在最后,避免覆盖更具体的处理
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleGenericException(Exception ex) {
Map<String, Object> errorDetails = new LinkedHashMap<>();
errorDetails.put("timestamp", LocalDateTime.now());
errorDetails.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
errorDetails.put("error", "Internal Server Error");
errorDetails.put("message", "An unexpected error occurred");
// **生产环境注意**:不要返回 ex.getMessage() 或堆栈,避免信息泄露
// errorDetails.put("detail", ex.getMessage()); // 仅开发环境调试用
return new ResponseEntity<>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
步骤 3:在控制器中抛出异常
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
// 模拟查找用户
if (id <= 0) {
throw new IllegalArgumentException("User ID must be positive");
}
// 模拟用户不存在
if (id == 999) {
throw new UserNotFoundException("User with ID " + id + " not found");
}
return new User(id, "John Doe", 30);
}
@PostMapping
public ResponseEntity<User> createUser(@Valid @RequestBody User user, BindingResult result) {
// 注意:@Valid 会自动触发校验,如果失败会抛出 MethodArgumentNotValidException
// 这里不需要手动检查 result,GlobalExceptionHandler 会处理
// ... 保存用户逻辑
user.setId(System.currentTimeMillis());
return ResponseEntity.status(HttpStatus.CREATED).body(user);
}
}
三、常见错误与解决方案
错误 | 原因 | 解决方案 |
---|---|---|
@ControllerAdvice 类未生效 |
类未被 Spring 扫描到 | 确保类在主应用类(@SpringBootApplication )的包或其子包下,或使用 @ComponentScan 明确指定。确保类上有 @ControllerAdvice 注解。 |
异常未被处理,返回了默认错误页 | @ExceptionHandler 方法未匹配到异常类型 |
检查 @ExceptionHandler 的参数类型是否与抛出的异常类型匹配(或其父类)。确保 @ControllerAdvice 类被正确加载。 |
@ExceptionHandler 方法返回 null 导致 404 |
方法没有返回值或返回 null |
@ExceptionHandler 方法必须返回一个值(如 ResponseEntity 、String 、对象等),该值将作为响应体。确保返回了有效的响应。 |
兜底 Exception 处理器覆盖了特定处理器 |
@ExceptionHandler(Exception.class) 定义在特定异常处理器之前 |
将最通用的 Exception 处理器放在最后。Spring 会优先调用最具体的匹配(子类优先于父类)。 |
ResponseEntity 状态码错误 |
返回的 HttpStatus 与错误类型不符 |
根据错误类型选择合适的 HTTP 状态码(400, 404, 500 等)。 |
生产环境泄露堆栈信息 | 在响应中返回了 ex.getMessage() 或 ex.getStackTrace() |
生产环境绝对不要在响应体中返回详细的异常信息或堆栈。只返回通用的错误消息。 |
四、注意事项
@ControllerAdvice
的扫描:它必须是一个被 Spring 管理的 Bean。通常放在主应用类的包下。- 异常匹配顺序:
- 子类异常优先于父类异常。
- 在同一个
@ControllerAdvice
类中,方法定义的顺序通常不影响匹配(基于类型精确匹配),但为清晰起见,建议将通用的Exception
放在最后。
@ExceptionHandler
的返回值:- 可以是
ResponseEntity<T>
(最灵活,可控制状态码和头信息)。 - 可以是直接的对象(如
Map<String, Object>
),其状态码默认为 200,除非方法上有@ResponseStatus
。 - 可以是
String
(通常用于视图名,但在@RestControllerAdvice
中会作为响应体)。
- 可以是
@ResponseStatus
注解:可以直接在异常类上使用@ResponseStatus
来指定 HTTP 状态码,这样即使没有@ExceptionHandler
,Spring 也会返回该状态码。@ResponseStatus(HttpStatus.NOT_FOUND) public class UserNotFoundException extends RuntimeException { ... }
@RestControllerAdvice
:这是@ControllerAdvice
+@ResponseBody
的组合。使用它时,@ExceptionHandler
方法的返回值会自动序列化为 JSON/XML,无需再加@ResponseBody
。推荐用于 REST API。@RestControllerAdvice // 推荐用于 REST API public class GlobalExceptionHandler { @ExceptionHandler(UserNotFoundException.class) public Map<String, Object> handleUserNotFound(...) { // 返回 Map 会自动转为 JSON return errorDetails; } }
- 安全性:永远不要在生产环境的错误响应中暴露敏感信息(数据库连接错误、文件路径、完整堆栈跟踪)。
五、使用技巧
@RestControllerAdvice
:对于纯 REST API,使用@RestControllerAdvice
可以省去在每个@ExceptionHandler
方法上加@ResponseBody
的麻烦。- 提取公共逻辑:创建一个基类或工具方法来构建标准化的错误响应体
Map
。 - 处理多种异常:一个
@ExceptionHandler
方法可以处理多个异常类型。@ExceptionHandler({UserNotFoundException.class, OrderNotFoundException.class}) public ResponseEntity<?> handleNotFound(RuntimeException ex) { // ... }
- 访问原始请求信息:可以在
@ExceptionHandler
方法参数中注入HttpServletRequest
或WebRequest
来获取请求的详细信息(如 URL、方法、头信息),用于日志记录或响应中。@ExceptionHandler(Exception.class) public ResponseEntity<?> handleGeneric(HttpServletRequest request, Exception ex) { // log.error("Error on request: {} {}", request.getMethod(), request.getRequestURI(), ex); // ... }
- 与日志集成:在
@ExceptionHandler
方法中记录详细的错误日志(包含堆栈),但只向客户端返回精简信息。 - 自定义异常包含元数据:在自定义异常类中添加
errorCode
,timestamp
等字段,在处理器中使用它们。
六、最佳实践
- 使用
@RestControllerAdvice
:对于现代 RESTful 服务,它是标准选择。 - 分层处理:
- 全局处理:使用
@ControllerAdvice
处理通用的、跨领域的异常(参数校验、资源未找到、服务器错误)。 - 局部处理:在特定的
@Controller
中使用@ExceptionHandler
处理该控制器特有的、需要特殊响应逻辑的异常。
- 全局处理:使用
- 标准化错误响应格式:定义一个统一的错误响应结构(如包含
timestamp
,status
,error
,message
,path
,errors
等字段),所有@ExceptionHandler
都遵循此格式。 - 优先处理具体异常:先处理具体的业务异常和 Spring 异常,最后用
Exception.class
作为兜底。 - 强制输入校验:结合
@Valid
和MethodArgumentNotValidException
处理器,确保数据入口的合法性。 - 有意义的错误消息:向客户端返回清晰、用户(或调用者)友好的错误信息,避免技术术语。
- 生产环境安全:严格控制错误信息的暴露,防止信息泄露。
- 全面的日志记录:确保所有异常(尤其是 500 错误)都被详细记录到日志中,便于排查。
七、性能优化
- 异常处理本身开销极小:
@ControllerAdvice
是基于 Spring AOP 代理的,其匹配和调用开销在正常请求处理中可以忽略不计。 - 避免在异常处理中进行耗时操作:
- 不要在
@ExceptionHandler
方法中执行复杂的数据库查询或远程调用。 - 记录日志是主要的“耗时”操作,但这是必要的。确保日志框架配置合理(如异步日志)。
- 不要在
- 减少异常发生:性能优化的关键在于预防异常,而不是优化异常处理。确保代码健壮,做好输入校验和边界检查。
@ResponseStatus
的效率:直接在异常上使用@ResponseStatus
是最轻量级的方式,因为它由 Spring 底层直接处理,无需调用额外的处理器方法。但对于需要返回复杂响应体的场景,@ExceptionHandler
更灵活。
总结
@ControllerAdvice
和 @ExceptionHandler
是 Spring Boot 中实现全局、统一、结构化异常处理的黄金标准。它们将错误处理逻辑从各个控制器中抽离出来,极大地提高了代码的可维护性和 API 的健壮性。
快速掌握路径:
- 创建
@RestControllerAdvice
类。 - 添加几个关键的
@ExceptionHandler
方法:处理MethodArgumentNotValidException
(校验),RuntimeException
(自定义业务异常),Exception
(兜底)。 - 在控制器中故意抛出异常进行测试。
- 使用 Postman/curl 观察返回的 JSON 错误响应。
- 集成日志,确保异常被记录。
- 审查并标准化错误响应格式。
遵循最佳实践,你就能构建出用户友好、易于调试且安全的 Web 服务。