Spring MVC 中如何进行全局异常处理?
在 Spring MVC(以及 Spring Boot)中,进行全局异常处理最推荐、最常用的方式是使用 @ControllerAdvice 或 @RestControllerAdvice 配合 @ExceptionHandler 注解。
这种方式基于 AOP(面向切面编程)思想,能够将异常处理逻辑与业务逻辑解耦,代码极其整洁。
以下是几种常见的实现方式,按推荐程度排序:
1. 推荐方式:@RestControllerAdvice + @ExceptionHandler
这是目前开发 RESTful API 最主流的方式。@RestControllerAdvice 本质上是 @ControllerAdvice + @ResponseBody,意味着返回的数据会自动序列化为 JSON。
步骤:
- 定义统一返回对象(可选但推荐): 为了让前端处理方便,通常会定义一个统一的 Result 类。
- 定义自定义异常(可选): 用于处理业务特定的错误。
- 创建全局异常处理类。
代码示例:
java
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.http.HttpStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// 1. 标记这是一个全局异常处理类,且返回 JSON 格式
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 2. 处理自定义业务异常
* 假设你有一个自定义异常叫 BusinessException
*/
@ExceptionHandler(BusinessException.class)
public Result<String> handleBusinessException(BusinessException e) {
logger.warn("业务异常: {}", e.getMessage());
// 返回自定义的错误码和信息
return Result.error(e.getCode(), e.getMessage());
}
/**
* 3. 处理参数校验异常 (例如使用 @Valid 时产生的 MethodArgumentNotValidException)
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<String> handleValidationException(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
logger.warn("参数校验失败: {}", msg);
return Result.error(400, msg);
}
/**
* 4. 处理所有未知的系统异常 (兜底策略)
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // 设置 HTTP 状态码为 500
public Result<String> handleException(Exception e) {
logger.error("系统内部异常", e);
return Result.error(500, "系统繁忙,请稍后再试");
}
}
优点:
- 解耦: 异常处理逻辑完全独立于 Controller。
- 灵活: 可以针对不同类型的异常编写不同的处理逻辑。
- 全局: 对所有 Controller 生效。
2. 传统方式:实现 HandlerExceptionResolver 接口
这是 Spring MVC 较底层的异常处理方式。你需要实现 resolveException 方法。
代码示例:
java
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class MyExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
// 这里可以根据异常类型做判断
ModelAndView mv = new ModelAndView();
// 如果是前后端不分离项目,可以跳转到错误页面
mv.setViewName("error/500");
mv.addObject("errorMsg", ex.getMessage());
// 如果是 JSON 项目,需要手动通过 response 写出 JSON,比较麻烦
/*
try {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":500, \"msg\":\"error\"}");
} catch (IOException e) { ... }
*/
return mv;
}
}
缺点:
- 需要手动处理 Response 响应流(如果是 JSON 格式),代码比较繁琐。
- 不如注解方式直观。
3. 继承方式:BaseController (不推荐)
创建一个基类 BaseController,在其中写 @ExceptionHandler,然后让所有业务 Controller 继承它。
java
public class BaseController {
@ExceptionHandler(Exception.class)
public Result handleException(Exception e) {
return Result.error(500, e.getMessage());
}
}
// 业务 Controller
@RestController
public class UserController extends BaseController {
// ...
}
缺点:
- 代码侵入性强: Java 是单继承,如果 Controller 需要继承其他类就麻烦了。
- 耦合度高: 强迫所有 Controller 继承基类。
总结与最佳实践
- 首选方案: 使用
@RestControllerAdvice(如果是传统页面开发则用@ControllerAdvice) 配合@ExceptionHandler。 - 异常分类:
- 自定义异常(如
UserNotFoundException):返回明确的业务错误码。 - 参数校验异常(
MethodArgumentNotValidException):提取具体的校验错误信息返回给前端。 - 兜底异常(
Exception.class):捕获所有未处理异常,防止堆栈信息直接暴露给前端,通常返回 "系统繁忙"。
- 自定义异常(如
- 日志记录: 在全局异常处理器中,务必打印日志(
logger.error),这对排查生产环境问题至关重要。
右滑查看面试常问