一、问题场景
你是不是也遇到过这种情况?Controller 里抛了个异常,前端收到的却是一坨看不懂的 JSON:
{
"timestamp": "2026-09-02T06:58:27.917+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/api/xxxxx"
}
调用方看到 500 根本不知道发生了什么。能不能让错误信息清晰、统一、结构化地返回?
答案很简单:一个全局异常处理器就够了。但要真正好用,配合另外两个小伙伴会更完整——总共 3 个文件,复制粘贴到新项目里就能用。
二、整体架构图
Controller 执行业务
│
├── 正常 → R.ok().result(data) → 返回 200 JSON
│
└── 抛异常 → GlobalExceptionHandler 拦截 → R.fail(code, msg) → 返回错误 JSON
│
├── ApiException → 业务异常(code=-1 或自定义)
├── IllegalArgumentException → 参数错误(code=400)
└── Exception → 兜底(code=500)
三个文件各司其职:
| 文件 | 职责 | 可选 |
|---|---|---|
GlobalExceptionHandler | 拦截异常、转成统一 JSON | ❌ 必须 |
R<T> | 统一响应封装 | ⚠️ 建议有 |
ApiException | 业务自定义异常 | ⚠️ 建议有 |
三、三个文件源码
1️⃣ 全局异常处理器(核心,不可少)
package com.example.common;
import com.example.vo.R;
import lombok.extern.log4j.Log4j2;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理器
* 作用: 把各种异常转成结构化 R 响应, 避免 Spring Boot 默认 500 白屏
*/
@Log4j2
@RestControllerAdvice // ① 这个注解是关键!拦截所有 @RestController 抛的异常
public class GlobalExceptionHandler {
/**
* 业务自定义异常: 直接返回异常里携带的 code 和 msg
* 使用方式: Controller / Service 里 throw new ApiException("xxxx不存在")
*/
@ExceptionHandler(ApiException.class) // ② 指定处理哪种异常
public R handleApiException(ApiException e) {
log.warn("业务异常: code={}, msg={}", e.getCode(), e.getMessage());
return R.fail(e.getCode(), e.getMessage());
}
/**
* 参数校验异常
*/
@ExceptionHandler(IllegalArgumentException.class)
public R handleIllegalArg(IllegalArgumentException e) {
log.warn("参数错误: {}", e.getMessage());
return R.fail(400, e.getMessage());
}
/**
* 兜底: 所有没被上面捕获的异常
* ❗ 一定要放在最后, 不然更具体的异常会被它吞掉
*/
@ExceptionHandler(Exception.class)
public R handleException(Exception e) {
log.error("系统异常: ", e);
return R.fail(500, "服务器内部错误: " + e.getMessage());
}
}
两个核心注解必须记住:
| 注解 | 作用 |
|---|---|
@RestControllerAdvice | 拦截所有 @RestController 抛出的异常,自动走这个类 |
@ExceptionHandler(XXException.class) | 声明这个方法处理哪种异常,可以写多个 |
2️⃣ 统一响应封装
package com.example.vo;
import lombok.Data;
/**
* 统一响应结构: 无论成功还是失败, 前端都收到这个 JSON 格式
* {
* "success": true/false,
* "code": 200 / 400 / -1,
* "message": "success" / "xxxx不存在",
* "result": { ... } // 只有成功才有
* }
*/
@Data
public class R<T> {
private Boolean success;
private Integer code;
private String message;
private T result;
private R() {} // 构造器私有, 强制用静态工厂
// ===== 成功响应 =====
public static <T> R<T> ok() {
R<T> r = new R<>();
r.setSuccess(true);
r.setCode(200);
r.setMessage("success");
return r;
}
public static <T> R<T> ok(T data) {
R<T> r = ok();
r.setResult(data);
return r;
}
// ===== 失败响应(核心!GlobalExceptionHandler 用的就是这个)=====
public static <T> R<T> fail(Integer code, String msg) {
R<T> r = new R<>();
r.setSuccess(false);
r.setCode(code);
r.setMessage(msg);
return r;
}
public static <T> R<T> fail(String msg) {
return fail(-1, msg); // 默认业务异常 code=-1
}
}
3️⃣ 业务自定义异常(可选,让错误更语义化)
package com.example.common;
import lombok.Getter;
/**
* 业务异常: 在 Service / Controller 里 throw new ApiException ("xxx不存在")
* GlobalExceptionHandler 会捕获它, 返回清晰的错误 JSON
*/
@Getter
public class ApiException extends RuntimeException {
private final Integer code;
public ApiException (String msg) {
super(msg);
this.code = -1;
}
public ApiException (Integer code, String msg) {
super(msg);
this.code = code;
}
public ApiException (Integer code, String msg, Throwable cause) {
super(msg, cause);
this.code = code;
}
}
四、效果对比
之前:Spring Boot 默认 500 白屏
{
"timestamp": "2026-09-02T06:58:27.917+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/api/xxxxxx"
}
之后:结构化清晰错误信息
业务异常(主动抛出):
{
"success": false,
"code": -1,
"message": "xxxx不存在: xxxxx",
"result": null
}
参数异常:
{
"success": false,
"code": 400,
"message": "pageNum must be positive",
"result": null
}
系统异常(兜底):
{
"success": false,
"code": 500,
"message": "服务器内部错误: Connection timeout",
"result": null
}
五、使用示例
Controller 里正常返回
@PostMapping("/getxxxx")
public R getxxxx(@RequestBody QueryReq req) {
// ✅ 正常返回
PO p= service.getByCode(req.getxxxxx());
return R.ok(p);
}
抛出业务异常
@PostMapping("/getxxxx")
public R getxxxx(@RequestBody QueryReq req) {
Po p= service.getByCode(req.getxxxxx());
if (p== null) {
// ✅ 主动抛出, GlobalExceptionHandler 自动转成 R.fail(-1, "xxxxx不存在")
throw new ApiException("xxxx不存在: " + req.xxxxx());
}
return R.ok(p);
}
抛出参数异常
public void query(int pageNum) {
if (pageNum <= 0) {
// ✅ IllegalArgumentException → code=400
throw new IllegalArgumentException("pageNum must be positive");
}
}
六、新项目接入 Checklist
✅ 复制 GlobalExceptionHandler.java 到项目的 common 包
✅ 复制 R.java 到项目的 vo 包(或改造你已有的响应类)
✅ 复制 ApiException.java 到项目的 common 包(可选)
✅ 在 pom.xml 确认有 spring-boot-starter-web(一般都有)
✅ 启动项目, 随便访问一个不存在的接口 → 应该返回 R.fail JSON 而不是白屏
注意: @RestControllerAdvice 只拦截 Controller 层抛出来的异常。如果用 AOP / Filter / 定时任务里的异常,那是另一个话题了,本文聚焦 HTTP 接口。
七、小结
| 方案 | 复杂度 | 效果 |
|---|---|---|
| ❌ 不处理 | 0 | 调用方只看到 500,不知道原因 |
| ❌ Controller 里 try-catch | 高,重复代码多 | 每个接口都要写 try-catch,难看 |
| ✅ GlobalExceptionHandler | 3 个文件,一次搞定 | 统一、清晰、可扩展 |
一句话总结: @RestControllerAdvice + @ExceptionHandler + 统一响应类 = 告别 500 白屏。

743

被折叠的 条评论
为什么被折叠?



