Spring Boot 全局异常处理最简方案:3 个文件干掉 500 白屏

一、问题场景

你是不是也遇到过这种情况?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,难看
✅ GlobalExceptionHandler3 个文件,一次搞定统一、清晰、可扩展

一句话总结: @RestControllerAdvice + @ExceptionHandler + 统一响应类 = 告别 500 白屏。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值