Spring统一异常处理该如何做

ps:如果这篇帖子对于还在找工作和找实习的你有所帮助,可以关注我,给本贴点赞、评论、收藏并订阅专栏;同时不要吝啬您的花花

在Spring Boot/Spring MVC项目中,分散在Controller层的try-catch会导致代码冗余、响应格式不统一,全局统一异常处理能实现异常集中管控、标准化响应、日志统一打印,是后端工程化的必备实践。核心依托Spring的全局异常切面能力,无需侵入业务代码,即可完成异常拦截与处理。

一、核心原理:为什么能全局捕获异常?

Spring提供@RestControllerAdvice(或@ControllerAdvice)注解,用于定义全局控制器增强类,配合@ExceptionHandler注解指定要捕获的异常类型,实现:

  • 拦截Controller层抛出的所有指定异常,跳过原生异常跳转流程
  • 统一封装异常响应结果,保证前端接收格式一致
  • 集中处理异常日志、异常状态码、异常信息脱敏

二者搭配是Spring官方推荐的标准方案,兼容Spring MVC、Spring Boot全版本,无额外依赖。

二、标准落地:五步实现统一异常处理

步骤1:定义统一响应体(前端标准化格式)

先封装全局响应对象,确保成功/异常响应格式统一,避免前端解析混乱,包含状态码、提示信息、数据三个核心字段。

import lombok.Data;

/**
 * 全局统一返回结果
 * @param <T> 响应数据泛型
 */
@Data
public class Result<T> {
    // 响应状态码:200成功,500服务器异常,400参数错误等
    private Integer code;
    // 响应提示信息
    private String msg;
    // 响应数据
    private T data;

    // 成功响应
    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.setCode(200);
        result.setMsg("操作成功");
        result.setData(data);
        return result;
    }

    // 失败响应(异常专用)
    public static <T> Result<T> fail(Integer code, String msg) {
        Result<T> result = new Result<>();
        result.setCode(code);
        result.setMsg(msg);
        result.setData(null);
        return result;
    }
}

步骤2:自定义业务异常(区分系统异常/业务异常)

系统内置异常(空指针、数组越界)属于未知异常,业务异常(参数非法、权限不足)属于已知可预见异常,自定义业务异常可精准传递业务错误信息。

/**
 * 自定义业务异常
 */
public class BusinessException extends RuntimeException {
    // 业务异常状态码
    private Integer code;

    public BusinessException(String msg) {
        super(msg);
        this.code = 500;
    }

    public BusinessException(Integer code, String msg) {
        super(msg);
        this.code = code;
    }

    // getter
    public Integer getCode() {
        return code;
    }
}

步骤3:编写全局异常处理器(核心类)

使用@RestControllerAdvice标记全局增强类,@ExceptionHandler绑定异常类型,实现不同异常的差异化处理。

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.resource.NoResourceFoundException;

/**
 * 全局统一异常处理器
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 捕获自定义业务异常
     */
    @ExceptionHandler(BusinessException.class)
    public Result<?> handleBusinessException(BusinessException e) {
        // 打印业务异常日志(仅打印信息,不打印堆栈,避免日志冗余)
        log.error("业务异常:code={}, msg={}", e.getCode(), e.getMessage());
        return Result.fail(e.getCode(), e.getMessage());
    }

    /**
     * 捕获参数校验异常(Spring Validation常用)
     */
    @ExceptionHandler(IllegalArgumentException.class)
    public Result<?> handleIllegalArgumentException(IllegalArgumentException e) {
        log.error("参数异常:msg={}", e.getMessage());
        return Result.fail(400, e.getMessage());
    }

    /**
     * 捕获404资源不存在异常
     */
    @ExceptionHandler(NoResourceFoundException.class)
    public Result<?> handleNoResourceFoundException(NoResourceFoundException e) {
        log.error("404异常:请求路径不存在", e);
        return Result.fail(404, "请求资源不存在");
    }

    /**
     * 捕获所有未知系统异常(兜底处理,优先级最低)
     */
    @ExceptionHandler(Exception.class)
    public Result<?> handleException(Exception e) {
        // 打印完整堆栈,便于排查问题
        log.error("系统未知异常", e);
        // 脱敏返回,不暴露敏感异常信息
        return Result.fail(500, "服务器繁忙,请稍后重试");
    }
}

步骤4:业务代码抛出异常(无需try-catch)

Controller/Service层直接抛出异常,全局处理器会自动拦截,无需编写冗余try-catch。

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/test")
public class TestController {

    @GetMapping("/biz")
    public Result<String> testBizException(@RequestParam Integer id) {
        if (id == null || id < 0) {
            // 抛出自定义业务异常
            throw new BusinessException(400, "用户ID不能为空且不能小于0");
        }
        return Result.success("测试成功");
    }

    @GetMapping("/sys")
    public Result<String> testSysException() {
        // 模拟系统异常
        int i = 1 / 0;
        return Result.success("测试成功");
    }
}

步骤5:测试验证

  • 请求/test/biz?id=-1:返回400+自定义提示,日志打印业务异常
  • 请求/test/sys:返回500+脱敏提示,日志打印完整堆栈
  • 请求不存在接口:返回404提示

所有异常响应格式统一,前端可轻松解析。

三、进阶优化:工程化细节处理

1. 异常优先级管控

@ExceptionHandler按精确匹配优先原则,子类异常会优先于父类异常捕获。例如:BusinessException优先于Exception捕获,无需担心兜底异常覆盖业务异常。

2. 整合Spring Validation参数校验

针对@Valid/@Validated校验失败的MethodArgumentNotValidException,新增专属捕获逻辑,提取字段错误提示:

import org.springframework.validation.BindException;
import org.springframework.web.bind.MethodArgumentNotValidException;

@ExceptionHandler({MethodArgumentNotValidException.class, BindException.class})
public Result<?> handleValidException(Exception e) {
    String msg;
    if (e instanceof MethodArgumentNotValidException ex) {
        msg = ex.getBindingResult().getFieldError().getDefaultMessage();
    } else {
        msg = ((BindException) e).getBindingResult().getFieldError().getDefaultMessage();
    }
    log.error("参数校验异常:{}", msg);
    return Result.fail(400, msg);
}

3. 全局状态码枚举化

避免硬编码状态码,定义枚举类统一管理:

import lombok.Getter;

@Getter
public enum ResultCodeEnum {
    SUCCESS(200, "操作成功"),
    BIZ_ERROR(500, "业务异常"),
    PARAM_ERROR(400, "参数错误"),
    NOT_FOUND(404, "资源不存在"),
    NO_AUTH(401, "未授权");

    private final Integer code;
    private final String msg;

    ResultCodeEnum(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}

4. 敏感信息脱敏

生产环境禁止返回堆栈信息、数据库异常等敏感内容,仅返回友好提示;开发环境可开启详细异常信息,便于调试。

四、常见避坑指南

  1. 异常处理器未生效:检查GlobalExceptionHandler是否被Spring扫描(包路径是否在启动类扫描范围内),是否添加@RestControllerAdvice注解。
  2. 异步方法异常无法捕获:@RestControllerAdvice仅拦截Controller层同步异常,异步方法需单独处理(@Async方法可搭配@ExceptionHandler或Future捕获)。
  3. 过滤器/拦截器异常无法捕获:全局处理器仅拦截Controller层异常,过滤器异常需在Filter中手动处理,拦截器异常可通过HandlerInterceptor适配。
  4. 重复捕获异常:避免在业务层try-catch后再次抛出,否则全局处理器无法正常拦截。

核心总结:Spring统一异常处理的核心是@RestControllerAdvice + @ExceptionHandler,搭配统一响应体+自定义异常,实现异常集中管控、格式标准化、代码无侵入,是后端项目的基础规范。

ps:如果这篇帖子对于还在找工作和找实习的你有所帮助,可以关注我,给本贴点赞、评论、收藏并订阅专栏;同时不要吝啬您的花花

Spring 文章被收录于专栏

本专栏聚焦Spring全生态体系,从IoC/AOP核心原理入手,覆盖Spring Boot自动配置、事务管理、Web开发等实战内容。拆解循环依赖、动态代理等高频面试难点,助力开发者从入门到精通,打通单体到微服务的技术链路,解决企业级开发痛点,提升架构设计与问题排查能力,成为Java后端进阶的必备技术专栏。

全部评论
感谢分享
点赞 回复 分享
发布于 昨天 12:01 上海

相关推荐

评论
1
1
分享

创作者周榜

更多
正在热议
更多
# 春招至今,你的战绩如何? #
10009次浏览 92人参与
# 你的实习产出是真实的还是包装的? #
1806次浏览 41人参与
# 巨人网络春招 #
11308次浏览 223人参与
# 军工所铁饭碗 vs 互联网高薪资,你会选谁 #
7499次浏览 43人参与
# 简历第一个项目做什么 #
31600次浏览 332人参与
# 重来一次,我还会选择这个专业吗 #
433408次浏览 3926人参与
# 米连集团26产品管培生项目 #
5823次浏览 214人参与
# 当下环境,你会继续卷互联网,还是看其他行业机会 #
187032次浏览 1122人参与
# 牛客AI文生图 #
21414次浏览 238人参与
# 不考虑薪资和职业,你最想做什么工作呢? #
152312次浏览 887人参与
# 研究所笔面经互助 #
118885次浏览 577人参与
# 简历中的项目经历要怎么写? #
310134次浏览 4199人参与
# AI时代,哪些岗位最容易被淘汰 #
63508次浏览 807人参与
# 面试紧张时你会有什么表现? #
30492次浏览 188人参与
# 你今年的平均薪资是多少? #
213040次浏览 1039人参与
# 你怎么看待AI面试 #
179929次浏览 1238人参与
# 高学历就一定能找到好工作吗? #
64317次浏览 620人参与
# 你最满意的offer薪资是哪家公司? #
76454次浏览 374人参与
# 我的求职精神状态 #
448010次浏览 3129人参与
# 正在春招的你,也参与了去年秋招吗? #
363300次浏览 2637人参与
# 腾讯音乐求职进展汇总 #
160604次浏览 1111人参与
# 校招笔试 #
470629次浏览 2964人参与
牛客网
牛客网在线编程
牛客网题解
牛客企业服务