Spring Boot 异常处理到底怎么玩的?从 DispatcherServlet 到全局兜底的全链路拆解
你天天写
@RestControllerAdvice,但你知道 Spring Boot 是怎么把异常一路从 Controller 扛到你的全局处理器手里的吗?中间经过了哪些层?为什么有的异常能被捕获、有的却直接 500?这篇文章从 DispatcherServlet 的异常处理链出发,一路拆到@ExceptionHandler、@ControllerAdvice、BasicErrorController,最后给出一套生产级异常处理方案。
一、从一个问题开始
你写了全局异常处理器:
java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public Result<String> handle(Exception e) {
return Result.fail(e.getMessage());
}
}
然后写了个接口故意抛异常:
java
@GetMapping("/test")
public String test() {
int i = 1 / 0; // ArithmeticException
}
请求后,你确实拿到了统一的 JSON 响应。但是------
- 谁捕获了这个异常? DispatcherServlet?还是 Tomcat?
- 异常是怎么传递到
@ExceptionHandler的? - 为什么 Filter 里抛的异常
@ExceptionHandler捕获不到? - 如果
@ExceptionHandler没匹配到,会发生什么? - 生产环境的 Whitelabel Error Page 是谁渲染的?
这五个问题,对应异常处理的五个层次。逐层拆解。
二、Spring MVC 异常处理架构总览
css
┌───────────────────────────────────────────────────────┐
│ HTTP 请求 │
└───────────────────────┬───────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────┐
│ Filter Chain(Servlet 容器层) │
│ 异常在这里不会被 @ExceptionHandler 捕获! │
└───────────────────────┬───────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────┐
│ DispatcherServlet.doDispatch() │
│ ┌─────────────────────────────────────────────┐ │
│ │ HandlerMapping → HandlerAdapter → Controller │ │
│ │ ↓ 异常 ↓ │ │
│ │ processDispatchResult() │ │
│ │ → processHandlerException() │ │
│ │ → HandlerExceptionResolver 链 │ │
│ └─────────────────────────────────────────────┘ │
└───────────────────────┬───────────────────────────────┘
│ 无 Resolver 能处理
▼
┌───────────────────────────────────────────────────────┐
│ Servlet 容器默认错误处理 │
│ → 转发到 /error │
│ → BasicErrorController │
│ → Whitelabel Error Page │
└───────────────────────────────────────────────────────┘
核心思路:Spring MVC 的异常处理在 DispatcherServlet 内部完成,Filter 层的异常它管不到。
三、第一层:DispatcherServlet 的异常捕获
3.1 doDispatch() 的 try-catch
DispatcherServlet 的核心方法 doDispatch() 大致结构:
java
void doDispatch(HttpServletRequest request, HttpServletResponse response) {
try {
ModelAndView mv = null;
try {
// 1. 找 Handler
mappedHandler = getHandler(processedRequest);
// 2. 找 Adapter
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 3. 执行 Handler(Controller 方法)
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
} catch (Exception ex) {
// 4. 异常在这里被捕获!
mv = processHandlerException(processedRequest, response,
mappedHandler, ex);
}
// 5. 渲染视图
processDispatchResult(processedRequest, response,
mappedHandler, mv, dispatchException);
} catch (Exception ex) {
// 6. 如果 processHandlerException 也抛异常,到这里
// 最终交给 Servlet 容器处理
}
}
关键:Controller 抛的异常在步骤 3 被捕获,交给 processHandlerException() 处理。
3.2 processHandlerException() 做了什么?
java
ModelAndView processHandlerException(HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex) {
// 遍历所有 HandlerExceptionResolver
for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
ModelAndView mv = resolver.resolveException(request, response, handler, ex);
if (mv != null) {
return mv; // 有 Resolver 能处理,返回
}
}
return null; // 无 Resolver 能处理,返回 null
}
如果所有 Resolver 都返回 null,异常会继续往外抛,最终由 Servlet 容器处理。
四、第二层:HandlerExceptionResolver 链
Spring Boot 默认注册三个 Resolver,按顺序执行:
| 顺序 | Resolver | 作用 |
|---|---|---|
| 1 | ExceptionHandlerExceptionResolver |
处理 @ExceptionHandler |
| 2 | ResponseStatusExceptionResolver |
处理 @ResponseStatus |
| 3 | DefaultHandlerExceptionResolver |
处理 Spring MVC 标准异常 |
4.1 ExceptionHandlerExceptionResolver
这是最常用的 Resolver。它负责找 @ExceptionHandler 方法:
java
// 核心逻辑(简化)
ModelAndView doResolveHandlerMethodException(request, response, handler, ex) {
// 1. 找到 @ControllerAdvice 标注的类
// 2. 在这些类中找 @ExceptionHandler 标注的方法
// 3. 匹配异常类型(精确匹配优先,然后按继承关系找)
// 4. 调用匹配的方法
// 5. 将返回值包装成 ModelAndView
}
匹配规则:
- 精确匹配优先:
@ExceptionHandler(ArithmeticException.class)优先于@ExceptionHandler(Exception.class) - 子类优先:如果异常同时匹配多个
@ExceptionHandler,最具体的那个胜出 - 如果没有精确匹配,按声明顺序尝试
4.2 ResponseStatusExceptionResolver
处理 @ResponseStatus 注解:
java
@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "资源不存在")
public class ResourceNotFoundException extends RuntimeException {}
或者直接在方法上标注:
java
@ResponseStatus(code = HttpStatus.BAD_REQUEST)
@GetMapping("/test")
public String test() {
throw new IllegalArgumentException("参数错误");
}
这个 Resolver 的处理逻辑很简单:读取 @ResponseStatus 的 code 和 reason,设置 response 状态码。
4.3 DefaultHandlerExceptionResolver
处理 Spring MVC 自定义的标准异常:
| 异常 | HTTP 状态码 |
|---|---|
HttpRequestMethodNotSupportedException |
405 |
HttpMediaTypeNotSupportedException |
415 |
MissingServletRequestParameterException |
400 |
TypeMismatchException |
400 |
MethodArgumentNotValidException |
400 |
这些异常通常由 Spring MVC 框架自身抛出,比如请求方法不匹配、参数类型转换失败等。
五、第三层:@ControllerAdvice 的底层机制
5.1 @ControllerAdvice 是怎么被发现的?
Spring Boot 启动时,RequestMappingHandlerAdapter 的初始化过程:
java
// 简化流程
void afterPropertiesSet() {
// 1. 扫描所有 @ControllerAdvice 标注的 Bean
this.controllerAdviceCache =
ControllerAdviceCache.find(this.applicationContext);
// 2. 解析每个 @ControllerAdvice 中的 @ExceptionHandler
for (Class<?> controllerAdvice : controllerAdvices) {
Map<Class<? extends Throwable>, Method> mappings =
ExceptionHandlerMethodResolver.resolve(controllerAdvice);
this.exceptionHandlerCache.put(controllerAdvice, mappings);
}
}
5.2 @ControllerAdvice 的作用范围
java
// 限制包范围
@ControllerAdvice("com.example.controller")
// 限制注解
@ControllerAdvice(annotations = RestController.class)
// 限制特定类
@ControllerAdvice(assignableTypes = OrderController.class)
| 属性 | 作用 | 默认值 |
|---|---|---|
basePackages |
限制包范围 | 所有包 |
annotations |
限制 Controller 注解 | 所有 |
assignableTypes |
限制 Controller 类型 | 所有 |
注意 :如果不限制范围,@ControllerAdvice 对所有 Controller 生效,可能产生意料外的匹配。
5.3 @ExceptionHandler 的返回值处理
@ExceptionHandler 方法的返回值会被 HandlerMethodReturnValueHandler 处理:
| 返回类型 | 处理器 | 效果 |
|---|---|---|
ResponseEntity<T> |
ResponseEntityReturnValueHandler |
直接设置状态码和 Body |
Result<T>(自定义) |
RequestResponseBodyMethodProcessor |
通过 @ResponseBody 序列化为 JSON |
ModelAndView |
ModelAndViewMethodReturnValueHandler |
渲染视图 |
String(视图名) |
ViewNameMethodReturnValueHandler |
渲染视图 |
所以 @RestControllerAdvice + 返回 Result<T> 的写法,本质是 @ResponseBody 把对象序列化为 JSON。
六、第四层:Filter 异常为什么捕获不到?
这是最常见的困惑:为什么 @ExceptionHandler 捕获不到 Filter 中的异常?
原因很简单:DispatcherServlet 的 try-catch 只包裹 Handler 执行过程,Filter 在 Servlet 容器层执行,不在 DispatcherServlet 的管辖范围内。
arduino
请求 → Filter1 → Filter2 → ... → DispatcherServlet → Controller
↑ try-catch 在这里
异常在 Filter 中抛出 → 不经过 DispatcherServlet → 直接交给 Servlet 容器
6.1 解法一:Filter 中 try-catch + 转发到 /error
java
@Component
public class AuthFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse resp,
FilterChain chain) {
try {
// 业务逻辑
chain.doFilter(req, resp);
} catch (Exception e) {
// 转发到 /error,让 Spring Boot 的 ErrorController 处理
req.setAttribute("javax.servlet.error.exception", e);
req.getRequestDispatcher("/error").forward(req, resp);
}
}
}
6.2 解法二:Spring Boot 的 ErrorController
Spring Boot 默认注册了 BasicErrorController,处理 /error 路径:
java
@Controller
@RequestMapping("${server.error.path:/error}")
public class BasicErrorController implements ErrorController {
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request,
isIncludeStackTrace(request));
HttpStatus status = getStatus(request);
return new ResponseEntity<>(body, status);
}
}
自定义 ErrorController:
java
@RestController
public class CustomErrorController implements ErrorController {
@RequestMapping("/error")
public Result<Map<String, Object>> error(HttpServletRequest request) {
Integer status = (Integer) request.getAttribute(
"javax.servlet.error.status_code");
Throwable ex = (Throwable) request.getAttribute(
"javax.servlet.error.exception");
Map<String, Object> detail = new HashMap<>();
detail.put("status", status);
detail.put("message", ex != null ? ex.getMessage() : "未知错误");
return Result.fail(detail);
}
}
6.3 对比
| 方案 | 优点 | 缺点 |
|---|---|---|
@ExceptionHandler |
在 Controller 层,可以统一 JSON 格式 | 只能捕获 Controller 异常 |
| Filter try-catch + 转发 | 能处理 Filter 异常 | 需要每个 Filter 手动处理 |
| 自定义 ErrorController | 全局兜底,Filter 和 Controller 异常都能处理 | 无法区分异常类型做精细化处理 |
生产推荐 :@ExceptionHandler + 自定义 ErrorController 双层兜底。
七、第五层:Spring Boot 的错误处理自动配置
7.1 ErrorMvcAutoConfiguration
Spring Boot 通过 ErrorMvcAutoConfiguration 自动配置错误处理:
java
@AutoConfiguration
@ConditionalOnClass({Servlet.class, DispatcherServlet.class})
public class ErrorMvcAutoConfiguration {
// 1. 注册 BasicErrorController
@Bean
@ConditionalOnMissingBean(ErrorController.class)
public BasicErrorController errorController() {
return new BasicErrorController(
this.serverProperties.getError(), this.errorViewResolvers);
}
// 2. 注册 Whitelabel Error Page 视图
@Bean
@ConditionalOnMissingBean(ErrorViewResolver.class)
public WhitelabelErrorView whitelabelErrorView() {
return new WhitelabelErrorView();
}
}
7.2 错误页面配置
yaml
server:
error:
path: /error # 错误处理路径
include-exception: true # 响应中包含异常类名
include-stacktrace: always # 包含堆栈(仅开发环境!)
include-message: always # 包含异常消息
whitelabel:
enabled: false # 关闭 Whitelabel 页面
| 配置 | 作用 | 生产推荐 |
|---|---|---|
include-exception |
返回异常类名 | false |
include-stacktrace |
返回堆栈信息 | never |
include-message |
返回异常消息 | on_param |
whitelabel.enabled |
启用白标错误页 | false |
生产环境一定要关闭堆栈信息和 Whitelabel 页面,否则会泄露内部信息。
八、生产级异常处理方案
8.1 自定义异常体系
java
// 基础异常
public class BaseException extends RuntimeException {
private final ErrorCode errorCode;
public BaseException(ErrorCode errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public BaseException(ErrorCode errorCode, String message, Throwable cause) {
super(message, cause);
this.errorCode = errorCode;
}
}
// 业务异常
public class BusinessException extends BaseException {
public BusinessException(ErrorCode errorCode, String message) {
super(errorCode, message);
}
}
// 参数异常
public class ParamException extends BaseException {
public ParamException(String message) {
super(ErrorCode.PARAM_ERROR, message);
}
}
8.2 错误码体系
java
public enum ErrorCode {
// 通用
SUCCESS(0, "成功"),
SYSTEM_ERROR(1, "系统异常"),
PARAM_ERROR(2, "参数错误"),
// 业务
USER_NOT_FOUND(1001, "用户不存在"),
ORDER_EXPIRED(2001, "订单已过期"),
INSUFFICIENT_BALANCE(2002, "余额不足"),
;
private final int code;
private final String message;
ErrorCode(int code, String message) {
this.code = code;
this.message = message;
}
}
8.3 全局异常处理器
java
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// 1. 业务异常
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusiness(BusinessException e) {
log.warn("业务异常: code={}, msg={}", e.getErrorCode().getCode(), e.getMessage());
return Result.fail(e.getErrorCode().getCode(), e.getMessage());
}
// 2. 参数校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValidation(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(fe -> fe.getField() + ": " + fe.getDefaultMessage())
.collect(Collectors.joining("; "));
log.warn("参数校验失败: {}", message);
return Result.fail(ErrorCode.PARAM_ERROR.getCode(), message);
}
// 3. 参数绑定异常
@ExceptionHandler(BindException.class)
public Result<Void> handleBind(BindException e) {
String message = e.getFieldErrors().stream()
.map(fe -> fe.getField() + ": " + fe.getDefaultMessage())
.collect(Collectors.joining("; "));
return Result.fail(ErrorCode.PARAM_ERROR.getCode(), message);
}
// 4. Spring Security 认证异常
@ExceptionHandler(AuthenticationException.class)
public Result<Void> handleAuth(AuthenticationException e) {
return Result.fail(401, "认证失败");
}
// 5. 权限异常
@ExceptionHandler(AccessDeniedException.class)
public Result<Void> handleAccessDenied(AccessDeniedException e) {
return Result.fail(403, "无权限");
}
// 6. 兜底:所有其他异常
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
log.error("未处理异常", e);
return Result.fail(ErrorCode.SYSTEM_ERROR.getCode(), "系统繁忙,请稍后重试");
}
}
8.4 自定义 ErrorController 兜底
java
@RestController
public class GlobalErrorController implements ErrorController {
@RequestMapping("/error")
public Result<Map<String, Object>> error(HttpServletRequest request) {
Integer status = (Integer) request.getAttribute(
"javax.servlet.error.status_code");
Throwable ex = (Throwable) request.getAttribute(
"javax.servlet.error.exception");
String message = "未知错误";
if (ex != null) {
message = ex.getMessage();
if (message == null) {
message = ex.getClass().getSimpleName();
}
}
Map<String, Object> detail = new HashMap<>();
detail.put("status", status);
detail.put("message", message);
return Result.fail(status, message);
}
}
九、常见踩坑与排错
9.1 @ExceptionHandler 不生效
原因 1 :@ControllerAdvice 的包路径不对,没扫描到 Controller。
解法 :检查包路径或用 @ControllerAdvice(basePackages = "com.example")。
原因 2 :异常类型不匹配。@ExceptionHandler(NullPointerException.class) 捕获不了 ArithmeticException。
解法 :用 @ExceptionHandler(Exception.class) 兜底,精确类型放在前面。
原因 3 :多个 @ControllerAdvice 中的 @ExceptionHandler 匹配同一种异常,执行顺序不可控。
解法 :用 @Order 控制优先级:
java
@RestControllerAdvice
@Order(1) // 数字越小优先级越高
public class BusinessExceptionHandler { ... }
@RestControllerAdvice
@Order(2)
public class SystemExceptionHandler { ... }
9.2 异常被吞了
原因 :@ExceptionHandler 方法内部又抛了异常。
java
@ExceptionHandler(Exception.class)
public Result<Void> handle(Exception e) {
throw new RuntimeException("又炸了"); // 这个异常不会被另一个 @ExceptionHandler 捕获
}
解法 :@ExceptionHandler 方法内部不要再抛异常,所有分支都要有兜底返回。
9.3 异步线程的异常
@Async 方法中抛的异常不会被 @ExceptionHandler 捕获,因为执行线程已经不在 DispatcherServlet 的请求上下文中。
解法 :异步方法内部自行处理异常,或通过 AsyncUncaughtExceptionHandler 全局处理:
java
@Configuration
public class AsyncConfig implements AsyncConfigurer {
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) -> {
log.error("异步任务异常: method={}", method.getName(), ex);
};
}
}
9.4 参数校验异常的消息格式
MethodArgumentNotValidException 默认的 getMessage() 格式很丑,不适合直接返回给前端。
解法 :在 @ExceptionHandler 中手动格式化:
java
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValidation(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(fe -> fe.getField() + ": " + fe.getDefaultMessage())
.collect(Collectors.joining("; "));
return Result.fail(ErrorCode.PARAM_ERROR.getCode(), message);
}
输出效果:name: 不能为空; age: 必须大于0,比原始的 Validation failed for ... 友好得多。
十、异常处理最佳实践总结
| 要点 | 说明 |
|---|---|
| 自定义异常体系 | 区分业务异常/系统异常/参数异常,不直接用 RuntimeException |
| 错误码体系 | 用枚举管理,code + message,前端根据 code 做差异化处理 |
| @ExceptionHandler 分层 | 精确类型在前,Exception 兜底在后 |
| ErrorController 兜底 | Filter 异常 + 无匹配的异常,都由 ErrorController 兜底 |
| 关闭堆栈信息 | 生产环境 include-stacktrace: never,防止信息泄露 |
| 异步异常单独处理 | @Async 异常走 AsyncUncaughtExceptionHandler |
| 日志分级 | 业务异常 warn,系统异常 error,参数异常 info |
| 不在 @ExceptionHandler 里抛异常 | 避免"异常套异常"的死循环 |
十一、总结
| 层次 | 机制 | 处理范围 |
|---|---|---|
| DispatcherServlet | processHandlerException() |
Controller 层异常 |
| HandlerExceptionResolver 链 | 三级 Resolver 依次处理 | @ExceptionHandler / @ResponseStatus / 默认 |
| @ControllerAdvice | 扫描 + 反射调用 | 仅 Controller 层 |
| ErrorController | /error 端点兜底 | 所有未被处理的异常 |
| Servlet 容器 | 默认 500 页面 | 兜底 |
记住一句话:Spring Boot 异常处理 = DispatcherServlet 内部 Resolver 链 + ErrorController 外部兜底。Controller 异常走 Resolver,其他异常走 ErrorController。两层都配好,才能保证任何异常都返回统一格式的 JSON。
下一篇,我们聊聊 Spring Boot 的缓存机制------@Cacheable 背后到底发生了什么。