Spring中统一异常处理详细教程

在没有统一异常处理时,项目通常会面临以下痛点:

  • 代码冗余 :每个 Controller 方法都需要 try-catch,导致业务逻辑与异常处理代码混杂。
  • 格式混乱:不同的异常会返回不同的错误信息,前端难以统一解析和展示。
  • 安全隐患:系统内部错误(如堆栈信息)可能直接暴露给用户,造成安全风险。

全局异常处理就像一个"中央客服中心",所有接口抛出的异常都会被它统一接收、分类处理,并返回标准化的友好提示。


️ 核心原理

Spring 通过两个核心注解实现全局异常处理:

  1. @RestControllerAdvice:这是一个组合注解,相当于 @ControllerAdvice + @ResponseBody。它标识一个类为全局异常处理器,可以拦截所有 Controller 抛出的异常,并确保返回结果是 JSON 格式。
  2. @ExceptionHandler:用在方法上,指定该方法专门处理某一种或某几种类型的异常。

从零开始实现

第一步:定义错误码枚举

这是整个异常体系的"字典"。建议根据业务模块划分错误码区间(例如用户模块 40001-49999,订单模块 50001-59999)。

java 复制代码
import lombok.Getter;

/**
 * 业务错误码枚举
 * 统一管理所有业务异常码和提示信息
 */
@Getter
public enum BizErrorCode {

    // --- 通用错误 (20000-29999) ---
    SUCCESS(20000, "操作成功"),
    PARAM_ERROR(20001, "参数错误"),

    // --- 用户相关错误 (40000-49999) ---
    USER_NOT_FOUND(40001, "用户不存在"),
    USER_LOGIN_ERROR(40002, "账号或密码错误"),
    USER_TOKEN_EXPIRED(40003, "登录已过期"),

    // --- 系统相关错误 (50000-59999) ---
    SYSTEM_ERROR(50000, "系统繁忙,请稍后再试");

    private final Integer code;
    private final String message;

    BizErrorCode(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
}
第二步:定义统一的返回结果

我们需要一个标准的响应格式,让前端知道如何解析成功和失败的响应。

java 复制代码
/**
 * 全局统一返回结果封装
 */
public class Result<T> {
    private Integer code;   // 状态码
    private String msg;     // 提示信息
    private T data;         // 返回数据

    // 无参构造
    public Result() {
    }

    // 全参构造
    public Result(Integer code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

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

    public static <T> Result<T> success(String msg, T data) {
        Result<T> r = new Result<>();
        r.setCode(200);
        r.setMsg(msg);
        r.setData(data);
        return r;
    }

    // --- 快速构建失败响应 ---
    public static <T> Result<T> error(Integer code, String msg) {
        Result<T> r = new Result<>();
        r.setCode(code);
        r.setMsg(msg);
        r.setData(null);
        return r;
    }

    public static <T> Result<T> error(String msg) {
        return error(500, msg);
    }

    // --- Getter & Setter ---
    public Integer getCode() { return code; }
    public void setCode(Integer code) { this.code = code; }
    public String getMsg() { return msg; }
    public void setMsg(String msg) { this.msg = msg; }
    public T getData() { return data; }
    public void setData(T data) { this.data = data; }
}
第三步:创建自定义业务异常

为了区分不同类型的业务错误(如用户不存在、参数错误等),我们定义一个自定义的运行时异常。继承 RuntimeException 可以避免在代码中强制进行 try-catch

java 复制代码
/**
 * 自定义业务异常
 */
public class BizException extends RuntimeException {
    private final Integer code;

    /**
     * 核心构造方法:传入错误码枚举
     */
    public BizException(BizErrorCode errorCode) {
        super(errorCode.getMessage()); // 将错误信息传递给父类
        this.code = errorCode.getCode();
    }
    
    // 也可以保留一个自定义消息的构造方法,用于特殊情况
    public BizException(Integer code, String msg) {
        super(msg);
        this.code = code;
    }

    public Integer getCode() {
        return code;
    }
}
第四步:实现全局异常处理器

这是整个机制的核心,负责捕获所有异常。我们创建一个类,使用 @RestControllerAdvice 注解,并在其中定义多个方法来处理不同类型的异常。

java 复制代码
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

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

    /**
     * 1. 捕获自定义的业务异常
     */
    @ExceptionHandler(BizException.class)
    public Result<?> handleBizException(BizException e) {
        // 业务异常,返回对应的错误码和信息
        return Result.error(e.getCode(), e.getMessage());
    }

    /**
     * 2. 捕获参数校验异常(如使用 @Valid 注解时)
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<?> handleValidException(MethodArgumentNotValidException e) {
        // 获取第一个校验失败的错误信息
        String errorMsg = e.getBindingResult()
                           .getFieldErrors()
                           .stream()
                           .map(error -> error.getDefaultMessage())
                           .findFirst()
                           .orElse("参数校验失败");
        return Result.error(400, errorMsg);
    }

    /**
     * 3. 兜底:捕获所有未被处理的未知异常
     */
    @ExceptionHandler(Exception.class)
    public Result<?> handleAllException(Exception e) {
        // 记录异常堆栈,方便后台排查问题
        e.printStackTrace();
        // 对前端隐藏具体的错误信息,只返回友好的提示
        return Result.error(500, "服务器繁忙,请稍后重试");
    }
}
第五步:在业务代码中使用

完成以上配置后,在业务层(Service)或控制器(Controller)中,你可以非常简洁地抛出异常,不需任何 try-catch,不需要关心怎么返回 JSON,只需要在逻辑不对时,抛出对应的枚举。

java 复制代码
@Service
public class UserService {

    public User getUserById(Long id) {
        // 模拟数据库查询
        User user = null; 
        
        if (user == null) {
            // 【核心用法】直接抛出携带枚举的异常
            // 不需要写 "用户不存在" 字符串,也不需要写 40001 数字
            throw new BizException(BizErrorCode.USER_NOT_FOUND);
        }
        
        // 其他业务逻辑...
        if (id < 0) {
             throw new BizException(BizErrorCode.PARAM_ERROR);
        }
        
        return user;
    }
}

getUserById 方法抛出 BizException 时,GlobalExceptionHandler 会自动拦截,并向前端返回一个标准的 JSON 响应:

json 复制代码
{
  "code": 4001,
  "msg": "用户不存在",
  "data": null
}

通过这种方式,你的业务代码变得非常干净,而错误处理则完全自动化和标准化。

相关推荐
one_love_zfl2 小时前
java面试-spring篇
java·spring·面试
炘爚2 小时前
深入解析C++多态:虚函数与动态联编
开发语言·c++·多态·虚函数
shjita2 小时前
maven涉及的配置
java·前端·maven
Gauss松鼠会2 小时前
GaussDB(DWS)数据融合:云端GaussDB(DWS)迁移
java·服务器·网络·数据库·性能优化·gaussdb
abcefg_h2 小时前
GORM——基础介绍与CRUD
开发语言·后端·golang
金融小白数据分析之路2 小时前
java 打包exe maven 版本
java·开发语言·maven
兩尛2 小时前
C++面向对象和类相关
java·c++·面试
changshuaihua0012 小时前
useState 状态管理
开发语言·前端·javascript·react.js