SpringBoot全局异常处理

先讲 Spring Boot 全局异常处理的简洁实现思路,再结合苍穹外卖项目代码讲解。

零基础预备

如果你刚接触后端,先记住下面 4 句话:

  1. 异常是什么 :程序运行到某一步失败了(比如密码不对、数据重复),会 throw 一个异常对象。
  2. 为什么不在每个接口里 try-catch :重复、啰嗦、易漏。更推荐业务处抛异常,统一处处理异常
  3. @RestControllerAdvice 是什么:给所有 Controller 做"统一增强",其中一个能力就是集中处理异常。
  4. @ExceptionHandler 是什么:告诉 Spring"这个方法专门处理哪一类异常"。

一个最小例子(不依赖苍穹外卖项目)

java 复制代码
// 业务层:只负责抛出语义明确的异常
if (!passwordOk) {
    throw new RuntimeException("密码错误");
}
java 复制代码
// 全局处理层:统一把异常变成前端可读 JSON
@RestControllerAdvice
public class DemoExceptionHandler {
    @ExceptionHandler(RuntimeException.class)
    public Map<String, Object> handle(RuntimeException ex) {
        return Map.of("code", 0, "msg", ex.getMessage());
    }
}

你可以先把它理解成一句话:
业务代码负责"发现错误并抛出",全局处理器负责"把错误翻译成统一返回格式"。


一、Spring Boot 的简洁实现思路

我们不希望每个 Controller 都写重复的 try-catch,而是把异常统一转成前端可识别的 JSON。

最小实现只需要 3 个点:

  1. 统一返回结构(如 Result
  2. 业务异常体系(如 BaseException + 子类)
  3. 全局异常处理器(@RestControllerAdvice + @ExceptionHandler

运行流程

sequenceDiagram participant Controller participant Service participant DispatcherServlet participant ExceptionResolver participant GlobalExceptionHandler participant 浏览器 Controller->>Service: 调用业务方法 Service-->>DispatcherServlet: throw BaseException("密码错误") DispatcherServlet->>ExceptionResolver: 触发异常解析链 ExceptionResolver->>GlobalExceptionHandler: 匹配 @ExceptionHandler GlobalExceptionHandler->>浏览器: Result.error("密码错误")
  • 业务层只负责"什么时候抛错、抛什么错"
  • 全局处理器负责"返回给前端什么格式"
  • 前端只认一种失败结构,不需要针对每个接口写不同解析逻辑

最小代码骨架(通用写法)

java 复制代码
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BaseException.class)
    public Result<?> handleBase(BaseException ex) {
        return Result.error(ex.getMessage());
    }

    @ExceptionHandler(Exception.class)
    public Result<?> handleUnknown(Exception ex) {
        return Result.error("系统繁忙,请稍后重试");
    }
}

核心思想:错误展示是横切关注点,应集中处理,而不是散落在每个接口里。


二、苍穹外卖项目是怎么实现的

2.1 业务场景

场景 谁抛异常 用户看到什么(示意)
登录时用户名不存在 AccountNotFoundException 账号不存在类提示
登录密码错误 PasswordErrorException 密码错误
账号被禁用 AccountLockedException 账号锁定
新增员工用户名重复 SQLIntegrityConstraintViolationException "xxx已存在"类文案
删除起售中的菜品 DeletionNotAllowedException 起售中不能删

这些异常最终都应在 Result 中以统一 JSON 返回(业务失败多为 HTTP 200 + code=0)。

抛异常位置(Service)

java 复制代码
        if (employee == null) {
            // 账号不存在
            throw new AccountNotFoundException(MessageConstant.ACCOUNT_NOT_FOUND);
        }
        ...
        if (!password.equals(employee.getPassword())) {
            // 密码错误
            throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR);
        }
        if (employee.getStatus() == StatusConstant.DISABLE) {
            // 账号被锁定
            throw new AccountLockedException(MessageConstant.ACCOUNT_LOCKED);
        }
java 复制代码
        for (Long id : ids) {
            Dish dish = dishMapper.getById(id);
            if (dish.getStatus() == StatusConstant.ENABLE) {
                // 当前菜品处于起售中,不能删除
                throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
            }
        }

        List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids);
        if (setmealIds != null && setmealIds.size() > 0) {
            throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
        }

统一返回结构(Result)

java 复制代码
@Data
public class Result<T> implements Serializable {

    private Integer code; // 编码:1 成功,0 和其它数字为失败
    private String msg; // 错误信息
    private T data; // 返回数据

    public static <T> Result<T> error(String msg) {
        Result result = new Result();
        result.msg = msg;
        result.code = 0;
        return result;
    }
}

约定:前端通常判断 code == 1 成功,否则读取 msg 提示。

业务异常基类(BaseException)

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

/**
 * 业务异常
 */
public class BaseException extends RuntimeException {

    public BaseException() {
    }

    public BaseException(String msg) {
        super(msg);
    }
}

项目中很多异常都继承它(如 AccountNotFoundExceptionPasswordErrorExceptionOrderBusinessException)。 这样全局处理器可以一次覆盖多数业务异常。

全局处理器

java 复制代码
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler
    public Result exceptionHandler(BaseException ex){
        log.error("异常信息:{}", ex.getMessage());
        return Result.error(ex.getMessage());
    }

    @ExceptionHandler
    public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
        String message = ex.getMessage();
        if(message.contains("Duplicate entry")){
            String[] split = message.split(" ");
            String username = split[2];
            String msg = username + MessageConstant.ALREADY_EXISTS;
            return Result.error(msg);
        }else{
            return Result.error(MessageConstant.UNKNOWN_ERROR);
        }
    }
}

当前仓库里显式处理了两类异常:

  • BaseException:返回业务可读文案
  • SQLIntegrityConstraintViolationException:把重复键英文报错转成"已存在"提示

执行顺序可以概括为:

  1. Service 抛出 BaseException 子类
  2. Spring 匹配到 GlobalExceptionHandler 对应方法
  3. 返回 Result.error(...)
  4. 前端统一按 Result 结构展示提示

三、业务错误与 401 的区别(容易混淆)

类型 典型来源 HTTP 层 前端处理
未登录 / token 无效 JwtTokenAdminInterceptor 401 跳转登录页
已登录但业务不允许 BaseException 多为 200 + Result.code=0 弹出业务提示

不要混用,否则前端无法区分"重新登录"和"业务失败"。


四、注意事项与可选增强

  1. 不要吞异常:捕获后至少记录日志。
  2. 生产环境建议有兜底处理器:@ExceptionHandler(Exception.class)
  3. 建议记录堆栈:log.error("...", ex),排障比只打 message 更有效。
  4. SQL 重复键字符串解析较脆弱,生产可考虑错误码映射或约束名规范。
  5. 异常不要滥用做流程控制,能正常分支表达的就不要抛异常。

总结

@RestControllerAdvice + @ExceptionHandler 把"错误返回格式"集中到一个地方治理。

业务代码专注抛出语义明确的异常(如 BaseException 子类),全局处理器负责统一转成 Result 返回前端。


附录:相关源码路径

css 复制代码
sky-server/src/main/java/com/sky/handler/GlobalExceptionHandler.java
sky-common/src/main/java/com/sky/exception/BaseException.java
sky-common/src/main/java/com/sky/result/Result.java
sky-common/src/main/java/com/sky/exception/*.java

参考

苍穹外卖www.bilibili.com/video/BV1TP...

deekseek-v4

相关推荐
skilllite作者7 小时前
Warp 终端效能与交互体验全景展示
人工智能·后端·架构·rust
0xDevNull7 小时前
Spring注解@Requestbody、@Requestparam、@PathVariable
java·后端·spring
IT_陈寒7 小时前
为什么我的Python multiprocessing总是卡在join()?
前端·人工智能·后端
QuZero8 小时前
ReentrantReadWriteLock mechanism
java·后端·算法
Victor3568 小时前
MongoDB(116)升级MongoDB时需要注意哪些事项?
后端
Victor3568 小时前
MongoDB(115)如何升级MongoDB?
后端
Rust研习社8 小时前
Rust 高性能内存缓存 moka 完全指南
开发语言·后端·缓存·rust
麦麦大数据8 小时前
基于以太坊区块链+Spring Boot+Solidity智能合约的投票系统设计与实现
spring boot·后端·区块链·智能合约·投票系统
薪火铺子8 小时前
CAS单点登录原理与实践
java·后端