发布于2026-07-08 阅读(0)
扫一扫,手机访问
后端接口报错,要是直接让异常往外抛,前端收到的要么是一大段 Tomcat 默认的 HTML 错误页,要么是乱七八糟的堆栈信息。前端同学一看就懵了——这到底是啥错?该提示用户什么?

所以得有一层兜底:不管哪层出了什么岔子,返回给前端的东西始终是统一格式的 JSON——有错误码、有错误信息,前端拿到手就能稳稳地做判断和处理。
参数校验也得一起安排上。Controller 里别写一堆 if (xxx == null),用注解声明规则,校验不通过直接抛异常,再由全局处理器统一拦截、统一返回。代码干净,逻辑也清晰。
@ControllerAdvice 和 @ExceptionHandler 这些都在 spring-boot-starter-web 里,如果你项目已经用了 Web 模块,这部分就不用重复加了。
但参数校验的依赖需要单独拎出来:
org.springframework.boot spring-boot-starter-validation
注意一下版本差异:Spring Boot 2.x 时代,validation 是内嵌在 spring-boot-starter-web 里的,到了 3.x 就被拆出来了。如果不加这个依赖,@NotBlank、@Valid 这些注解不会报编译错误,但运行时校验根本不生效——加了个寂寞,所有参数直接放行。
先搭两个基础的东西:异常接口 + 异常类。这两个放在 common 模块,全项目都能复用。
public interface BaseExceptionInterface {
String getErrorCode();
String getErrorMessage();
}
一个很简单的接口,它就规定一件事:异常必须包含错误码和错误信息。后面我们的枚举实现它,BizException 也依赖它,所有东西都围绕这个约定来走。
BizException 代表业务异常,每个模块都可能出现,所以定义在 common 里最合适。
@Getter
@Setter
public class BizException extends RuntimeException {
private String errorCode;
private String errorMessage;
public BizException(BaseExceptionInterface baseExceptionInterface) {
this.errorCode = baseExceptionInterface.getErrorCode();
this.errorMessage = baseExceptionInterface.getErrorMessage();
}
}
继承 RuntimeException,有两个好处:抛的时候不用在方法签名上加 throws,Spring 对运行时异常默认也会做事务回滚。构造参数只接收 BaseExceptionInterface,不让你随便写 new BizException("随便写的错")——所有异常必须提前在枚举里定义好,方便统一管理。
接下来定义你这个模块可能有哪些异常。
@Getter
@AllArgsConstructor
public enum ResponseCodeEnum implements BaseExceptionInterface {
// 通用异常
SYSTEM_ERROR("AUTH-10000", "出错啦,后台小哥正在努力修复中..."),
PARAM_NOT_VALID("AUTH-10001", "参数错误"),
// 业务异常往后加
;
private final String errorCode;
private final String errorMessage;
}
这里有个小巧妙:BaseExceptionInterface 定义的 getErrorCode 和 getErrorMessage 两个方法,枚举里定义了两个变量,加上 @Getter,刚好就把接口方法给实现了。巧不巧?
| 要点 | 说明 |
|---|---|
实现 BaseExceptionInterface | 枚举实例可以直接丢进 BizException 的构造参数 |
| 错误码前缀 | AUTH 代表这个模块,不同模块用不同前缀,比如订单模块用 ORDER-20000,一眼就能看出问题出在哪 |
| 后面加什么 | 登录失败、用户不存在、权限不足……随着业务迭代,直接在枚举里加新的即可 |
抛一个业务异常就一句话:
throw new BizException(ResponseCodeEnum.SYSTEM_ERROR);
BizException 的构造参数是 BaseExceptionInterface(接口),不是 ResponseCodeEnum(具体枚举)。所以层级关系是这样的:
BizException 构造参数
↓ 依赖
BaseExceptionInterface(接口:规定必须有 errorCode + errorMessage)
↑ 实现
ResponseCodeEnum(枚举:集中管理所有错误码)
BizExceptionAuthResponseCodeEnum、order 模块的 OrderResponseCodeEnum,只要实现了接口,都能用如果直接用 new BizException(String, String),错误码就会散落在项目各个角落,改一个提示语得全项目搜索,想看看系统里有哪些错误码更是无从下手。
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/** 业务异常 */
@ExceptionHandler({ BizException.class })
@ResponseBody
public Response
| 注解 | 干什么的 |
|---|---|
@ControllerAdvice | 全局增强,所有 Controller 的异常都会被这里的 @ExceptionHandler 拦截 |
@ExceptionHandler | 声明这个方法专门处理哪种类型的异常 |
@ResponseBody | 返回的内容直接序列化为 JSON,不走视图模板 |
Exception(其他所有异常) ← 第三层:保底,全拦住了 └─ MethodArgumentNotValidException ← 第二层:参数校验 └─ BizException ← 第一层:业务异常
Spring 的匹配规则是找最精确的。如果抛的是 BizException,只走第一个 handler;抛的是 MethodArgumentNotValidException,走第二个;其他任何没被前两个匹配到的异常,都掉进第三个兜底。三层覆盖,不会有漏网之鱼。
注意看日志级别:
BizException用的log.warn,因为它是业务"可预期"的错,不需要大惊小怪。最后那个Exception用log.error并打印了堆栈,因为这是真正该修 bug 的异常。
上面那个 MethodArgumentNotValidException 是什么时候抛的?当加了 @Valid 的入参校验不通过时。
@Data
public class User {
@NotBlank(message = "昵称不能为空")
private String nickName;
private LocalDateTime createTime;
}
@PostMapping("/test2")
@ApiOperationLog(description = "测试接口2")
public Response test2(@Valid @RequestBody User user) {
return Response.success(user);
}
注意 @Valid 是加在方法参数上的,不是加在类上。两个动作缺一不可:
@NotBlank → 不会校验,什么都能通过@Valid → 不会触发校验,注解成了摆设GlobalExceptionHandler 里的 handleMethodArgumentNotValidException 遍历了 bindingResult.getFieldErrors(),把每条错误的字段名、校验描述、实际收到的值拼接在一起:
sb: "nickName 昵称不能为空, 当前值: ''; "
这样前端一看到就知道是哪个字段出问题、为什么不通过、实际传了什么值,排查效率高不少。
| 注解 | 校验规则 |
|---|---|
@NotBlank | 不能为 null 且不能是空字符串(""不行," "也不行) |
@NotNull | 不能为 null(但 "" 可以通过) |
@NotEmpty | 不能为 null 且不能是空集合或空字符串 |
@Email | 必须是邮箱格式 |
@Size(min, max) | 字符串或集合的长度范围 |
@Min / @Max | 数字的最小值/最大值 |
@Pattern(regexp) | 自定义正则校验 |
选哪个要看业务场景。比如昵称不允许空字符串,用 @NotBlank 而不是 @NotNull,因为 @NotNull 允许 "" 通过,这个差别很关键。
以一个参数校验失败的请求为例,整个调用链路是这样的:
/test2,body 为 { "nickName": "" }User 对象@Valid 触发校验 → @NotBlank 校验失败MethodArgumentNotValidException@ControllerAdvice 中的 handleMethodArgumentNotValidException 将其拦截"nickName 昵称不能为空, 当前值: ''; "Response.fail("AUTH-10001", "...") → 最终输出为 JSON前端从头到尾拿到的都是统一结构,一个 if (response.success) 就能判断是否正常,处理起来非常清爽。
想手动中断一个请求、返回错误信息给前端,直接这样写:
// 不满足条件,直接抛
if (user == null) {
throw new BizException(ResponseCodeEnum.SYSTEM_ERROR);
}
在 ResponseCodeEnum 里加新的异常码:
// 通用异常
SYSTEM_ERROR("AUTH-10000", "出错啦,后台小哥正在努力修复中..."),
PARAM_NOT_VALID("AUTH-10001", "参数错误"),
// 业务异常
LOGIN_FAILED("AUTH-20001", "用户名或密码错误"),
USER_NOT_FOUND("AUTH-20002", "用户不存在"),
UNAUTHORIZED("AUTH-20003", "没有访问权限"),
;
然后在代码里直接抛:
throw new BizException(ResponseCodeEnum.LOGIN_FAILED);
所有异常码统一管理、统一抛出、统一处理,整个异常体系就非常规整了。
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8