SpringBoot 全局统一异常处理器

一、概括

在 SpringBoot Web 项目开发中,如果不对异常做统一处理,程序报错时会直接抛出原生异常堆栈,前端会收到杂乱的错误页面 / 无规范 JSON,用户体验差、前后端联调麻烦。

采用 @RestControllerAdvice + @ExceptionHandler 实现全局统一异常捕获,分为两层处理:

  1. 精准捕获数据库唯一索引冲突 DuplicateKeyException(重复数据),返回友好提示
  2. 兜底捕获所有未知 Exception,统一返回系统错误提示,同时完整打印日志便于排查

二、核心注解说明

  1. @RestControllerAdvice
    • 作用:全局 Controller 增强注解,只拦截所有 @RestController 接口抛出的异常
    • 等价组合:@ControllerAdvice + @ResponseBody,方法返回值直接转为 JSON 响应给前端
    • 生效范围:整个项目所有接口,无需在每个 Controller 重复写 try-catch
  2. @ExceptionHandler
    • 作用:标记当前方法为异常处理方法,括号内指定要捕获的异常类型
    • 优先级规则:精确异常优先于父类异常
      示例:DuplicateKeyExceptionException 的子类,数据库重复报错会先走专门处理方法,不会进入兜底通用异常方法
  3. @Slf4j
    Lombok 日志注解,自动生成 log 对象,用来打印异常完整堆栈,线上排查问题必备。
java 复制代码
package org.example.springbootpractice.exception;

import lombok.extern.slf4j.Slf4j;
import org.example.springbootpractice.dept.utils.Result;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * 全局异常处理器
 * 统一拦截所有Controller抛出的异常,规范化返回前端JSON
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

	/**
    * 兜底异常处理:捕获所有未单独处理的通用异常
    * @param e 通用异常对象
    * @return 统一错误返回体
    */
    @ExceptionHandler
    public Result<String> handleException(Exception e) {
    	// 打印完整异常堆栈日志,方便后端排查
        log.error("程序出错了~", e);
        // 返回通用友好提示,屏蔽底层错误细节,避免泄露系统信息
        return Result.fail("服务器异常");
    }

	 /**
     * 精准捕获:数据库唯一约束重复异常 DuplicateKeyException
     * 场景:数据库字段添加unique唯一索引,新增/修改时出现重复数据触发
     * @param e 唯一键冲突异常
     * @return 提取重复字段,返回人性化提示
     */
    @ExceptionHandler
    public Result<String> handleDuplicateKeyException(DuplicateKeyException e) {
        log.error("程序出错了~", e);
        // 提取异常信息
        String message = e.getMessage();
        // 定位Duplicate entry关键字起始位置
        int i = message.lastIndexOf("Duplicate entry");
        String errMsg = message.substring(i);
        // 按空格分割错误信息,格式示例:Duplicate entry '张三' for key 'name'
        String[] arr = errMsg.split(" ");
        // arr[2] 为重复的值,拼接提示返回前端
        return Result.fail("【" + arr[2] + "】已存在");
    }
}

三、两种异常场景测试效果

场景 1:数据库唯一索引重复(用户名 / 手机号重复)

数据库给 username 添加 unique 唯一索引,重复插入同名数据,触发 DuplicateKeyException

  1. 后端日志:完整打印异常堆栈
  2. 前端返回 JSON:
json 复制代码
{
    "code": 0,
    "msg": "张三 已存在",
    "data": null
}

场景 2:系统未知异常(空指针、数组越界、SQL 语法错误等)

代码出现未捕获的空指针异常,进入兜底 handleException 方法

前端统一返回:

json 复制代码
{
    "code": 0,
    "msg": "程序出错了~",
    "data": null
}

优势:不暴露底层报错信息,防止数据库表名、字段、SQL 等敏感信息泄露。

四、核心知识点详解

1. 异常执行优先级原理

Spring 异常处理器匹配规则:就近匹配精确异常

  • DuplicateKeyException 继承自 DataAccessException,最终父类是 Exception
  • 当抛出重复键异常时,优先匹配 handleDuplicateKeyException,不会走到通用 handleException
  • 只有抛出其他所有未单独定义的异常,才会进入兜底方法

2. @RestControllerAdvice 生效范围限制

  1. 只拦截 Controller 层抛出 的异常
    • Service 内部 try-catch 吃掉的异常不会进入全局处理器
    • 过滤器(Filter)中抛出的异常无法被捕获(过滤器执行在 ControllerAdvice 之前)
  2. 只对 @RestController / @Controller 接口生效,定时任务、单元测试异常不拦截

3. 日志打印 log.error("描述", e) 的作用

  • 只写 log.error("程序出错了~"):只会打印文字,无异常堆栈,无法定位报错行号
  • log.error("程序出错了~", e):会完整输出异常堆栈、报错类、行号、异常根因,线上排查必备

4. DuplicateKeyException 解析逻辑说明

MySQL 唯一索引冲突原始报错信息示例:

Duplicate entry 'zhangsan' for key 't_user.username'

代码解析流程:

  1. 截取 Duplicate entry 之后的字符串
  2. 使用空格分割数组 ["Duplicate","entry","'zhangsan'","for","key","..."]
  3. arr[2] 取出重复值 'zhangsan',拼接友好提示返回前端