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

采用 @RestControllerAdvice + @ExceptionHandler 实现全局统一异常捕获,分为两层处理:
- 精准捕获数据库唯一索引冲突
DuplicateKeyException(重复数据),返回友好提示 - 兜底捕获所有未知
Exception,统一返回系统错误提示,同时完整打印日志便于排查
二、核心注解说明
@RestControllerAdvice- 作用:全局 Controller 增强注解,只拦截所有
@RestController接口抛出的异常 - 等价组合:
@ControllerAdvice+@ResponseBody,方法返回值直接转为 JSON 响应给前端 - 生效范围:整个项目所有接口,无需在每个 Controller 重复写 try-catch
- 作用:全局 Controller 增强注解,只拦截所有
@ExceptionHandler- 作用:标记当前方法为异常处理方法,括号内指定要捕获的异常类型
- 优先级规则:精确异常优先于父类异常
示例:DuplicateKeyException是Exception的子类,数据库重复报错会先走专门处理方法,不会进入兜底通用异常方法
@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
- 后端日志:完整打印异常堆栈
- 前端返回 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 生效范围限制
- 只拦截 Controller 层抛出 的异常
- Service 内部 try-catch 吃掉的异常不会进入全局处理器
- 过滤器(Filter)中抛出的异常无法被捕获(过滤器执行在 ControllerAdvice 之前)
- 只对
@RestController / @Controller接口生效,定时任务、单元测试异常不拦截
3. 日志打印 log.error("描述", e) 的作用
- 只写
log.error("程序出错了~"):只会打印文字,无异常堆栈,无法定位报错行号 log.error("程序出错了~", e):会完整输出异常堆栈、报错类、行号、异常根因,线上排查必备
4. DuplicateKeyException 解析逻辑说明
MySQL 唯一索引冲突原始报错信息示例:
Duplicate entry 'zhangsan' for key 't_user.username'
代码解析流程:
- 截取
Duplicate entry之后的字符串 - 使用空格分割数组
["Duplicate","entry","'zhangsan'","for","key","..."] arr[2]取出重复值'zhangsan',拼接友好提示返回前端