告别 try-catch 地狱:Spring Boot 全局异常处理 (GlobalExceptionHandler) 最佳实践

一、 为什么要用 GlobalExceptionHandler?(作用)

在传统的 Web 开发中,我们经常会在 Controller 层看到大量的重复代码:

java 复制代码
// ❌ 糟糕的代码示范
@GetMapping("/user")
public Result getUser() {
    try {
        userService.doSomething();
        return Result.success();
    } catch (BusinessException e) {
        return Result.error(e.getCode(), e.getMessage());
    } catch (Exception e) {
        log.error("未知错误", e);
        return Result.error(500, "系统内部错误");
    }
}

这种写法有 三个痛点

  1. 代码冗余 :每个接口都要写一遍 try-catch
  2. 格式不统一 :有的返回 {"code": 500},有的返回 {"status": "error"},前端处理起来非常痛苦。
  3. 事务失效风险 :如果在 catch 块中吞掉了异常且没有手动回滚,数据库事务可能提交脏数据。

GlobalExceptionHandler 的核心作用:

  1. 统一响应格式 :无论发生什么错误,最终返回给前端的 JSON 结构永远是固定的(如 CommonResult)。
  2. 代码解耦:业务逻辑(Service/Controller)只需关注"成功"的情况,失败直接抛出异常,由全局处理器兜底。
  3. 保障事务 :让异常抛出 Service 层,触发 Spring 的 @Transactional 自动回滚,保证数据一致性。
  4. 集中日志管理:在一个地方统一打印异常堆栈,避免日志到处乱飞。

二、 核心注解与原理(用法)

要实现全局异常处理,主要依赖两个注解:

  1. @RestControllerAdvice
    • 它是 @ControllerAdvice@ResponseBody 的组合注解。
    • 含义:拦截项目中所有 Controller 的异常,并将处理结果直接以 JSON 格式返回。
  2. @ExceptionHandler(Exception.class)
    • 含义:标记在方法上,声明该方法负责处理哪一种类型的异常。

三、 实战代码示例(示例)

以下是一个标准的企业级异常处理器模板。

1. 定义统一返回对象

首先,我们需要一个标准的 JSON 包装类。

java 复制代码
@Data
public class CommonResult<T> {
    private Integer code;
    private String msg;
    private T data;

    public static CommonResult<?> error(Integer code, String msg) {
        CommonResult<?> result = new CommonResult<>();
        result.code = code;
        result.msg = msg;
        return result;
    }
}
2. 编写全局异常处理器
java 复制代码
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    // 忽略的错误信息(噪音过滤)
    public static final Set<String> IGNORE_ERROR_MESSAGES = Set.of("无效的刷新令牌");

    /**
     * 1. 处理自定义业务异常 (ServiceException)
     * 场景:库存不足、账号密码错误
     */
    @ExceptionHandler(ServiceException.class)
    public CommonResult<?> handleServiceException(ServiceException ex) {
        // 只有不在忽略名单里的错误,才打印日志,避免刷屏
        if (!IGNORE_ERROR_MESSAGES.contains(ex.getMessage())) {
            log.warn("[业务异常] {}", ex.getMessage());
        }
        return CommonResult.error(ex.getCode(), ex.getMessage());
    }

    /**
     * 2. 处理参数校验异常 (JSON 格式)
     * 场景:@RequestBody + @Valid 校验失败
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public CommonResult<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
        // 从异常中提取第一个具体的错误提示
        String errorMessage = ex.getBindingResult().getFieldError().getDefaultMessage();
        log.warn("[参数校验异常] {}", errorMessage);
        return CommonResult.error(400, String.format("请求参数不正确: %s", errorMessage));
    }

    /**
     * 3. 处理参数校验异常 (表单/URL 参数格式)
     * 场景:GET 请求 @Valid 校验失败
     */
    @ExceptionHandler(BindException.class)
    public CommonResult<?> handleBindException(BindException ex) {
        String errorMessage = ex.getBindingResult().getFieldError().getDefaultMessage();
        log.warn("[参数绑定异常] {}", errorMessage);
        return CommonResult.error(400, String.format("请求参数不正确: %s", errorMessage));
    }
    
    /**
     * 4. 处理必填参数缺失
     * 场景:@RequestParam 没传
     */
    @ExceptionHandler(MissingServletRequestParameterException.class)
    public CommonResult<?> handleMissingServletRequestParameterException(MissingServletRequestParameterException ex) {
        return CommonResult.error(400, String.format("请求参数缺失: %s", ex.getParameterName()));
    }

    /**
     * 5. 兜底异常处理 (Exception)
     * 场景:空指针、数据库连接超时、代码 Bug
     */
    @ExceptionHandler(Exception.class)
    public CommonResult<?> handleGlobalException(Exception ex) {
        // 这里必须打印 ERROR 级别的堆栈日志,方便排查 Bug
        log.error("[系统异常]", ex);
        return CommonResult.error(500, "系统内部错误,请联系管理员");
    }
}

四、 进阶:如何处理 Filter 中的异常?

这是面试和实战中的高频考点。

问题@RestControllerAdvice 只能捕获 Controller 层 之后的异常。如果 Filter(如 Token 认证过滤器)抛出异常,它是抓不到的。

解决方案

yudao 架构中,通常会提供一个 "手动分发器" 方法。Filter 捕获异常后,手动调用这个方法。

代码片段:

java 复制代码
// 在 GlobalExceptionHandler 中添加
public CommonResult<?> allExceptionHandler(HttpServletRequest request, Throwable ex) {
    if (ex instanceof ServiceException) {
        return handleServiceException((ServiceException) ex);
    }
    // ... 手动判断其他类型
    return handleGlobalException((Exception) ex);
}

// 在 Filter 中调用
try {
    chain.doFilter(request, response);
} catch (Exception e) {
    // 手动转交给全局异常处理器
    CommonResult<?> result = globalExceptionHandler.allExceptionHandler(request, e);
    ServletUtils.writeJSON(response, result);
}

五、 总结

GlobalExceptionHandler 是 Spring Boot 工程化开发的基石。

它像一个污水净化器

  1. 输入:各种乱七八糟的异常(ServiceException, NullPointerException, ValidationException)。
  2. 处理:记录日志、提取关键信息、过滤噪音。
  3. 输出:标准、干净、前端友好的 JSON 数据。

通过它,我们实现了后端代码的快速失败,Service 层只需关注业务逻辑,遇到问题直接抛异常,剩下的全交给它处理。

相关推荐
神奇的程序员8 小时前
从已损坏的备份中拯救数据
运维·后端·前端工程化
Goldn.8 小时前
Java核心技术栈全景解析:从Web开发到AI融合
java· spring boot· 微服务· ai· jvm· maven· hibernate
oden8 小时前
AI服务商切换太麻烦?一个AI Gateway搞定监控、缓存和故障转移(成本降40%)
后端·openai·api
ะัี潪ิื8 小时前
springboot加载本地application.yml和加载Consul中的application.yml配置反序列化LocalDate类型差异
spring boot·consul·java-consul
李慕婉学姐9 小时前
【开题答辩过程】以《基于Android的出租车运行监测系统设计与实现》为例,不知道这个选题怎么做的,不知道这个选题怎么开题答辩的可以进来看看
java·后端·vue
m0_740043739 小时前
SpringBoot05-配置文件-热加载/日志框架slf4j/接口文档工具Swagger/Knife4j
java·spring boot·后端·log4j
编织幻境的妖9 小时前
SQL查询连续登录用户方法详解
java·数据库·sql
未若君雅裁10 小时前
JVM面试篇总结
java·jvm·面试
kk哥889910 小时前
C++ 对象 核心介绍
java·jvm·c++