Spring Boot 全局异常处理:从入门到原理源码解析

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方法并缓存
}

关键步骤包括:

  1. 扫描整个Spring容器 ,找出所有标注了@ControllerAdvice的Bean
  2. 对每一个@ControllerAdvice类,使用ExceptionHandlerMethodResolver扫描所有方法
  3. 找出标注了@ExceptionHandler的方法
  4. 建立异常类型 → 处理方法的映射关系并缓存

每一个Controller或Advice,都会对应一个ExceptionHandlerMethodResolver实例。

3.4 @ControllerAdvice的作用范围机制

@ControllerAdvice支持通过basePackagesannotations等属性限定作用范围:

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抛出异常时,执行流程如下:

  1. DispatcherServletdoDispatch()方法捕获异常
  2. 进入processDispatchResult()方法处理异常
  3. 遍历HandlerExceptionResolver责任链,首先调用ExceptionHandlerExceptionResolver
  4. ExceptionHandlerExceptionResolver根据异常类型,从缓存的映射中查找匹配的@ExceptionHandler方法
  5. 找到后通过反射执行该方法,将返回值写入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 最佳实践建议

  1. 使用@RestControllerAdvice:前后端分离项目优先使用,自动返回JSON
  2. 定义统一返回格式 :封装codemessagedata三个字段
  3. 自定义业务异常 :继承RuntimeException,携带错误码和错误信息
  4. 精确优先、兜底在后 :具体异常写在前面,Exception.class兜底
  5. 记录日志:在异常处理方法中打印日志,便于线上问题排查
  6. 生产环境隐藏细节:兜底异常不要返回堆栈信息,避免安全风险

4.3 原理一句话总结

全局异常处理本质上是Spring MVC在启动时扫描所有@ControllerAdvice类中的@ExceptionHandler方法,建立异常类型到处理方法的映射缓存;运行时由ExceptionHandlerExceptionResolver根据异常类型匹配并执行对应方法,实现统一的异常拦截和响应

相关推荐
小小洋洋7 小时前
OpenWrt 从U盘迁移到内置 eMMC,并完成扩容与 Docker 安装
java·docker·eureka
SomeB1oody7 小时前
【RustyML入门】2.0. 经典机器学习
开发语言·后端·机器学习·rust·教程
就改了7 小时前
SpringBoot 自定义线程池 + 实时监控指标
java·spring boot·后端
暗黑小白7 小时前
脱敏引擎工程化
后端·ai agent
plainGeekDev7 小时前
运行时获取依赖 → 编译时注入
android·java·kotlin
用户8181870627467 小时前
第23章 JPA / Hibernate 异常
后端
用户8181870627467 小时前
第22章 MyBatis / MyBatis-Plus 常见异常与 SQL 调试
后端
雪隐8 小时前
个人电脑玩AI-15让5060 Ti给你打工——MiniMax H3 本地部署实录:一个自带录音棚的视频模型,和它的 NVFP4 瘦身奇遇
前端·人工智能·后端
玖石书9 小时前
ASP.NET Core 迁移至 Spring系列:类库框架篇
java·后端·asp.net
笨蛋不要掉眼泪9 小时前
RabbitMQ消息队列:延迟消息
java·rabbitmq·java-rabbitmq