Spring Boot 全局异常处理:从入门到原理源码解析
-
- 前言
- 一、快速上手:5分钟实现全局异常处理
-
- [1.1 核心注解](#1.1 核心注解)
- [1.2 统一返回结果封装](#1.2 统一返回结果封装)
- [1.3 自定义业务异常](#1.3 自定义业务异常)
- [1.4 全局异常处理器](#1.4 全局异常处理器)
- [1.5 在业务中使用](#1.5 在业务中使用)
- 二、进阶实践
-
- [2.1 异常匹配优先级](#2.1 异常匹配优先级)
- [2.2 限定处理范围](#2.2 限定处理范围)
- [2.3 一个方法处理多个异常](#2.3 一个方法处理多个异常)
- [2.4 参数校验异常处理](#2.4 参数校验异常处理)
- 三、源码原理深度解析
-
- [3.1 整体执行流程](#3.1 整体执行流程)
- [3.2 核心角色:ExceptionHandlerExceptionResolver](#3.2 核心角色:ExceptionHandlerExceptionResolver)
- [3.3 初始化阶段:扫描并缓存异常处理方法](#3.3 初始化阶段:扫描并缓存异常处理方法)
- [3.4 @ControllerAdvice的作用范围机制](#3.4 @ControllerAdvice的作用范围机制)
- [3.5 @ControllerAdvice的本质](#3.5 @ControllerAdvice的本质)
- [3.6 运行时:异常如何被匹配和处理](#3.6 运行时:异常如何被匹配和处理)
- [3.7 关于ResponseEntityExceptionHandler](#3.7 关于ResponseEntityExceptionHandler)
- 四、总结
-
- [4.1 核心要点回顾](#4.1 核心要点回顾)
- [4.2 最佳实践建议](#4.2 最佳实践建议)
- [4.3 原理一句话总结](#4.3 原理一句话总结)
前言
在Spring Boot项目开发中,异常处理是绕不开的话题。无论是参数校验失败、空指针异常,还是数据库操作报错,如果不对异常做统一处理,任由错误堆栈直接抛给前端,不仅会暴露底层实现细节带来安全风险,还会返回杂乱无章的错误信息,严重影响用户体验。
传统的做法是在每个Controller方法里用try-catch捕获异常,这种方式代码冗余、维护成本高,而且极易遗漏。Spring Boot提供的全局异常处理机制,能够集中拦截项目中所有层级的异常,统一封装标准化返回结果,优雅地区分业务异常与系统未知异常。
本文将从代码实践到源码原理,全面讲解Spring Boot全局异常处理的实现方式。
一、快速上手:5分钟实现全局异常处理
1.1 核心注解
全局异常处理主要依赖两个核心注解:
@ControllerAdvice:作用于类,表示该类是一个全局的控制器增强器,会对所有@Controller或@RestController中抛出的异常进行拦截。@ExceptionHandler:作用于方法,用于指定该方法处理哪些类型的异常。
对于前后端分离的RESTful项目,推荐使用@RestControllerAdvice,它是@ControllerAdvice和@ResponseBody的组合注解,能让返回值自动转为JSON格式。
1.2 统一返回结果封装
首先定义一个统一的API返回结果类:
java
package com.example.demo.common;
import lombok.Data;
@Data
public class Result<T> {
private int code;
private String message;
private T data;
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage("success");
result.setData(data);
return result;
}
public static <T> Result<T> error(int code, String message) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMessage(message);
return result;
}
public static <T> Result<T> error(String message) {
return error(500, message);
}
}
1.3 自定义业务异常
定义业务异常类,继承RuntimeException,这样在业务代码中抛出时不需要显式处理:
java
package com.example.demo.exception;
public class BusinessException extends RuntimeException {
private int code;
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
public int getCode() {
return code;
}
}
1.4 全局异常处理器
创建全局异常处理类,这是整个机制的核心:
java
package com.example.demo.handler;
import com.example.demo.common.Result;
import com.example.demo.exception.BusinessException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.bind.MissingServletRequestParameterException;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理空指针异常
*/
@ExceptionHandler(NullPointerException.class)
public Result<Void> handleNullPointerException(NullPointerException e) {
log.error("空指针异常:", e);
return Result.error(500, "系统发生空指针异常");
}
/**
* 处理自定义业务异常
*/
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e) {
log.error("业务异常:code={}, message={}", e.getCode(), e.getMessage());
return Result.error(e.getCode(), e.getMessage());
}
/**
* 处理参数缺失异常
*/
@ExceptionHandler(MissingServletRequestParameterException.class)
public Result<Void> handleMissingParams(MissingServletRequestParameterException e) {
log.error("缺少请求参数:{}", e.getMessage());
return Result.error(400, "缺少必传参数:" + e.getParameterName());
}
/**
* 兜底处理所有未捕获的异常
*/
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
log.error("系统异常:", e);
// 生产环境建议不暴露具体异常信息
return Result.error(500, "服务器内部错误");
}
}
1.5 在业务中使用
java
@RestController
@RequestMapping("/api/user")
public class UserController {
@GetMapping("/{id}")
public Result<User> getUser(@PathVariable Long id) {
if (id == null || id <= 0) {
throw new BusinessException(400, "用户ID不合法");
}
// 业务逻辑...
return Result.success(user);
}
}
当业务代码抛出BusinessException时,全局异常处理器会自动捕获并返回统一的JSON格式响应,前端可以按照统一的格式解析。
二、进阶实践
2.1 异常匹配优先级
当一个异常能被多个@ExceptionHandler匹配时,精确匹配优先于父类匹配。
例如抛出NullPointerException时,会优先匹配@ExceptionHandler(NullPointerException.class),而不是@ExceptionHandler(Exception.class)。
最佳实践 :将具体的异常处理写在前面,通用的Exception兜底处理写在最后。
2.2 限定处理范围
@RestControllerAdvice默认拦截整个应用的所有Controller。如果需要限定范围,可以使用basePackages属性指定包路径:
java
@RestControllerAdvice(basePackages = "com.example.demo.controller")
public class GlobalExceptionHandler {
// 只处理指定包下的Controller抛出的异常
}
2.3 一个方法处理多个异常
@ExceptionHandler支持同时指定多个异常类型:
java
@ExceptionHandler({IllegalArgumentException.class, NullPointerException.class})
public Result<Void> handleBadRequest(Exception e) {
return Result.error(400, "请求参数错误:" + e.getMessage());
}
2.4 参数校验异常处理
在使用@Valid进行参数校验时,会抛出MethodArgumentNotValidException,需要单独处理:
java
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValidationExceptions(MethodArgumentNotValidException e) {
String errorMsg = e.getBindingResult().getFieldErrors().stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.joining(", "));
log.error("参数校验失败:{}", errorMsg);
return Result.error(400, errorMsg);
}
三、源码原理深度解析
了解了如何使用之后,我们深入源码,看看这一切背后的运行机制。
3.1 整体执行流程
全局异常处理的执行流程如下:
Controller抛出异常 → DispatcherServlet捕获 →
查找匹配的@ExceptionHandler方法 → 执行并返回结果
3.2 核心角色:ExceptionHandlerExceptionResolver
在Spring MVC中,真正执行@ExceptionHandler和@ControllerAdvice的是ExceptionHandlerExceptionResolver。
从类注释就能看出它的定位:
java
/**
* An {@link HandlerExceptionResolver} that resolves exceptions through
* {@link ExceptionHandler} methods.
*/
它只做一件事:找到能处理当前异常的@ExceptionHandler方法,并执行它。
Spring MVC的异常处理遵循一条责任链:
ExceptionHandlerExceptionResolver →
ResponseStatusExceptionResolver →
DefaultHandlerExceptionResolver
ExceptionHandlerExceptionResolver是第一个被调用的,专门处理通过注解定义的异常处理方法。
3.3 初始化阶段:扫描并缓存异常处理方法
在Spring容器启动阶段,ExceptionHandlerExceptionResolver会执行初始化逻辑:
java
public void afterPropertiesSet() {
initExceptionHandlerAdviceCache();
}
initExceptionHandlerAdviceCache()方法的核心工作是:
java
private void initExceptionHandlerAdviceCache() {
List<ControllerAdviceBean> adviceBeans =
ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
// 遍历所有@ControllerAdvice Bean,扫描其中的@ExceptionHandler方法并缓存
}
关键步骤包括:
- 扫描整个Spring容器 ,找出所有标注了
@ControllerAdvice的Bean - 对每一个
@ControllerAdvice类,使用ExceptionHandlerMethodResolver扫描所有方法 - 找出标注了
@ExceptionHandler的方法 - 建立异常类型 → 处理方法的映射关系并缓存
每一个Controller或Advice,都会对应一个ExceptionHandlerMethodResolver实例。
3.4 @ControllerAdvice的作用范围机制
@ControllerAdvice支持通过basePackages、annotations等属性限定作用范围:
java
@ControllerAdvice(
basePackages = "com.example.web",
annotations = RestController.class
)
源码中通过HandlerTypePredicate判断当前@ControllerAdvice是否适用于某个Controller。这也是为什么:有些Advice明明存在,却对某些Controller不生效。
3.5 @ControllerAdvice的本质
点开@ControllerAdvice的源码会发现,它是一个组合注解,包含了@Component:
java
@Component
public @interface ControllerAdvice {
// ...
}
这意味着标注了@ControllerAdvice的类会被Spring容器扫描并注册为Bean。
3.6 运行时:异常如何被匹配和处理
当Controller抛出异常时,执行流程如下:
DispatcherServlet的doDispatch()方法捕获异常- 进入
processDispatchResult()方法处理异常 - 遍历
HandlerExceptionResolver责任链,首先调用ExceptionHandlerExceptionResolver ExceptionHandlerExceptionResolver根据异常类型,从缓存的映射中查找匹配的@ExceptionHandler方法- 找到后通过反射执行该方法,将返回值写入HTTP响应
3.7 关于ResponseEntityExceptionHandler
Spring框架还提供了一个基类ResponseEntityExceptionHandler,它几乎枚举了所有常见的Spring MVC异常。如果你需要细分处理各种Spring内置异常,可以继承这个类:
java
@ControllerAdvice
public class CustomResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
// 重写具体异常的handle方法
}
四、总结
4.1 核心要点回顾
| 组件 | 作用 |
|---|---|
@RestControllerAdvice |
标记全局异常处理类,自动注册为Bean,拦截所有Controller异常 |
@ExceptionHandler |
标记具体异常处理方法,指定处理的异常类型 |
ExceptionHandlerExceptionResolver |
源码层面的执行者,扫描、缓存、匹配并执行异常处理方法 |
4.2 最佳实践建议
- 使用
@RestControllerAdvice:前后端分离项目优先使用,自动返回JSON - 定义统一返回格式 :封装
code、message、data三个字段 - 自定义业务异常 :继承
RuntimeException,携带错误码和错误信息 - 精确优先、兜底在后 :具体异常写在前面,
Exception.class兜底 - 记录日志:在异常处理方法中打印日志,便于线上问题排查
- 生产环境隐藏细节:兜底异常不要返回堆栈信息,避免安全风险
4.3 原理一句话总结
全局异常处理本质上是Spring MVC在启动时扫描所有@ControllerAdvice类中的@ExceptionHandler方法,建立异常类型到处理方法的映射缓存;运行时由ExceptionHandlerExceptionResolver根据异常类型匹配并执行对应方法,实现统一的异常拦截和响应。