Sprnig MVC 如何统一异常处理 (Exception Handling)?
主要有以下几种方式来实现统一异常处理,其中 @ControllerAdvice
(或 @RestControllerAdvice
) 结合 @ExceptionHandler
是最常用的方式。
1. @ExceptionHandler
注解
-
作用: 用于标记一个方法,该方法将处理在同一个 Controller 类中抛出的特定类型的异常。
-
怎么用:
- 在一个 Controller 类中,创建一个方法。
- 给这个方法添加
@ExceptionHandler
注解。 - 在注解中指定该方法要处理的异常类型 (一个或多个)。
- 方法的参数可以是:
- 要处理的异常对象本身 (例如
NullPointerException ex
)。 HttpServletRequest
,HttpServletResponse
,HttpSession
。Model
(如果你想返回一个视图并向模型中添加数据)。
- 要处理的异常对象本身 (例如
- 方法的返回值可以是:
ModelAndView
: 用于返回一个特定的错误视图,并可以携带错误信息。String
: 作为视图名。@ResponseBody
+ 任何可被HttpMessageConverter
转换的对象 (例如ResponseEntity<ErrorResponse>
,Map<String, Object>
, 自定义错误对象): 用于返回JSON/XML等格式的错误响应体。ResponseEntity<?>
: 可以更灵活的控制响应状态码、头信息和响应体。
-
示例 (在单个 Controller 内):
import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.ModelAndView;@Controller @RequestMapping("/items") public class ItemController {@GetMapping("/{id}")public String getItem(@PathVariable String id) {if ("error".equals(id)) {throw new ItemNotFoundException("Item with id " + id + " not found!");}if ("npe".equals(id)) {String str = null;str.length(); // 会抛出 NullPointerException}return "itemDetails"; // 假设有一个 itemDetails.jsp 或 .html}// 处理当前Controller中抛出的ItemNotFoundException@ExceptionHandler(ItemNotFoundException.class)public ModelAndView handleItemNotFoundException(ItemNotFoundException ex) {ModelAndView mav = new ModelAndView("error/itemNotFound"); // 指向错误视图mav.addObject("errorMessage", ex.getMessage());mav.addObject("exceptionType", ex.getClass().getSimpleName());return mav;}// 处理当前Controller中抛出的NullPointerException,并返回JSON响应@ExceptionHandler(NullPointerException.class)@ResponseBody // 或者直接使用 @RestControllerAdvice 来替代public ResponseEntity<ErrorResponse> handleNullPointerException(NullPointerException ex, HttpServletRequest request) {ErrorResponse errorResponse = new ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(),"A null pointer exception occurred.",ex.getMessage(),request.getRequestURI());return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);}// 处理其他未被特定处理的Exception (作为兜底)@ExceptionHandler(Exception.class)public ResponseEntity<String> handleGenericException(Exception ex) {return new ResponseEntity<>("An unexpected error occurred: " + ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);} }// 自定义异常 class ItemNotFoundException extends RuntimeException {public ItemNotFoundException(String message) {super(message);} }// 自定义错误响应体 class ErrorResponse {private int status;private String message;private String details;private String path;// Constructors, Getters, Setterspublic ErrorResponse(int status, String message, String details, String path) {this.status = status;this.message = message;this.details = details;this.path = path;}// ... (getters and setters) }
-
缺点:
@ExceptionHandler
注解的方法只能处理其所在 Controller 类内部抛出的异常。如果想在多个 Controller 之间共享异常处理逻辑,这种方式就不够高效。
2. @ControllerAdvice
或 @RestControllerAdvice
注解 (推荐的全局异常处理方式)
-
作用:
@ControllerAdvice
: 标记一个类,使其成为一个全局的 Controller 建言 (advice) 组件。这个类中的方法可以应用到应用程序中所有 (或指定的) Controller。@RestControllerAdvice
: 是@ControllerAdvice
和@ResponseBody
的组合注解。它特别适用于构建 RESTful API,因为该类中所有@ExceptionHandler
方法的返回值都会被自动转换为 HTTP 响应体 (通常是 JSON 或 XML)。
-
怎么用:
- 创建一个普通的 Java 类。
- 给这个类添加
@ControllerAdvice
或@RestControllerAdvice
注解。 - 在这个类中,定义一个或多个使用
@ExceptionHandler
注解的方法,就像在单个 Controller 中那样。 @ExceptionHandler
方法的参数和返回值规则与在单个 Controller 中使用时相同。
-
示例 (全局异常处理):
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.context.request.WebRequest; // 更通用的请求对象import javax.servlet.http.HttpServletRequest; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors;@RestControllerAdvice // 因为我们想返回JSON/XML响应体 // @ControllerAdvice // 如果你想返回ModelAndView或String作为视图名 public class GlobalExceptionHandler {private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);// 处理自定义的 ItemNotFoundException@ExceptionHandler(ItemNotFoundException.class)@ResponseStatus(HttpStatus.NOT_FOUND) // 可以直接在这里设置响应状态码public ErrorResponse handleItemNotFoundException(ItemNotFoundException ex, WebRequest request) {logger.error("ItemNotFoundException occurred: {}", ex.getMessage(), ex);return new ErrorResponse(HttpStatus.NOT_FOUND.value(),"Resource Not Found",ex.getMessage(),request.getDescription(false) // 获取请求路径,不包含查询参数);}// 处理参数校验异常 (例如 @Valid 注解失败)@ExceptionHandler(MethodArgumentNotValidException.class)@ResponseStatus(HttpStatus.BAD_REQUEST)public Map<String, Object> handleValidationExceptions(MethodArgumentNotValidException ex, WebRequest request) {logger.warn("Validation error: {}", ex.getMessage());Map<String, Object> response = new HashMap<>();response.put("timestamp", new Date());response.put("status", HttpStatus.BAD_REQUEST.value());response.put("error", "Validation Failed");response.put("path", request.getDescription(false));// 获取所有字段的校验错误信息Map<String, String> errors = ex.getBindingResult().getFieldErrors().stream().collect(Collectors.toMap(fieldError -> fieldError.getField(),fieldError -> fieldError.getDefaultMessage() != null ? fieldError.getDefaultMessage() : "Invalid value"));response.put("errors", errors);return response;}// 处理其他所有未被特定处理的Exception (作为全局兜底)@ExceptionHandler(Exception.class)@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)public ErrorResponse handleAllUncaughtException(Exception ex, WebRequest request) {logger.error("An unexpected error occurred: {}", ex.getMessage(), ex);return new ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(),"Internal Server Error","An unexpected error occurred. Please try again later.",request.getDescription(false));} }// 假设 ItemNotFoundException 和 ErrorResponse 类定义同上
-
@ControllerAdvice 的属性 (用于限定范围):
value
或basePackages
: 指定该ControllerAdvice
应用的包。例如@ControllerAdvice("com.example.api.controllers")
。annotations
: 指定该ControllerAdvice
应用于带有特定注解的 Controller。例如@ControllerAdvice(annotations = RestController.class)
。assignableTypes
: 指定该ControllerAdvice
应用于特定类型的 Controller。例如@ControllerAdvice(assignableTypes = {MyBaseController.class})
。
3. 实现 HandlerExceptionResolver
接口 (更底层,不常用)
- 这是一个更底层的接口,允许你创建自定义的异常解析策略。
- 你需要实现
resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
方法。 - Spring MVC 默认提供了几个实现,如
ExceptionHandlerExceptionResolver
(处理@ExceptionHandler
注解)、ResponseStatusExceptionResolver
(处理带有@ResponseStatus
注解的异常)、DefaultHandlerExceptionResolver
(处理Spring MVC内部的一些标准异常)。 - 通常情况下,使用
@ControllerAdvice
和@ExceptionHandler
已经足够了,不需要直接实现此接口。
4. 使用 @ResponseStatus
注解标记自定义异常类
-
你可以直接在自定义异常类上使用
@ResponseStatus
注解来指定当该异常被抛出且未被@ExceptionHandler
捕获时,应该返回的 HTTP 状态码。import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus;@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "The requested resource was not found.") class ResourceNotFoundException extends RuntimeException {public ResourceNotFoundException(String message) {super(message);} }
当
ResourceNotFoundException
抛出且没有被更具体的@ExceptionHandler
处理时,Spring MVC (通过ResponseStatusExceptionResolver
) 会自动返回 404 状态码,并将reason
作为响应体(如果reason
存在)。
统一异常处理的实现步骤总结 (使用 @RestControllerAdvice
和 @ExceptionHandler
):
-
创建自定义异常类 (可选但推荐):
- 根据业务需求创建具体的异常类,继承自
RuntimeException
或Exception
。 - 例如:
UserNotFoundException
,InvalidInputException
,OrderProcessingException
。
- 根据业务需求创建具体的异常类,继承自
-
创建全局异常处理类:
- 创建一个类,并使用
@RestControllerAdvice
(用于REST API) 或@ControllerAdvice
(用于传统MVC) 注解。
- 创建一个类,并使用
-
在全局异常处理类中定义
@ExceptionHandler
方法:- 为每种你想要特殊处理的异常类型(包括自定义异常和Spring内置异常如
MethodArgumentNotValidException
)创建一个方法。 - 在方法上使用
@ExceptionHandler(YourExceptionClass.class)
注解。 - 在方法体中,构造并返回一个统一的错误响应对象 (例如一个包含状态码、错误消息、详细信息、时间戳等的POJO)。
- 可以使用
@ResponseStatus
注解在方法级别直接指定HTTP状态码,或者在ResponseEntity
中设置。
- 为每种你想要特殊处理的异常类型(包括自定义异常和Spring内置异常如
-
定义统一的错误响应结构 (POJO):
- 创建一个类来表示标准的错误响应格式,例如前面示例中的
ErrorResponse
。这样可以确保所有错误响应都有一致的结构。
- 创建一个类来表示标准的错误响应格式,例如前面示例中的
-
(可选) 配置日志记录:
- 在
@ExceptionHandler
方法中,使用日志框架 (如 SLF4J + Logback/Log4j2) 记录异常的详细信息,包括堆栈跟踪,调试非常重要。
- 在
通过上述方式,我们可以将异常处理逻辑从业务代码中分离出来,使Controller 代码更简洁,并且能够为客户端提供友好的错误反馈。