Spring MVC 全局异常处理实战

在 Spring Boot 前后端分离项目开发中,异常处理是保障系统健壮性的关键环节。如果不对异常做统一处理,Controller 层抛出的异常会直接返回给前端杂乱的 500 错误页面或原生异常信息,不仅前端无法友好解析,还可能泄露服务端核心代码信息,影响系统安全性和用户体验。

而基于 Spring MVC 的@RestControllerAdvice + @ExceptionHandler实现的全局异常处理,正是解决这一问题的最优解。它就像系统的 "统一兜底管家",能拦截项目中 Controller 层抛出的所有异常,将其转换为前端能统一解析的标准 JSON 响应格式,让异常处理更规范、更优雅。本文将从核心原理、代码实现、执行流程三个维度,详解项目中全局异常处理的设计与落地。

一、全局异常处理核心思想:统一拦截,标准化响应

在讲解具体实现前,先理解全局异常处理的核心设计思想,这是整个方案的基础:

  1. 统一兜底 :将项目中分散在各个 Controller、Service 层的异常捕获逻辑抽离出来,集中在一个类中统一处理,避免重复的try-catch代码,让业务代码更专注于核心逻辑;
  2. 精准匹配:针对不同类型的异常(如业务异常、SQL 异常、系统异常),配置对应的异常处理方法,实现 "什么异常用什么方式处理";
  3. 标准化响应:将所有异常都转换为项目统一的 Result 响应格式,前端只需根据固定的格式解析(如错误码、错误信息),无需适配不同的异常返回结果;
  4. 无侵入式 :无需修改原有业务代码,只需在需要抛异常的地方直接throw异常即可,全局异常处理类会自动拦截,符合 "开闭原则"。

整个方案的核心只有一个类 ------GlobalExceptionHandler,通过两个核心注解实现所有功能,代码简洁且功能强大,是 Spring MVC 提供的原生解决方案,无需引入任何第三方依赖。

二、核心注解解析:@RestControllerAdvice & @ExceptionHandler

全局异常处理的实现依赖 Spring MVC 的两个核心注解,这两个注解是整个方案的 "基石",先搞懂它们的作用,才能理解后续的代码实现:

1. @RestControllerAdvice:声明全局异常处理类

  • 核心作用 :标记当前类为全局异常处理类 ,并具备两个关键特性:
    1. 全局生效 :能拦截整个项目中所有@RestController/@Controller标记的类抛出的异常;
    2. 自动 JSON 序列化 :处理异常的方法返回值会自动转换为 JSON 格式,无需再添加@ResponseBody注解,完美适配前后端分离的 JSON 交互场景。
  • 底层原理:基于 AOP(面向切面编程)实现,相当于在所有 Controller 层方法上织入了异常捕获的切面,当方法抛出异常时,自动触发对应的异常处理逻辑。

2. @ExceptionHandler:指定处理的异常类型

  • 核心作用 :标注在方法上,指定该方法专门处理某一种 / 某一类 异常,如@ExceptionHandler(BaseException.class)表示该方法处理所有BaseException及其子类的异常;
  • 匹配规则 :当系统抛出异常时,Spring MVC 会根据异常的类型,自动匹配到对应的@ExceptionHandler方法,精准执行处理逻辑;
  • 灵活度高 :一个全局异常处理类中可以定义多个@ExceptionHandler方法,分别处理不同类型的异常,实现异常的精细化处理。

三、核心实现类:GlobalExceptionHandler------ 系统的 "异常兜底管家"

项目中的全局异常处理核心类为GlobalExceptionHandler,它通过上述两个核心注解,实现了业务异常SQL 异常两大核心场景的处理,同时可轻松扩展处理其他类型的异常。以下是完整的实战代码,并附带详细的代码解释,贴合企业级开发规范。

1. 先备基础:统一响应类 Result & 基础业务异常类 BaseException

在实现全局异常处理前,项目中需先定义统一响应类 Result基础业务异常类 BaseException,这是全局异常处理的基础,保证异常响应的标准化和业务异常的统一管理。

(1)统一响应类 Result

项目中所有接口的返回结果都遵循该格式,异常处理也不例外,让前端能统一解析响应数据:

java 复制代码
package com.sky.result;

import lombok.Data;

/**
 * 全局统一响应结果类
 */
@Data
public class Result<T> {
    /**
     * 响应码:0表示成功,1/其他表示失败
     */
    private Integer code;
    /**
     * 响应信息:成功时为"成功",失败时为错误信息
     */
    private String msg;
    /**
     * 响应数据:成功时返回业务数据,失败时为null
     */
    private T data;

    // 快速返回成功结果(无数据)
    public static <T> Result<T> success() {
        Result<T> result = new Result<>();
        result.setCode(0);
        result.setMsg("成功");
        return result;
    }

    // 快速返回成功结果(带数据)
    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.setCode(0);
        result.setMsg("成功");
        result.setData(data);
        return result;
    }

    // 快速返回失败结果(自定义错误信息)
    public static <T> Result<T> error(String msg) {
        Result<T> result = new Result<>();
        result.setCode(1);
        result.setMsg(msg);
        return result;
    }
}
(2)基础业务异常类 BaseException

项目中所有的业务异常都继承自该类,实现业务异常的统一管理,如密码错误、账号锁定、参数非法等:

java 复制代码
package com.sky.exception;

/**
 * 项目基础业务异常类,所有业务异常都继承此类
 */
public class BaseException extends RuntimeException {
    // 构造方法,传入错误信息
    public BaseException(String message) {
        super(message);
    }
}
(3)业务异常子类示例(密码错误)

可根据业务需求,定义多个 BaseException 的子类,实现更精细化的业务异常管理:

java 复制代码
package com.sky.exception;

/**
 * 密码错误异常
 */
public class PasswordErrorException extends BaseException {
    public PasswordErrorException(String message) {
        super(message);
    }
}

2. 全局异常处理核心代码

java 复制代码
package com.sky.handler;

import com.sky.constant.MessageConstant;
import com.sky.exception.BaseException;
import com.sky.result.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.sql.SQLIntegrityConstraintViolationException;

/**
 * 全局异常处理类,统一拦截并处理项目中所有的异常
 */
@RestControllerAdvice // 声明全局异常处理类,返回值自动转JSON
@Slf4j // 日志注解,记录异常信息
public class GlobalExceptionHandler {

    /**
     * 处理业务异常:捕获BaseException及其所有子类异常
     * 适用场景:密码错误、账号锁定、参数非法等自定义业务异常
     */
    @ExceptionHandler(BaseException.class)
    public Result<String> handleBaseException(BaseException ex) {
        // 记录异常信息,便于问题排查
        log.error("业务异常:{}", ex.getMessage(), ex);
        // 将业务异常的错误信息封装为标准Result格式,返回给前端
        return Result.error(ex.getMessage());
    }

    /**
     * 处理SQL唯一约束异常:捕获用户名/手机号等唯一字段重复的异常
     * 适用场景:新增员工/用户时,用户名重复;新增手机号时,号码重复等
     */
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public Result<String> handleSQLIntegrityException(SQLIntegrityConstraintViolationException ex) {
        log.error("SQL约束异常:{}", ex.getMessage(), ex);
        // 异常信息示例:Duplicate entry 'zhangsan' for key 'idx_employee_username'
        String message = ex.getMessage();
        // 解析异常信息,提取重复的字段值,返回友好的错误信息
        if (message.contains("Duplicate entry")) {
            String[] split = message.split(" ");
            String duplicateValue = split[2].replace("'", ""); // 去除单引号,如zhangsan
            String msg = duplicateValue + "已存在,请勿重复添加";
            return Result.error(msg);
        }
        // 其他SQL约束异常,返回通用错误信息
        return Result.error(MessageConstant.UNKNOWN_ERROR);
    }

    /**
     * 处理系统通用异常:捕获所有未被上述方法匹配的异常(兜底处理)
     * 适用场景:空指针、数组越界、网络异常等系统级异常
     */
    @ExceptionHandler(Exception.class)
    public Result<String> handleGlobalException(Exception ex) {
        log.error("系统异常:{}", ex.getMessage(), ex);
        // 系统异常不暴露具体错误信息给前端,返回通用友好提示
        return Result.error(MessageConstant.SYSTEM_ERROR);
    }
}

3. 关键代码详解

(1)业务异常处理方法handleBaseException
  • 匹配范围 :处理所有BaseException及其子类(如PasswordErrorExceptionAccountLockedException),覆盖项目中所有自定义的业务异常;
  • 处理逻辑:记录异常日志→将异常的自定义错误信息(如 "密码错误")封装为 Result 格式返回;
  • 设计优势 :业务层只需直接抛出new BaseException("错误信息"),无需关心异常如何处理,全局异常处理类会自动拦截,让业务代码更简洁。
(2)SQL 唯一约束异常处理方法handleSQLIntegrityException
  • 匹配场景 :专门处理数据库唯一索引约束异常(如用户名、手机号重复),该异常由数据库直接抛出,类型为SQLIntegrityConstraintViolationException
  • 友好解析:对数据库原生的异常信息进行解析,提取重复的字段值,返回如 "zhangsan 已存在,请勿重复添加" 的友好信息,而非原生的杂乱 SQL 异常信息;
  • 用户体验:前端收到友好的错误信息后,可直接弹窗提示用户,提升交互体验。
(3)系统通用异常处理方法handleGlobalException
  • 兜底处理 :匹配Exception.class,即所有未被上述方法处理的异常都会被该方法捕获,避免有异常未被拦截而返回原生错误;
  • 安全设计:系统级异常(如空指针、数组越界)的具体信息不暴露给前端,仅返回 "系统繁忙,请稍后重试" 等通用友好提示,防止泄露服务端核心代码信息;
  • 日志记录:详细记录异常的堆栈信息,便于开发人员排查问题,实现 "前端友好,后端可查"。

四、实战使用:业务层直接抛异常,无需任何处理

完成全局异常处理类的编写后,在项目中使用变得极其简单 ------业务层只需根据业务逻辑直接抛出异常,无需编写任何 try-catch 代码,全局异常处理类会自动拦截并处理,返回标准化的 JSON 响应。

1. 业务层抛业务异常示例(员工登录密码错误)

在员工登录的 Service 层中,若校验密码错误,直接抛出PasswordErrorException(继承自 BaseException):

java 复制代码
package com.sky.service.impl;

import com.sky.constant.MessageConstant;
import com.sky.entity.Employee;
import com.sky.exception.PasswordErrorException;
import com.sky.mapper.EmployeeMapper;
import com.sky.service.EmployeeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class EmployeeServiceImpl implements EmployeeService {

    @Autowired
    private EmployeeMapper employeeMapper;

    @Override
    public Employee login(String username, String password) {
        // 1. 根据用户名查询员工
        Employee employee = employeeMapper.selectByUsername(username);
        // 2. 用户名不存在,抛业务异常
        if (employee == null) {
            throw new BaseException(MessageConstant.USERNAME_NOT_FOUND);
        }
        // 3. 密码错误,抛密码错误异常
        if (!password.equals(employee.getPassword())) {
            throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR);
        }
        // 4. 账号锁定,抛业务异常
        if (employee.getStatus() == 0) {
            throw new BaseException(MessageConstant.ACCOUNT_LOCKED);
        }
        // 5. 登录成功,返回员工信息
        return employee;
    }
}

2. 前端收到的标准化异常响应

当密码错误时,全局异常处理类会自动拦截PasswordErrorException,返回以下 JSON 响应,前端可直接解析msg字段提示用户:

复制代码
{
  "code": 1,
  "msg": "密码错误,请重新输入",
  "data": null
}

3. SQL 唯一约束异常示例(新增员工用户名重复)

当新增员工时,若用户名已存在,数据库会抛出SQLIntegrityConstraintViolationException,全局异常处理类会解析异常信息,返回友好的响应:

复制代码
{
  "code": 1,
  "msg": "zhangsan已存在,请勿重复添加",
  "data": null
}

4. 系统通用异常示例(空指针异常)

当项目中出现空指针异常时,全局异常处理类会捕获该异常,返回通用友好提示,同时在服务端日志中记录详细的堆栈信息:

复制代码
{
  "code": 1,
  "msg": "系统繁忙,请稍后重试",
  "data": null
}

五、完整执行流程:从抛异常到前端收到响应

全局异常处理的整个执行流程完全自动化,对开发者无感知,具体步骤如下(以密码错误为例):

  1. 业务层抛异常 :员工登录时,Service 层校验密码错误,抛出PasswordErrorException("密码错误,请重新输入")
  2. 异常向上冒泡:该异常会从 Service 层向上抛到 Controller 层,Controller 层未编写 try-catch 代码,异常继续向上抛;
  3. 全局拦截异常 :Spring MVC 的核心调度器DispatcherServlet捕获到异常,发现项目中有@RestControllerAdvice标记的GlobalExceptionHandler类;
  4. 匹配异常处理方法 :根据异常类型PasswordErrorException(继承自 BaseException),自动匹配到handleBaseException方法;
  5. 处理异常并返回 :执行handleBaseException方法,记录异常日志,将异常信息封装为标准 Result 格式的 JSON 响应;
  6. 前端接收响应 :前端收到包含错误码和错误信息的 JSON 数据,根据code=1判断为失败,解析msg字段弹窗提示用户。

整个流程一气呵成,无需开发者介入,实现了 "抛异常即处理" 的优雅效果。

六、扩展场景:轻松适配更多异常类型

项目中的GlobalExceptionHandler具备极强的可扩展性,若需要处理其他类型的异常,只需新增一个 @ExceptionHandler 方法,指定对应的异常类型即可,无需修改原有代码。以下是几个常见的扩展场景示例:

1. 处理请求参数校验异常(@Validated 校验失败)

项目中常用@Validated做请求参数的后端校验(如非空、长度限制),校验失败会抛出MethodArgumentNotValidException,新增处理方法:

java 复制代码
/**
 * 处理请求参数校验异常:@Validated校验失败时触发
 */
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<String> handleValidException(MethodArgumentNotValidException ex) {
    log.error("参数校验异常:{}", ex.getMessage(), ex);
    // 获取第一个校验失败的错误信息
    String msg = ex.getBindingResult().getFieldError().getDefaultMessage();
    return Result.error(msg);
}

2. 处理跨域异常

若项目中跨域配置不当,会抛出跨域异常,新增处理方法:

java 复制代码
/**
 * 处理跨域异常
 */
@ExceptionHandler(NoHandlerFoundException.class)
public Result<String> handleNoHandlerFoundException(NoHandlerFoundException ex) {
    log.error("跨域异常:{}", ex.getMessage(), ex);
    return Result.error(MessageConstant.CROSS_DOMAIN_ERROR);
}

3. 处理 Redis 连接异常

若项目中使用 Redis,Redis 连接失败会抛出异常,新增处理方法:

java 复制代码
/**
 * 处理Redis连接异常
 */
@ExceptionHandler(RedisConnectionFailureException.class)
public Result<String> handleRedisException(RedisConnectionFailureException ex) {
    log.error("Redis连接异常:{}", ex.getMessage(), ex);
    return Result.error(MessageConstant.REDIS_CONNECT_ERROR);
}

七、方案优势:为什么说这是企业级项目的最佳实践?

这套基于@RestControllerAdvice + @ExceptionHandler的全局异常处理方案,是 Spring MVC 项目中异常处理的企业级最佳实践 ,相比传统的分散式try-catch,具有以下核心优势:

1. 消除重复代码,提升代码整洁度

将分散在各个 Controller、Service 层的try-catch代码全部抽离,集中在一个类中统一处理,业务代码中只需专注于核心逻辑,无需关心异常处理,代码更简洁、更优雅。

2. 标准化响应,降低前后端沟通成本

所有异常都返回统一的 Result 格式,前端只需根据固定的codemsg字段解析响应,无需适配不同的异常返回结果,大幅降低前后端的沟通和开发成本。

3. 精细化处理,兼顾友好性和可排查性

  • 对前端友好:业务异常返回自定义的友好错误信息,系统异常返回通用提示,避免前端收到杂乱的原生异常信息;
  • 对后端友好:详细记录所有异常的日志(包括堆栈信息),开发人员可通过日志快速定位问题,实现 "前端友好,后端可查"。

4. 无侵入式设计,符合开闭原则

无需修改原有业务代码,只需在需要抛异常的地方直接throw即可,新增异常类型时,只需新增对应的@ExceptionHandler方法,无需修改原有处理逻辑,完全符合 "对扩展开放,对修改关闭" 的开闭原则。

5. 全局生效,全覆盖无遗漏

能拦截项目中所有 Controller 层抛出的异常,包括业务异常、SQL 异常、系统异常等,实现异常处理的全覆盖,避免有异常未被拦截而返回原生 500 错误。

6. 可扩展性强,适配所有业务场景

可根据项目需求,轻松扩展处理任意类型的异常,只需新增方法即可,适配从简单的业务异常到复杂的系统异常的所有场景。

八、生产环境优化建议

以上实现为项目基础版的全局异常处理方案,在生产环境中,可根据实际需求进行以下优化,让方案更健壮、更安全:

  1. 自定义错误码体系:扩展 Result 类,增加自定义错误码(如 1001 = 密码错误、1002 = 账号锁定、2001 = 用户名重复),让前端能根据错误码做更精细化的处理(如不同的错误提示样式);
  2. 异常分类管理:将异常分为业务异常、系统异常、第三方异常(如 Redis、MQ、接口调用异常),分别进行处理,返回不同的错误码和提示信息;
  3. 统一日志格式 :规范异常日志的记录格式,包含异常类型、错误信息、请求 URL、请求 IP、用户 ID、堆栈信息等,便于生产环境的日志分析和问题排查;
  4. 添加异常监控:结合监控平台(如 Prometheus + Grafana、ELK),对系统异常进行监控和告警,当系统异常数量达到阈值时,自动发送告警通知(如邮件、钉钉、企业微信);
  5. 自定义异常页面 :若项目包含非前后端分离的页面,可通过@ControllerAdvice(不带 Rest)配合ModelAndView,返回自定义的异常页面;
  6. 限制异常日志打印级别 :生产环境中,业务异常可打印INFO级别日志,系统异常打印ERROR级别日志,避免日志过多导致磁盘溢出;
  7. 敏感信息脱敏:对异常信息中的敏感数据(如密码、手机号、身份证号)进行脱敏处理,防止敏感信息泄露在日志中。

九、总结

本文讲解的全局异常处理方案,是Spring MVC 原生特性 在企业级项目中的经典落地实践,核心是通过@RestControllerAdvice@ExceptionHandler两个注解,实现了异常的统一拦截、精准匹配、标准化响应

这套方案的核心价值,不仅在于解决了前端异常解析的问题,更在于让异常处理与业务逻辑完全解耦------ 开发者无需在业务层编写任何异常处理代码,只需专注于业务逻辑的实现,异常的捕获和处理全部由全局异常处理类自动完成。同时,其无侵入式的设计和极强的可扩展性,让它能轻松适配项目的所有业务场景,从简单的业务异常到复杂的系统异常,都能实现精细化处理。

在实际项目开发中,全局异常处理是必备的基础功能,它是保障系统健壮性、提升用户体验、降低开发成本的关键环节。掌握这套方案,不仅能解决项目中的异常处理问题,更能理解 Spring MVC 的 AOP 思想和 "集中式处理通用逻辑" 的设计理念,让我们的代码更规范、更优雅、更具可维护性。

优秀的项目架构,不仅能实现业务功能,更能在细节处做到极致 ------ 而全局异常处理,正是项目架构细节中 "优雅" 的体现。

相关推荐
BUG指挥官15 小时前
Sa-Token和Spring Security对比
java·后端·spring
geminigoth15 小时前
Spring AI Alibaba 入门开发一(备份)
java·人工智能·spring
xbgRS18 小时前
spring整合mybaits
java·spring·mybatis
ruleslol18 小时前
静态依赖注入 VS 动态依赖查询
spring
墨雨晨曦8821 小时前
Spring AI总结
java·人工智能·spring
用户3126874877201 天前
Spring Boot 异常处理到底怎么玩的?从 DispatcherServlet 到全局兜底的全链路拆解
spring
不才不才不不才1 天前
Spring 源码系列(17): HandlerMapping 与 HandlerAdapter 两大体系
java·后端·spring
(轻舟已过万重山)1 天前
第40章 Spring AI 实战:企业级 AI 应用架构
人工智能·spring·架构
堕落年代2 天前
Ollama CPU 推理大提示词优化实测报告(细致化数据版)
java·后端·spring
cfm_29142 天前
基于OAuth2.0实现微服务SSO单点登录
后端·spring·微服务·架构