在没有统一异常处理时,项目通常会面临以下痛点:
- 代码冗余 :每个 Controller 方法都需要
try-catch,导致业务逻辑与异常处理代码混杂。 - 格式混乱:不同的异常会返回不同的错误信息,前端难以统一解析和展示。
- 安全隐患:系统内部错误(如堆栈信息)可能直接暴露给用户,造成安全风险。
全局异常处理就像一个"中央客服中心",所有接口抛出的异常都会被它统一接收、分类处理,并返回标准化的友好提示。
️ 核心原理
Spring 通过两个核心注解实现全局异常处理:
@RestControllerAdvice:这是一个组合注解,相当于@ControllerAdvice+@ResponseBody。它标识一个类为全局异常处理器,可以拦截所有 Controller 抛出的异常,并确保返回结果是 JSON 格式。@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
}
通过这种方式,你的业务代码变得非常干净,而错误处理则完全自动化和标准化。