如何自研一个全局异常处理器

如何自研一个全局异常处理器

------一个 Java 实习生的异常处理实战笔记

一、开场:从"我的第一个 PR 被驳回"说起

我刚入职那会儿,自信满满地提了第一个 PR------一个用户注册接口。

我写得很"用心":每个方法都 try/catch,每个错误都返回 Map,每个提示都写中文。

第二天 mentor review 完只说了一句话:

"你这个 Controller 全是 try/catch,明天来我办公室,我们聊一下'全局异常处理器'。"

我当时的表情大概是:

  • 全局异常处理器?是 Spring 的什么东西?
  • 不用 try/catch,那异常怎么办?让程序崩溃?
  • 我这不是"防御性编程"吗?有什么问题?

后来我才知道,满屏 try/catch 不是"防御性编程",是"代码的味道不对了"

今天这篇博客,我就把"一个实习生是怎么一步步把手撕 try/catch 改造成全局异常处理器的"全过程复盘一遍。

一句话核心观点全局异常处理器不是"装酷"的设计模式,它是"把错误处理从业务代码里抽出去"的工程化方案。


二、概念扫盲:异常处理到底要解决什么?

2.1 三个真实场景的痛点

在我学全局异常处理之前,写过一段时间"防御性代码",踩了不少坑:

痛点 实际场景
💥 返回体不统一 有的接口返回 {code: 200, data: ...},有的返回 {"success": true, "result": ...},前端同学拿着文档哭了
💀 异常信息暴露 数据库异常直接把 SQL 抛到前端:You have an error in your SQL syntax... 安全风险
😵 业务错误无章法 有的抛 RuntimeException("用户不存在"),有的抛 new Exception("xxx"),前端拿到 500 不知道怎么处理

2.2 引入"统一响应体"概念

标准响应体长这样

json 复制代码
{
  "code": 200,
  "message": "操作成功",
  "data": { "id": 1, "name": "张三" }
}

错误时:

json 复制代码
{
  "code": 5001,
  "message": "用户不存在",
  "data": null
}
  • code:业务码(200=成功、5001=业务错误、6000=系统错误)
  • message:人类可读的提示
  • data:业务数据

2.3 引入"统一异常处理"概念

一句话理解
业务方法只管抛,ControllerAdvice 负责接,接完包成 Result 还回去。
#mermaid-svg-tJWcem0VUWIyKhHS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-tJWcem0VUWIyKhHS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-tJWcem0VUWIyKhHS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-tJWcem0VUWIyKhHS .error-icon{fill:#552222;}#mermaid-svg-tJWcem0VUWIyKhHS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-tJWcem0VUWIyKhHS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-tJWcem0VUWIyKhHS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-tJWcem0VUWIyKhHS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-tJWcem0VUWIyKhHS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-tJWcem0VUWIyKhHS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-tJWcem0VUWIyKhHS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-tJWcem0VUWIyKhHS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-tJWcem0VUWIyKhHS .marker.cross{stroke:#333333;}#mermaid-svg-tJWcem0VUWIyKhHS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-tJWcem0VUWIyKhHS p{margin:0;}#mermaid-svg-tJWcem0VUWIyKhHS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-tJWcem0VUWIyKhHS .cluster-label text{fill:#333;}#mermaid-svg-tJWcem0VUWIyKhHS .cluster-label span{color:#333;}#mermaid-svg-tJWcem0VUWIyKhHS .cluster-label span p{background-color:transparent;}#mermaid-svg-tJWcem0VUWIyKhHS .label text,#mermaid-svg-tJWcem0VUWIyKhHS span{fill:#333;color:#333;}#mermaid-svg-tJWcem0VUWIyKhHS .node rect,#mermaid-svg-tJWcem0VUWIyKhHS .node circle,#mermaid-svg-tJWcem0VUWIyKhHS .node ellipse,#mermaid-svg-tJWcem0VUWIyKhHS .node polygon,#mermaid-svg-tJWcem0VUWIyKhHS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-tJWcem0VUWIyKhHS .rough-node .label text,#mermaid-svg-tJWcem0VUWIyKhHS .node .label text,#mermaid-svg-tJWcem0VUWIyKhHS .image-shape .label,#mermaid-svg-tJWcem0VUWIyKhHS .icon-shape .label{text-anchor:middle;}#mermaid-svg-tJWcem0VUWIyKhHS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-tJWcem0VUWIyKhHS .rough-node .label,#mermaid-svg-tJWcem0VUWIyKhHS .node .label,#mermaid-svg-tJWcem0VUWIyKhHS .image-shape .label,#mermaid-svg-tJWcem0VUWIyKhHS .icon-shape .label{text-align:center;}#mermaid-svg-tJWcem0VUWIyKhHS .node.clickable{cursor:pointer;}#mermaid-svg-tJWcem0VUWIyKhHS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-tJWcem0VUWIyKhHS .arrowheadPath{fill:#333333;}#mermaid-svg-tJWcem0VUWIyKhHS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-tJWcem0VUWIyKhHS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-tJWcem0VUWIyKhHS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tJWcem0VUWIyKhHS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-tJWcem0VUWIyKhHS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tJWcem0VUWIyKhHS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-tJWcem0VUWIyKhHS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-tJWcem0VUWIyKhHS .cluster text{fill:#333;}#mermaid-svg-tJWcem0VUWIyKhHS .cluster span{color:#333;}#mermaid-svg-tJWcem0VUWIyKhHS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-tJWcem0VUWIyKhHS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-tJWcem0VUWIyKhHS rect.text{fill:none;stroke-width:0;}#mermaid-svg-tJWcem0VUWIyKhHS .icon-shape,#mermaid-svg-tJWcem0VUWIyKhHS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tJWcem0VUWIyKhHS .icon-shape p,#mermaid-svg-tJWcem0VUWIyKhHS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-tJWcem0VUWIyKhHS .icon-shape .label rect,#mermaid-svg-tJWcem0VUWIyKhHS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tJWcem0VUWIyKhHS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-tJWcem0VUWIyKhHS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-tJWcem0VUWIyKhHS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-tJWcem0VUWIyKhHS .biz>*{fill:#9013FE!important;stroke:#5C00B3!important;color:#fff!important;}#mermaid-svg-tJWcem0VUWIyKhHS .biz span{fill:#9013FE!important;stroke:#5C00B3!important;color:#fff!important;}#mermaid-svg-tJWcem0VUWIyKhHS .biz tspan{fill:#fff!important;}#mermaid-svg-tJWcem0VUWIyKhHS .mvc>*{fill:#4A90E2!important;stroke:#2E5C8A!important;color:#fff!important;}#mermaid-svg-tJWcem0VUWIyKhHS .mvc span{fill:#4A90E2!important;stroke:#2E5C8A!important;color:#fff!important;}#mermaid-svg-tJWcem0VUWIyKhHS .mvc tspan{fill:#fff!important;}#mermaid-svg-tJWcem0VUWIyKhHS .handler>*{fill:#F5A623!important;stroke:#C77A00!important;color:#fff!important;}#mermaid-svg-tJWcem0VUWIyKhHS .handler span{fill:#F5A623!important;stroke:#C77A00!important;color:#fff!important;}#mermaid-svg-tJWcem0VUWIyKhHS .handler tspan{fill:#fff!important;}#mermaid-svg-tJWcem0VUWIyKhHS .result>*{fill:#7ED321!important;stroke:#5C9C1A!important;color:#fff!important;}#mermaid-svg-tJWcem0VUWIyKhHS .result span{fill:#7ED321!important;stroke:#5C9C1A!important;color:#fff!important;}#mermaid-svg-tJWcem0VUWIyKhHS .result tspan{fill:#fff!important;} 业务方法

抛 BizException
🌐 Spring MVC

DispatcherServlet
🛡️ GlobalExceptionHandler

@ControllerAdvice
📦 统一响应体 Result
📤 返回前端


三、改造前:满屏 try/catch 的"原始代码"

先看一段我实习前写的"反例代码"------真实新手常写

java 复制代码
@PostMapping("/register")
public Map<String, Object> register(@RequestBody UserDTO dto) {
    Map<String, Object> result = new HashMap<>();
    try {
        // 1. 校验用户名
        if (userDao.existsByUsername(dto.getUsername())) {
            result.put("code", 5001);
            result.put("message", "用户名已存在");
            return result;
        }
        // 2. 保存
        try {
            userDao.save(dto);
        } catch (DataAccessException e) {
            result.put("code", 5000);
            result.put("message", "数据库异常");
            return result;
        }
        result.put("code", 200);
        result.put("message", "注册成功");
        result.put("data", dto);
        return result;
    } catch (Exception e) {
        result.put("code", 5000);
        result.put("message", "未知错误:" + e.getMessage());
        return result;
    }
}

3 个要命的问题

问题 后果
🐌 业务被淹没 真正干活的代码(userDao.save)只有 1 行
💥 重复样板 每个 Controller 都要写 try/catch + Map.put
🔗 返回体不统一 各种字段名(code / success / data / result)混乱

💡 导师的那句原话业务方法只做一件事------告诉调用方"成"或"不成"。 怎么告诉、告诉成什么样,交给框架处理。


四、改造中:手撕一个全局异常处理器

这一节是重头戏。我手把手带你从 0 手撕一个能跑的统一异常处理器。

4.1 整体改造方案:5 步走完

#mermaid-svg-JsQ1L4JsqgUVl64E{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JsQ1L4JsqgUVl64E .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JsQ1L4JsqgUVl64E .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JsQ1L4JsqgUVl64E .error-icon{fill:#552222;}#mermaid-svg-JsQ1L4JsqgUVl64E .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JsQ1L4JsqgUVl64E .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JsQ1L4JsqgUVl64E .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JsQ1L4JsqgUVl64E .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JsQ1L4JsqgUVl64E .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JsQ1L4JsqgUVl64E .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JsQ1L4JsqgUVl64E .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JsQ1L4JsqgUVl64E .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JsQ1L4JsqgUVl64E .marker.cross{stroke:#333333;}#mermaid-svg-JsQ1L4JsqgUVl64E svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JsQ1L4JsqgUVl64E p{margin:0;}#mermaid-svg-JsQ1L4JsqgUVl64E .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-JsQ1L4JsqgUVl64E .cluster-label text{fill:#333;}#mermaid-svg-JsQ1L4JsqgUVl64E .cluster-label span{color:#333;}#mermaid-svg-JsQ1L4JsqgUVl64E .cluster-label span p{background-color:transparent;}#mermaid-svg-JsQ1L4JsqgUVl64E .label text,#mermaid-svg-JsQ1L4JsqgUVl64E span{fill:#333;color:#333;}#mermaid-svg-JsQ1L4JsqgUVl64E .node rect,#mermaid-svg-JsQ1L4JsqgUVl64E .node circle,#mermaid-svg-JsQ1L4JsqgUVl64E .node ellipse,#mermaid-svg-JsQ1L4JsqgUVl64E .node polygon,#mermaid-svg-JsQ1L4JsqgUVl64E .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-JsQ1L4JsqgUVl64E .rough-node .label text,#mermaid-svg-JsQ1L4JsqgUVl64E .node .label text,#mermaid-svg-JsQ1L4JsqgUVl64E .image-shape .label,#mermaid-svg-JsQ1L4JsqgUVl64E .icon-shape .label{text-anchor:middle;}#mermaid-svg-JsQ1L4JsqgUVl64E .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-JsQ1L4JsqgUVl64E .rough-node .label,#mermaid-svg-JsQ1L4JsqgUVl64E .node .label,#mermaid-svg-JsQ1L4JsqgUVl64E .image-shape .label,#mermaid-svg-JsQ1L4JsqgUVl64E .icon-shape .label{text-align:center;}#mermaid-svg-JsQ1L4JsqgUVl64E .node.clickable{cursor:pointer;}#mermaid-svg-JsQ1L4JsqgUVl64E .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-JsQ1L4JsqgUVl64E .arrowheadPath{fill:#333333;}#mermaid-svg-JsQ1L4JsqgUVl64E .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-JsQ1L4JsqgUVl64E .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-JsQ1L4JsqgUVl64E .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JsQ1L4JsqgUVl64E .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-JsQ1L4JsqgUVl64E .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JsQ1L4JsqgUVl64E .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-JsQ1L4JsqgUVl64E .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-JsQ1L4JsqgUVl64E .cluster text{fill:#333;}#mermaid-svg-JsQ1L4JsqgUVl64E .cluster span{color:#333;}#mermaid-svg-JsQ1L4JsqgUVl64E div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-JsQ1L4JsqgUVl64E .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-JsQ1L4JsqgUVl64E rect.text{fill:none;stroke-width:0;}#mermaid-svg-JsQ1L4JsqgUVl64E .icon-shape,#mermaid-svg-JsQ1L4JsqgUVl64E .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JsQ1L4JsqgUVl64E .icon-shape p,#mermaid-svg-JsQ1L4JsqgUVl64E .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-JsQ1L4JsqgUVl64E .icon-shape .label rect,#mermaid-svg-JsQ1L4JsqgUVl64E .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JsQ1L4JsqgUVl64E .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-JsQ1L4JsqgUVl64E .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-JsQ1L4JsqgUVl64E :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-JsQ1L4JsqgUVl64E .step>*{fill:#FFF7E6!important;stroke:#F5A623!important;color:#333!important;}#mermaid-svg-JsQ1L4JsqgUVl64E .step span{fill:#FFF7E6!important;stroke:#F5A623!important;color:#333!important;}#mermaid-svg-JsQ1L4JsqgUVl64E .step tspan{fill:#333!important;}#mermaid-svg-JsQ1L4JsqgUVl64E .result>*{fill:#7ED321!important;stroke:#5C9C1A!important;color:#fff!important;}#mermaid-svg-JsQ1L4JsqgUVl64E .result span{fill:#7ED321!important;stroke:#5C9C1A!important;color:#fff!important;}#mermaid-svg-JsQ1L4JsqgUVl64E .result tspan{fill:#fff!important;} 第 1 步

📦 定义业务异常码枚举
第 2 步

📝 定义统一响应体 Result
第 3 步

⚠️ 定义业务异常类 BizException
第 4 步

🛡️ 写 GlobalExceptionHandler
第 5 步

🔄 Controller 用 Result.success() 返回
✅ 全局异常处理器跑通!

4.2 第 1 步:业务异常码枚举

java 复制代码
public enum ResultCode {
    // ✅ 通用成功
    SUCCESS(200, "操作成功"),

    // ⚠️ 客户端错误 4xx
    BAD_REQUEST(400, "请求参数错误"),
    UNAUTHORIZED(401, "未登录"),
    FORBIDDEN(403, "无权限"),
    NOT_FOUND(404, "资源不存在"),

    // 🔴 业务错误 5xxx
    USER_NOT_EXISTS(5001, "用户不存在"),
    USER_ALREADY_EXISTS(5002, "用户名已存在"),
    PASSWORD_INCORRECT(5003, "密码错误"),
    PHONE_INVALID(5004, "手机号格式错误"),

    // 💀 系统错误 6xxx
    SYSTEM_ERROR(6000, "系统异常,请稍后再试"),
    DATABASE_ERROR(6001, "数据库异常"),
    REMOTE_CALL_ERROR(6002, "远程调用失败");

    private final int code;
    private final String message;

    ResultCode(int code, String message) {
        this.code = code;
        this.message = message;
    }

    public int getCode() { return code; }
    public String getMessage() { return message; }
}

🎯 设计技巧 :用段位 区分错误类型------2xx 成功、4xx 客户端错、5xxx 业务错、6xxx 系统错。前两位数能直接告诉前端"这是哪类问题"

4.3 第 2 步:统一响应体 Result<T>

java 复制代码
@Data
public class Result<T> {
    private int code;
    private String message;
    private T data;

    // 构造器私有化,强制用静态工厂
    private Result(int code, String message, T data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }

    // 成功 - 无数据
    public static <T> Result<T> success() {
        return new Result<>(ResultCode.SUCCESS.getCode(),
                            ResultCode.SUCCESS.getMessage(),
                            null);
    }

    // 成功 - 带数据
    public static <T> Result<T> success(T data) {
        return new Result<>(ResultCode.SUCCESS.getCode(),
                            ResultCode.SUCCESS.getMessage(),
                            data);
    }

    // 失败 - 用异常码
    public static <T> Result<T> fail(ResultCode rc) {
        return new Result<>(rc.getCode(), rc.getMessage(), null);
    }

    // 失败 - 自定义消息
    public static <T> Result<T> fail(int code, String message) {
        return new Result<>(code, message, null);
    }
}

4.4 第 3 步:业务异常类 BizException

java 复制代码
@Getter
public class BizException extends RuntimeException {
    private final int code;

    public BizException(ResultCode rc) {
        super(rc.getMessage());
        this.code = rc.getCode();
    }

    public BizException(ResultCode rc, String message) {
        super(message);
        this.code = rc.getCode();
    }

    public BizException(int code, String message) {
        super(message);
        this.code = code;
    }
}

💡 为什么继承 RuntimeException 而不是 Exception

  • RuntimeException非受检异常 ,调用方不需要强制 catch
  • 这正是我们要的------业务方法只管抛,不需要每个调用方都写 try/catch
  • 全局异常处理器统一接住

4.5 第 4 步:全局异常处理器 GlobalExceptionHandler核心

java 复制代码
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    // 1. 业务异常
    @ExceptionHandler(BizException.class)
    public Result<Void> handleBizException(BizException e) {
        log.warn("业务异常: code={}, message={}", e.getCode(), e.getMessage());
        return Result.fail(e.getCode(), e.getMessage());
    }

    // 2. 参数校验异常(@Valid 失败)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<Void> handleValidException(MethodArgumentNotValidException e) {
        String msg = e.getBindingResult().getFieldErrors().stream()
                .map(FieldError::getDefaultMessage)
                .collect(Collectors.joining("; "));
        log.warn("参数校验失败: {}", msg);
        return Result.fail(ResultCode.BAD_REQUEST.getCode(), msg);
    }

    // 3. 404 资源不存在
    @ExceptionHandler(NoHandlerFoundException.class)
    public Result<Void> handleNotFound(NoHandlerFoundException e) {
        log.warn("资源不存在: {}", e.getRequestURL());
        return Result.fail(ResultCode.NOT_FOUND);
    }

    // 4. 兜底: 兜住所有未捕获的异常
    @ExceptionHandler(Throwable.class)
    public Result<Void> handleThrowable(Throwable e) {
        // ⚠️ 关键: 记录完整堆栈,但不暴露给前端
        log.error("系统异常", e);
        return Result.fail(ResultCode.SYSTEM_ERROR);
    }
}

4.6 第 5 步:Controller 改造

Controller 变得干干净净

java 复制代码
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @PostMapping("/register")
    public Result<Void> register(@RequestBody @Valid UserDTO dto) {
        userService.register(dto);  // 业务异常由 Service 抛
        return Result.success();
    }

    @GetMapping("/{id}")
    public Result<User> getById(@PathVariable Long id) {
        return Result.success(userService.getById(id));
    }
}

Service 也是干干净净

java 复制代码
@Service
public class UserService {

    public void register(UserDTO dto) {
        if (userDao.existsByUsername(dto.getUsername())) {
            throw new BizException(ResultCode.USER_ALREADY_EXISTS);
            // ✅ 业务代码里没有 try/catch
        }
        userDao.save(dto);
    }

    public User getById(Long id) {
        return userDao.findById(id)
                .orElseThrow(() -> new BizException(ResultCode.USER_NOT_EXISTS));
    }
}

4.7 改造前后的对比

#mermaid-svg-U8Ov4laRGHBA2rFW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-U8Ov4laRGHBA2rFW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-U8Ov4laRGHBA2rFW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-U8Ov4laRGHBA2rFW .error-icon{fill:#552222;}#mermaid-svg-U8Ov4laRGHBA2rFW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-U8Ov4laRGHBA2rFW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-U8Ov4laRGHBA2rFW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-U8Ov4laRGHBA2rFW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-U8Ov4laRGHBA2rFW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-U8Ov4laRGHBA2rFW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-U8Ov4laRGHBA2rFW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-U8Ov4laRGHBA2rFW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-U8Ov4laRGHBA2rFW .marker.cross{stroke:#333333;}#mermaid-svg-U8Ov4laRGHBA2rFW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-U8Ov4laRGHBA2rFW p{margin:0;}#mermaid-svg-U8Ov4laRGHBA2rFW .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-U8Ov4laRGHBA2rFW .cluster-label text{fill:#333;}#mermaid-svg-U8Ov4laRGHBA2rFW .cluster-label span{color:#333;}#mermaid-svg-U8Ov4laRGHBA2rFW .cluster-label span p{background-color:transparent;}#mermaid-svg-U8Ov4laRGHBA2rFW .label text,#mermaid-svg-U8Ov4laRGHBA2rFW span{fill:#333;color:#333;}#mermaid-svg-U8Ov4laRGHBA2rFW .node rect,#mermaid-svg-U8Ov4laRGHBA2rFW .node circle,#mermaid-svg-U8Ov4laRGHBA2rFW .node ellipse,#mermaid-svg-U8Ov4laRGHBA2rFW .node polygon,#mermaid-svg-U8Ov4laRGHBA2rFW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-U8Ov4laRGHBA2rFW .rough-node .label text,#mermaid-svg-U8Ov4laRGHBA2rFW .node .label text,#mermaid-svg-U8Ov4laRGHBA2rFW .image-shape .label,#mermaid-svg-U8Ov4laRGHBA2rFW .icon-shape .label{text-anchor:middle;}#mermaid-svg-U8Ov4laRGHBA2rFW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-U8Ov4laRGHBA2rFW .rough-node .label,#mermaid-svg-U8Ov4laRGHBA2rFW .node .label,#mermaid-svg-U8Ov4laRGHBA2rFW .image-shape .label,#mermaid-svg-U8Ov4laRGHBA2rFW .icon-shape .label{text-align:center;}#mermaid-svg-U8Ov4laRGHBA2rFW .node.clickable{cursor:pointer;}#mermaid-svg-U8Ov4laRGHBA2rFW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-U8Ov4laRGHBA2rFW .arrowheadPath{fill:#333333;}#mermaid-svg-U8Ov4laRGHBA2rFW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-U8Ov4laRGHBA2rFW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-U8Ov4laRGHBA2rFW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-U8Ov4laRGHBA2rFW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-U8Ov4laRGHBA2rFW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-U8Ov4laRGHBA2rFW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-U8Ov4laRGHBA2rFW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-U8Ov4laRGHBA2rFW .cluster text{fill:#333;}#mermaid-svg-U8Ov4laRGHBA2rFW .cluster span{color:#333;}#mermaid-svg-U8Ov4laRGHBA2rFW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-U8Ov4laRGHBA2rFW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-U8Ov4laRGHBA2rFW rect.text{fill:none;stroke-width:0;}#mermaid-svg-U8Ov4laRGHBA2rFW .icon-shape,#mermaid-svg-U8Ov4laRGHBA2rFW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-U8Ov4laRGHBA2rFW .icon-shape p,#mermaid-svg-U8Ov4laRGHBA2rFW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-U8Ov4laRGHBA2rFW .icon-shape .label rect,#mermaid-svg-U8Ov4laRGHBA2rFW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-U8Ov4laRGHBA2rFW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-U8Ov4laRGHBA2rFW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-U8Ov4laRGHBA2rFW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-U8Ov4laRGHBA2rFW .before>*{fill:#FFEBEE!important;stroke:#D32F2F!important;color:#333!important;}#mermaid-svg-U8Ov4laRGHBA2rFW .before span{fill:#FFEBEE!important;stroke:#D32F2F!important;color:#333!important;}#mermaid-svg-U8Ov4laRGHBA2rFW .before tspan{fill:#333!important;}#mermaid-svg-U8Ov4laRGHBA2rFW .after>*{fill:#E8F5E9!important;stroke:#388E3C!important;color:#333!important;}#mermaid-svg-U8Ov4laRGHBA2rFW .after span{fill:#E8F5E9!important;stroke:#388E3C!important;color:#333!important;}#mermaid-svg-U8Ov4laRGHBA2rFW .after tspan{fill:#333!important;} 改造后
Service 抛 BizException
ControllerAdvice 统一接
包成 Result 返回前端
改造前
Controller 满屏 try/catch
每个方法自己包 Map
异常信息直接抛给前端

代码量对比

指标 改造前 改造后
Controller 每个方法 15--20 行 3--5 行
异常处理 每个方法自己 try/catch 全局一处
返回体 不统一(Map) 统一(Result)
前端对接成本

4.8 @ControllerAdvice 完整执行流程

Result @ControllerAdvice Service Controller 客户端 Result @ControllerAdvice Service Controller 客户端 #mermaid-svg-5e82YiDyy0sE3k9e{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5e82YiDyy0sE3k9e .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5e82YiDyy0sE3k9e .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5e82YiDyy0sE3k9e .error-icon{fill:#552222;}#mermaid-svg-5e82YiDyy0sE3k9e .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5e82YiDyy0sE3k9e .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5e82YiDyy0sE3k9e .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5e82YiDyy0sE3k9e .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5e82YiDyy0sE3k9e .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5e82YiDyy0sE3k9e .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5e82YiDyy0sE3k9e .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5e82YiDyy0sE3k9e .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5e82YiDyy0sE3k9e .marker.cross{stroke:#333333;}#mermaid-svg-5e82YiDyy0sE3k9e svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5e82YiDyy0sE3k9e p{margin:0;}#mermaid-svg-5e82YiDyy0sE3k9e .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-5e82YiDyy0sE3k9e text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-5e82YiDyy0sE3k9e .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-5e82YiDyy0sE3k9e .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-5e82YiDyy0sE3k9e .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-5e82YiDyy0sE3k9e .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-5e82YiDyy0sE3k9e #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-5e82YiDyy0sE3k9e .sequenceNumber{fill:white;}#mermaid-svg-5e82YiDyy0sE3k9e #sequencenumber{fill:#333;}#mermaid-svg-5e82YiDyy0sE3k9e #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-5e82YiDyy0sE3k9e .messageText{fill:#333;stroke:none;}#mermaid-svg-5e82YiDyy0sE3k9e .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-5e82YiDyy0sE3k9e .labelText,#mermaid-svg-5e82YiDyy0sE3k9e .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-5e82YiDyy0sE3k9e .loopText,#mermaid-svg-5e82YiDyy0sE3k9e .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-5e82YiDyy0sE3k9e .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-5e82YiDyy0sE3k9e .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-5e82YiDyy0sE3k9e .noteText,#mermaid-svg-5e82YiDyy0sE3k9e .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-5e82YiDyy0sE3k9e .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-5e82YiDyy0sE3k9e .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-5e82YiDyy0sE3k9e .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-5e82YiDyy0sE3k9e .actorPopupMenu{position:absolute;}#mermaid-svg-5e82YiDyy0sE3k9e .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-5e82YiDyy0sE3k9e .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-5e82YiDyy0sE3k9e .actor-man circle,#mermaid-svg-5e82YiDyy0sE3k9e line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-5e82YiDyy0sE3k9e :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 业务异常时 HTTP 状态码仍是 200 1. HTTP 请求 1 2. 调用业务方法 2 3. 抛 BizException 3 4. 匹配 @ExceptionHandler 4 5. 包成 Result.fail(...) 5 6. 返回 200 + 业务码 6 7. 看 code 判断成功失败 7


五、踩坑预警:5 个真实会遇到的坑

坑 1:业务异常时 HTTP 状态码是 200,前端拿不到 500

#mermaid-svg-VObvybdi9qQgxYQG{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-VObvybdi9qQgxYQG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-VObvybdi9qQgxYQG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-VObvybdi9qQgxYQG .error-icon{fill:#552222;}#mermaid-svg-VObvybdi9qQgxYQG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VObvybdi9qQgxYQG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-VObvybdi9qQgxYQG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VObvybdi9qQgxYQG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VObvybdi9qQgxYQG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-VObvybdi9qQgxYQG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VObvybdi9qQgxYQG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VObvybdi9qQgxYQG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VObvybdi9qQgxYQG .marker.cross{stroke:#333333;}#mermaid-svg-VObvybdi9qQgxYQG svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VObvybdi9qQgxYQG p{margin:0;}#mermaid-svg-VObvybdi9qQgxYQG .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-VObvybdi9qQgxYQG .cluster-label text{fill:#333;}#mermaid-svg-VObvybdi9qQgxYQG .cluster-label span{color:#333;}#mermaid-svg-VObvybdi9qQgxYQG .cluster-label span p{background-color:transparent;}#mermaid-svg-VObvybdi9qQgxYQG .label text,#mermaid-svg-VObvybdi9qQgxYQG span{fill:#333;color:#333;}#mermaid-svg-VObvybdi9qQgxYQG .node rect,#mermaid-svg-VObvybdi9qQgxYQG .node circle,#mermaid-svg-VObvybdi9qQgxYQG .node ellipse,#mermaid-svg-VObvybdi9qQgxYQG .node polygon,#mermaid-svg-VObvybdi9qQgxYQG .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-VObvybdi9qQgxYQG .rough-node .label text,#mermaid-svg-VObvybdi9qQgxYQG .node .label text,#mermaid-svg-VObvybdi9qQgxYQG .image-shape .label,#mermaid-svg-VObvybdi9qQgxYQG .icon-shape .label{text-anchor:middle;}#mermaid-svg-VObvybdi9qQgxYQG .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-VObvybdi9qQgxYQG .rough-node .label,#mermaid-svg-VObvybdi9qQgxYQG .node .label,#mermaid-svg-VObvybdi9qQgxYQG .image-shape .label,#mermaid-svg-VObvybdi9qQgxYQG .icon-shape .label{text-align:center;}#mermaid-svg-VObvybdi9qQgxYQG .node.clickable{cursor:pointer;}#mermaid-svg-VObvybdi9qQgxYQG .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-VObvybdi9qQgxYQG .arrowheadPath{fill:#333333;}#mermaid-svg-VObvybdi9qQgxYQG .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-VObvybdi9qQgxYQG .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-VObvybdi9qQgxYQG .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VObvybdi9qQgxYQG .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-VObvybdi9qQgxYQG .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VObvybdi9qQgxYQG .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-VObvybdi9qQgxYQG .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-VObvybdi9qQgxYQG .cluster text{fill:#333;}#mermaid-svg-VObvybdi9qQgxYQG .cluster span{color:#333;}#mermaid-svg-VObvybdi9qQgxYQG div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-VObvybdi9qQgxYQG .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-VObvybdi9qQgxYQG rect.text{fill:none;stroke-width:0;}#mermaid-svg-VObvybdi9qQgxYQG .icon-shape,#mermaid-svg-VObvybdi9qQgxYQG .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VObvybdi9qQgxYQG .icon-shape p,#mermaid-svg-VObvybdi9qQgxYQG .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-VObvybdi9qQgxYQG .icon-shape .label rect,#mermaid-svg-VObvybdi9qQgxYQG .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VObvybdi9qQgxYQG .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-VObvybdi9qQgxYQG .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-VObvybdi9qQgxYQG :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-VObvybdi9qQgxYQG .err>*{fill:#FFEBEE!important;stroke:#D32F2F!important;color:#333!important;}#mermaid-svg-VObvybdi9qQgxYQG .err span{fill:#FFEBEE!important;stroke:#D32F2F!important;color:#333!important;}#mermaid-svg-VObvybdi9qQgxYQG .err tspan{fill:#333!important;}#mermaid-svg-VObvybdi9qQgxYQG .ok>*{fill:#E8F5E9!important;stroke:#388E3C!important;color:#333!important;}#mermaid-svg-VObvybdi9qQgxYQG .ok span{fill:#E8F5E9!important;stroke:#388E3C!important;color:#333!important;}#mermaid-svg-VObvybdi9qQgxYQG .ok tspan{fill:#333!important;} ❌ 前端判断接口失败

看 HTTP 状态码
HTTP 200

(业务异常时也是 200)
✅ 改判断业务 code

坑点 :Spring 默认情况下,业务异常被 ControllerAdvice 接住后,HTTP 状态码仍然是 200,业务码在 body 里。

前端常见错误写法

javascript 复制代码
// ❌ 错误: 用 HTTP 状态码判断
if (response.status !== 200) {
    alert("出错了");
}

正确写法

javascript 复制代码
// ✅ 正确: 看业务 code
const data = await response.json();
if (data.code !== 200) {
    alert(data.message);
}

💡 前后端约定HTTP 状态码管"网络通不通",业务 code 管"业务对不对"------这是行业共识。

坑 2:参数校验异常被其他 ExceptionHandler 抢先接住

坑点@Valid 失败时抛 MethodArgumentNotValidException,如果兜底的 @ExceptionHandler(Throwable.class) 在前面,会先被兜底接住------业务用户看到的可能是"系统异常"而不是"密码长度 6-20 位"。

修复 :把兜底放最后,具体异常放前面 ------Spring 会优先匹配最具体的异常类型

java 复制代码
// ✅ 顺序很重要: 从具体到宽泛
@ExceptionHandler(BizException.class)                      // 1. 最具体
@ExceptionHandler(MethodArgumentNotValidException.class)  // 2.
@ExceptionHandler(NoHandlerFoundException.class)           // 3.
@ExceptionHandler(Throwable.class)                         // 4. 兜底放最后

坑 3:异常信息直接抛给前端,泄露敏感信息

反例

java 复制代码
// ❌ 千万别这么干
@ExceptionHandler(Throwable.class)
public Result<Void> handle(Throwable e) {
    return Result.fail(6000, e.getMessage());  // 把 SQL 异常原样抛出去
}

前端会收到

json 复制代码
{
  "code": 6000,
  "message": "com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'WHERE id=1' at line 1"
}

安全风险

  • 💀 暴露 SQL 语句
  • 💀 暴露表名 / 字段名
  • 💀 暴露技术栈(MySQL 版本)

修复

java 复制代码
// ✅ 正确
@ExceptionHandler(Throwable.class)
public Result<Void> handle(Throwable e) {
    log.error("系统异常", e);  // 服务端记录完整堆栈
    return Result.fail(ResultCode.SYSTEM_ERROR);  // 前端只看到友好提示
}

🎯 金句服务端要"详尽"(记录全),前端要"克制"(只给友好提示)

坑 4:404 异常处理器不生效

坑点 :默认情况下 Spring 不会把 404 抛到 ControllerAdvice ,而是直接走 BasicErrorController

修复 :在 application.yml 显式配置:

yaml 复制代码
spring:
  mvc:
    throw-exception-if-no-handler-found: true   # 找不到 URL 时抛异常
  web:
    resources:
      add-mappings: false                       # 关闭默认静态资源映射

坑 5:异步方法 / MQ 消费里的异常拦截不到

坑点@ControllerAdvice 只能拦截 Controller 层的异常

场景 能不能被拦截
Controller 抛异常
Service 抛异常(被 Controller 调用)
@Async 异步方法抛异常 ❌(在另一个线程)
MQ 消费者抛异常 ❌(在消费线程)
定时任务抛异常 ❌(在调度线程)

#mermaid-svg-ACNhpkoZ5TqgUpvL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ACNhpkoZ5TqgUpvL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ACNhpkoZ5TqgUpvL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ACNhpkoZ5TqgUpvL .error-icon{fill:#552222;}#mermaid-svg-ACNhpkoZ5TqgUpvL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ACNhpkoZ5TqgUpvL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ACNhpkoZ5TqgUpvL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ACNhpkoZ5TqgUpvL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ACNhpkoZ5TqgUpvL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ACNhpkoZ5TqgUpvL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ACNhpkoZ5TqgUpvL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ACNhpkoZ5TqgUpvL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ACNhpkoZ5TqgUpvL .marker.cross{stroke:#333333;}#mermaid-svg-ACNhpkoZ5TqgUpvL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ACNhpkoZ5TqgUpvL p{margin:0;}#mermaid-svg-ACNhpkoZ5TqgUpvL .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ACNhpkoZ5TqgUpvL .cluster-label text{fill:#333;}#mermaid-svg-ACNhpkoZ5TqgUpvL .cluster-label span{color:#333;}#mermaid-svg-ACNhpkoZ5TqgUpvL .cluster-label span p{background-color:transparent;}#mermaid-svg-ACNhpkoZ5TqgUpvL .label text,#mermaid-svg-ACNhpkoZ5TqgUpvL span{fill:#333;color:#333;}#mermaid-svg-ACNhpkoZ5TqgUpvL .node rect,#mermaid-svg-ACNhpkoZ5TqgUpvL .node circle,#mermaid-svg-ACNhpkoZ5TqgUpvL .node ellipse,#mermaid-svg-ACNhpkoZ5TqgUpvL .node polygon,#mermaid-svg-ACNhpkoZ5TqgUpvL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ACNhpkoZ5TqgUpvL .rough-node .label text,#mermaid-svg-ACNhpkoZ5TqgUpvL .node .label text,#mermaid-svg-ACNhpkoZ5TqgUpvL .image-shape .label,#mermaid-svg-ACNhpkoZ5TqgUpvL .icon-shape .label{text-anchor:middle;}#mermaid-svg-ACNhpkoZ5TqgUpvL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ACNhpkoZ5TqgUpvL .rough-node .label,#mermaid-svg-ACNhpkoZ5TqgUpvL .node .label,#mermaid-svg-ACNhpkoZ5TqgUpvL .image-shape .label,#mermaid-svg-ACNhpkoZ5TqgUpvL .icon-shape .label{text-align:center;}#mermaid-svg-ACNhpkoZ5TqgUpvL .node.clickable{cursor:pointer;}#mermaid-svg-ACNhpkoZ5TqgUpvL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ACNhpkoZ5TqgUpvL .arrowheadPath{fill:#333333;}#mermaid-svg-ACNhpkoZ5TqgUpvL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ACNhpkoZ5TqgUpvL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ACNhpkoZ5TqgUpvL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ACNhpkoZ5TqgUpvL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ACNhpkoZ5TqgUpvL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ACNhpkoZ5TqgUpvL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ACNhpkoZ5TqgUpvL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ACNhpkoZ5TqgUpvL .cluster text{fill:#333;}#mermaid-svg-ACNhpkoZ5TqgUpvL .cluster span{color:#333;}#mermaid-svg-ACNhpkoZ5TqgUpvL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ACNhpkoZ5TqgUpvL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ACNhpkoZ5TqgUpvL rect.text{fill:none;stroke-width:0;}#mermaid-svg-ACNhpkoZ5TqgUpvL .icon-shape,#mermaid-svg-ACNhpkoZ5TqgUpvL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ACNhpkoZ5TqgUpvL .icon-shape p,#mermaid-svg-ACNhpkoZ5TqgUpvL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ACNhpkoZ5TqgUpvL .icon-shape .label rect,#mermaid-svg-ACNhpkoZ5TqgUpvL .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ACNhpkoZ5TqgUpvL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ACNhpkoZ5TqgUpvL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ACNhpkoZ5TqgUpvL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-ACNhpkoZ5TqgUpvL .ok>*{fill:#E8F5E9!important;stroke:#388E3C!important;color:#333!important;}#mermaid-svg-ACNhpkoZ5TqgUpvL .ok span{fill:#E8F5E9!important;stroke:#388E3C!important;color:#333!important;}#mermaid-svg-ACNhpkoZ5TqgUpvL .ok tspan{fill:#333!important;}#mermaid-svg-ACNhpkoZ5TqgUpvL .err>*{fill:#FFEBEE!important;stroke:#D32F2F!important;color:#333!important;}#mermaid-svg-ACNhpkoZ5TqgUpvL .err span{fill:#FFEBEE!important;stroke:#D32F2F!important;color:#333!important;}#mermaid-svg-ACNhpkoZ5TqgUpvL .err tspan{fill:#333!important;} Controller
Async
MQ
Job
业务方法抛异常
在哪个线程?
Controller 线程

✅ 拦截到
@Async 线程

❌ 拦截不到
MQ Consumer 线程

❌ 拦截不到
定时任务线程

❌ 拦截不到

修复 :这三种场景需要各自单独的异常处理

场景 处理方案
@Async 配置 AsyncUncaughtExceptionHandler
MQ 消费者 走死信队列(DLQ)+ 单独日志
定时任务 try/catch 手动接 + 告警

六、3 个进阶优化

6.1 优化 1:把异常消息 i18n 化

问题:错误信息硬编码中文,多语言项目怎么办?

方案 :用 MessageSource

java 复制代码
@ExceptionHandler(BizException.class)
public Result<Void> handle(BizException e, Locale locale) {
    String msg = messageSource.getMessage(
        "biz.error." + e.getCode(), null, e.getMessage(), locale);
    return Result.fail(e.getCode(), msg);
}

messages.properties 里配上:

properties 复制代码
biz.error.5001=User does not exist
biz.error.5002=Username already exists

6.2 优化 2:记录异常的"现场信息"

java 复制代码
@ExceptionHandler(BizException.class)
public Result<Void> handle(BizException e, HttpServletRequest req) {
    // 记录: 谁、什么时候、什么请求、什么 IP
    log.warn("业务异常: code={}, message={}, user={}, uri={}, ip={}",
             e.getCode(), e.getMessage(),
             getCurrentUser(), req.getRequestURI(), getClientIp(req));
    return Result.fail(e.getCode(), e.getMessage());
}

💡 好处:出问题时能直接定位"是哪个用户、哪次请求"。

6.3 优化 3:异常码分门别类

#mermaid-svg-gshZ4Ilri1lMqupc{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gshZ4Ilri1lMqupc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gshZ4Ilri1lMqupc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gshZ4Ilri1lMqupc .error-icon{fill:#552222;}#mermaid-svg-gshZ4Ilri1lMqupc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gshZ4Ilri1lMqupc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gshZ4Ilri1lMqupc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gshZ4Ilri1lMqupc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gshZ4Ilri1lMqupc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gshZ4Ilri1lMqupc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gshZ4Ilri1lMqupc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gshZ4Ilri1lMqupc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gshZ4Ilri1lMqupc .marker.cross{stroke:#333333;}#mermaid-svg-gshZ4Ilri1lMqupc svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gshZ4Ilri1lMqupc p{margin:0;}#mermaid-svg-gshZ4Ilri1lMqupc .edge{stroke-width:3;}#mermaid-svg-gshZ4Ilri1lMqupc .section--1 rect,#mermaid-svg-gshZ4Ilri1lMqupc .section--1 path,#mermaid-svg-gshZ4Ilri1lMqupc .section--1 circle,#mermaid-svg-gshZ4Ilri1lMqupc .section--1 polygon,#mermaid-svg-gshZ4Ilri1lMqupc .section--1 path{fill:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-gshZ4Ilri1lMqupc .section--1 text{fill:#ffffff;}#mermaid-svg-gshZ4Ilri1lMqupc .node-icon--1{font-size:40px;color:#ffffff;}#mermaid-svg-gshZ4Ilri1lMqupc .section-edge--1{stroke:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-gshZ4Ilri1lMqupc .edge-depth--1{stroke-width:17;}#mermaid-svg-gshZ4Ilri1lMqupc .section--1 line{stroke:hsl(60, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-gshZ4Ilri1lMqupc .disabled,#mermaid-svg-gshZ4Ilri1lMqupc .disabled circle,#mermaid-svg-gshZ4Ilri1lMqupc .disabled text{fill:lightgray;}#mermaid-svg-gshZ4Ilri1lMqupc .disabled text{fill:#efefef;}#mermaid-svg-gshZ4Ilri1lMqupc .section-0 rect,#mermaid-svg-gshZ4Ilri1lMqupc .section-0 path,#mermaid-svg-gshZ4Ilri1lMqupc .section-0 circle,#mermaid-svg-gshZ4Ilri1lMqupc .section-0 polygon,#mermaid-svg-gshZ4Ilri1lMqupc .section-0 path{fill:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-gshZ4Ilri1lMqupc .section-0 text{fill:black;}#mermaid-svg-gshZ4Ilri1lMqupc .node-icon-0{font-size:40px;color:black;}#mermaid-svg-gshZ4Ilri1lMqupc .section-edge-0{stroke:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-gshZ4Ilri1lMqupc .edge-depth-0{stroke-width:14;}#mermaid-svg-gshZ4Ilri1lMqupc .section-0 line{stroke:hsl(240, 100%, 83.5294117647%);stroke-width:3;}#mermaid-svg-gshZ4Ilri1lMqupc .disabled,#mermaid-svg-gshZ4Ilri1lMqupc .disabled circle,#mermaid-svg-gshZ4Ilri1lMqupc .disabled text{fill:lightgray;}#mermaid-svg-gshZ4Ilri1lMqupc .disabled text{fill:#efefef;}#mermaid-svg-gshZ4Ilri1lMqupc .section-1 rect,#mermaid-svg-gshZ4Ilri1lMqupc .section-1 path,#mermaid-svg-gshZ4Ilri1lMqupc .section-1 circle,#mermaid-svg-gshZ4Ilri1lMqupc .section-1 polygon,#mermaid-svg-gshZ4Ilri1lMqupc .section-1 path{fill:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-gshZ4Ilri1lMqupc .section-1 text{fill:black;}#mermaid-svg-gshZ4Ilri1lMqupc .node-icon-1{font-size:40px;color:black;}#mermaid-svg-gshZ4Ilri1lMqupc .section-edge-1{stroke:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-gshZ4Ilri1lMqupc .edge-depth-1{stroke-width:11;}#mermaid-svg-gshZ4Ilri1lMqupc .section-1 line{stroke:hsl(260, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-gshZ4Ilri1lMqupc .disabled,#mermaid-svg-gshZ4Ilri1lMqupc .disabled circle,#mermaid-svg-gshZ4Ilri1lMqupc .disabled text{fill:lightgray;}#mermaid-svg-gshZ4Ilri1lMqupc .disabled text{fill:#efefef;}#mermaid-svg-gshZ4Ilri1lMqupc .section-2 rect,#mermaid-svg-gshZ4Ilri1lMqupc .section-2 path,#mermaid-svg-gshZ4Ilri1lMqupc .section-2 circle,#mermaid-svg-gshZ4Ilri1lMqupc .section-2 polygon,#mermaid-svg-gshZ4Ilri1lMqupc .section-2 path{fill:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-gshZ4Ilri1lMqupc .section-2 text{fill:#ffffff;}#mermaid-svg-gshZ4Ilri1lMqupc .node-icon-2{font-size:40px;color:#ffffff;}#mermaid-svg-gshZ4Ilri1lMqupc .section-edge-2{stroke:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-gshZ4Ilri1lMqupc .edge-depth-2{stroke-width:8;}#mermaid-svg-gshZ4Ilri1lMqupc .section-2 line{stroke:hsl(90, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-gshZ4Ilri1lMqupc .disabled,#mermaid-svg-gshZ4Ilri1lMqupc .disabled circle,#mermaid-svg-gshZ4Ilri1lMqupc .disabled text{fill:lightgray;}#mermaid-svg-gshZ4Ilri1lMqupc .disabled text{fill:#efefef;}#mermaid-svg-gshZ4Ilri1lMqupc .section-3 rect,#mermaid-svg-gshZ4Ilri1lMqupc .section-3 path,#mermaid-svg-gshZ4Ilri1lMqupc .section-3 circle,#mermaid-svg-gshZ4Ilri1lMqupc .section-3 polygon,#mermaid-svg-gshZ4Ilri1lMqupc .section-3 path{fill:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-gshZ4Ilri1lMqupc .section-3 text{fill:black;}#mermaid-svg-gshZ4Ilri1lMqupc .node-icon-3{font-size:40px;color:black;}#mermaid-svg-gshZ4Ilri1lMqupc .section-edge-3{stroke:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-gshZ4Ilri1lMqupc .edge-depth-3{stroke-width:5;}#mermaid-svg-gshZ4Ilri1lMqupc .section-3 line{stroke:hsl(120, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-gshZ4Ilri1lMqupc .disabled,#mermaid-svg-gshZ4Ilri1lMqupc .disabled circle,#mermaid-svg-gshZ4Ilri1lMqupc .disabled text{fill:lightgray;}#mermaid-svg-gshZ4Ilri1lMqupc .disabled text{fill:#efefef;}#mermaid-svg-gshZ4Ilri1lMqupc .section-4 rect,#mermaid-svg-gshZ4Ilri1lMqupc .section-4 path,#mermaid-svg-gshZ4Ilri1lMqupc .section-4 circle,#mermaid-svg-gshZ4Ilri1lMqupc .section-4 polygon,#mermaid-svg-gshZ4Ilri1lMqupc .section-4 path{fill:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-gshZ4Ilri1lMqupc .section-4 text{fill:black;}#mermaid-svg-gshZ4Ilri1lMqupc .node-icon-4{font-size:40px;color:black;}#mermaid-svg-gshZ4Ilri1lMqupc .section-edge-4{stroke:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-gshZ4Ilri1lMqupc .edge-depth-4{stroke-width:2;}#mermaid-svg-gshZ4Ilri1lMqupc .section-4 line{stroke:hsl(150, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-gshZ4Ilri1lMqupc .disabled,#mermaid-svg-gshZ4Ilri1lMqupc .disabled circle,#mermaid-svg-gshZ4Ilri1lMqupc .disabled text{fill:lightgray;}#mermaid-svg-gshZ4Ilri1lMqupc .disabled text{fill:#efefef;}#mermaid-svg-gshZ4Ilri1lMqupc .section-5 rect,#mermaid-svg-gshZ4Ilri1lMqupc .section-5 path,#mermaid-svg-gshZ4Ilri1lMqupc .section-5 circle,#mermaid-svg-gshZ4Ilri1lMqupc .section-5 polygon,#mermaid-svg-gshZ4Ilri1lMqupc .section-5 path{fill:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-gshZ4Ilri1lMqupc .section-5 text{fill:black;}#mermaid-svg-gshZ4Ilri1lMqupc .node-icon-5{font-size:40px;color:black;}#mermaid-svg-gshZ4Ilri1lMqupc .section-edge-5{stroke:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-gshZ4Ilri1lMqupc .edge-depth-5{stroke-width:-1;}#mermaid-svg-gshZ4Ilri1lMqupc .section-5 line{stroke:hsl(180, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-gshZ4Ilri1lMqupc .disabled,#mermaid-svg-gshZ4Ilri1lMqupc .disabled circle,#mermaid-svg-gshZ4Ilri1lMqupc .disabled text{fill:lightgray;}#mermaid-svg-gshZ4Ilri1lMqupc .disabled text{fill:#efefef;}#mermaid-svg-gshZ4Ilri1lMqupc .section-6 rect,#mermaid-svg-gshZ4Ilri1lMqupc .section-6 path,#mermaid-svg-gshZ4Ilri1lMqupc .section-6 circle,#mermaid-svg-gshZ4Ilri1lMqupc .section-6 polygon,#mermaid-svg-gshZ4Ilri1lMqupc .section-6 path{fill:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-gshZ4Ilri1lMqupc .section-6 text{fill:black;}#mermaid-svg-gshZ4Ilri1lMqupc .node-icon-6{font-size:40px;color:black;}#mermaid-svg-gshZ4Ilri1lMqupc .section-edge-6{stroke:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-gshZ4Ilri1lMqupc .edge-depth-6{stroke-width:-4;}#mermaid-svg-gshZ4Ilri1lMqupc .section-6 line{stroke:hsl(210, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-gshZ4Ilri1lMqupc .disabled,#mermaid-svg-gshZ4Ilri1lMqupc .disabled circle,#mermaid-svg-gshZ4Ilri1lMqupc .disabled text{fill:lightgray;}#mermaid-svg-gshZ4Ilri1lMqupc .disabled text{fill:#efefef;}#mermaid-svg-gshZ4Ilri1lMqupc .section-7 rect,#mermaid-svg-gshZ4Ilri1lMqupc .section-7 path,#mermaid-svg-gshZ4Ilri1lMqupc .section-7 circle,#mermaid-svg-gshZ4Ilri1lMqupc .section-7 polygon,#mermaid-svg-gshZ4Ilri1lMqupc .section-7 path{fill:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-gshZ4Ilri1lMqupc .section-7 text{fill:black;}#mermaid-svg-gshZ4Ilri1lMqupc .node-icon-7{font-size:40px;color:black;}#mermaid-svg-gshZ4Ilri1lMqupc .section-edge-7{stroke:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-gshZ4Ilri1lMqupc .edge-depth-7{stroke-width:-7;}#mermaid-svg-gshZ4Ilri1lMqupc .section-7 line{stroke:hsl(270, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-gshZ4Ilri1lMqupc .disabled,#mermaid-svg-gshZ4Ilri1lMqupc .disabled circle,#mermaid-svg-gshZ4Ilri1lMqupc .disabled text{fill:lightgray;}#mermaid-svg-gshZ4Ilri1lMqupc .disabled text{fill:#efefef;}#mermaid-svg-gshZ4Ilri1lMqupc .section-8 rect,#mermaid-svg-gshZ4Ilri1lMqupc .section-8 path,#mermaid-svg-gshZ4Ilri1lMqupc .section-8 circle,#mermaid-svg-gshZ4Ilri1lMqupc .section-8 polygon,#mermaid-svg-gshZ4Ilri1lMqupc .section-8 path{fill:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-gshZ4Ilri1lMqupc .section-8 text{fill:black;}#mermaid-svg-gshZ4Ilri1lMqupc .node-icon-8{font-size:40px;color:black;}#mermaid-svg-gshZ4Ilri1lMqupc .section-edge-8{stroke:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-gshZ4Ilri1lMqupc .edge-depth-8{stroke-width:-10;}#mermaid-svg-gshZ4Ilri1lMqupc .section-8 line{stroke:hsl(330, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-gshZ4Ilri1lMqupc .disabled,#mermaid-svg-gshZ4Ilri1lMqupc .disabled circle,#mermaid-svg-gshZ4Ilri1lMqupc .disabled text{fill:lightgray;}#mermaid-svg-gshZ4Ilri1lMqupc .disabled text{fill:#efefef;}#mermaid-svg-gshZ4Ilri1lMqupc .section-9 rect,#mermaid-svg-gshZ4Ilri1lMqupc .section-9 path,#mermaid-svg-gshZ4Ilri1lMqupc .section-9 circle,#mermaid-svg-gshZ4Ilri1lMqupc .section-9 polygon,#mermaid-svg-gshZ4Ilri1lMqupc .section-9 path{fill:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-gshZ4Ilri1lMqupc .section-9 text{fill:black;}#mermaid-svg-gshZ4Ilri1lMqupc .node-icon-9{font-size:40px;color:black;}#mermaid-svg-gshZ4Ilri1lMqupc .section-edge-9{stroke:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-gshZ4Ilri1lMqupc .edge-depth-9{stroke-width:-13;}#mermaid-svg-gshZ4Ilri1lMqupc .section-9 line{stroke:hsl(0, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-gshZ4Ilri1lMqupc .disabled,#mermaid-svg-gshZ4Ilri1lMqupc .disabled circle,#mermaid-svg-gshZ4Ilri1lMqupc .disabled text{fill:lightgray;}#mermaid-svg-gshZ4Ilri1lMqupc .disabled text{fill:#efefef;}#mermaid-svg-gshZ4Ilri1lMqupc .section-10 rect,#mermaid-svg-gshZ4Ilri1lMqupc .section-10 path,#mermaid-svg-gshZ4Ilri1lMqupc .section-10 circle,#mermaid-svg-gshZ4Ilri1lMqupc .section-10 polygon,#mermaid-svg-gshZ4Ilri1lMqupc .section-10 path{fill:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-gshZ4Ilri1lMqupc .section-10 text{fill:black;}#mermaid-svg-gshZ4Ilri1lMqupc .node-icon-10{font-size:40px;color:black;}#mermaid-svg-gshZ4Ilri1lMqupc .section-edge-10{stroke:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-gshZ4Ilri1lMqupc .edge-depth-10{stroke-width:-16;}#mermaid-svg-gshZ4Ilri1lMqupc .section-10 line{stroke:hsl(30, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-gshZ4Ilri1lMqupc .disabled,#mermaid-svg-gshZ4Ilri1lMqupc .disabled circle,#mermaid-svg-gshZ4Ilri1lMqupc .disabled text{fill:lightgray;}#mermaid-svg-gshZ4Ilri1lMqupc .disabled text{fill:#efefef;}#mermaid-svg-gshZ4Ilri1lMqupc .section-root rect,#mermaid-svg-gshZ4Ilri1lMqupc .section-root path,#mermaid-svg-gshZ4Ilri1lMqupc .section-root circle,#mermaid-svg-gshZ4Ilri1lMqupc .section-root polygon{fill:hsl(240, 100%, 46.2745098039%);}#mermaid-svg-gshZ4Ilri1lMqupc .section-root text{fill:#ffffff;}#mermaid-svg-gshZ4Ilri1lMqupc .section-root span{color:#ffffff;}#mermaid-svg-gshZ4Ilri1lMqupc .section-2 span{color:#ffffff;}#mermaid-svg-gshZ4Ilri1lMqupc .icon-container{height:100%;display:flex;justify-content:center;align-items:center;}#mermaid-svg-gshZ4Ilri1lMqupc .edge{fill:none;}#mermaid-svg-gshZ4Ilri1lMqupc .mindmap-node-label{dy:1em;alignment-baseline:middle;text-anchor:middle;dominant-baseline:middle;text-align:center;}#mermaid-svg-gshZ4Ilri1lMqupc :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 业务异常码

体系
2xx 成功
200 操作成功
4xx 客户端错误
400 参数错误
401 未登录
403 无权限
404 资源不存在
5xx 业务错误
5001 用户不存在
5002 用户已存在
5003 密码错误
6xx 系统错误
6000 系统异常
6001 数据库异常
6002 远程调用失败

好处

  • 前端可以按段位处理(5xx 弹错误提示,6xx 跳错误页)
  • 监控可以按段位告警(6xx 触发紧急告警)
  • 文档可以按段位归类

七、如果重来一次,我会怎么学全局异常处理?

#mermaid-svg-IFhhyBTa3mldrGyo{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-IFhhyBTa3mldrGyo .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-IFhhyBTa3mldrGyo .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-IFhhyBTa3mldrGyo .error-icon{fill:#552222;}#mermaid-svg-IFhhyBTa3mldrGyo .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-IFhhyBTa3mldrGyo .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-IFhhyBTa3mldrGyo .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-IFhhyBTa3mldrGyo .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-IFhhyBTa3mldrGyo .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-IFhhyBTa3mldrGyo .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-IFhhyBTa3mldrGyo .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-IFhhyBTa3mldrGyo .marker{fill:#333333;stroke:#333333;}#mermaid-svg-IFhhyBTa3mldrGyo .marker.cross{stroke:#333333;}#mermaid-svg-IFhhyBTa3mldrGyo svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-IFhhyBTa3mldrGyo p{margin:0;}#mermaid-svg-IFhhyBTa3mldrGyo .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-IFhhyBTa3mldrGyo .cluster-label text{fill:#333;}#mermaid-svg-IFhhyBTa3mldrGyo .cluster-label span{color:#333;}#mermaid-svg-IFhhyBTa3mldrGyo .cluster-label span p{background-color:transparent;}#mermaid-svg-IFhhyBTa3mldrGyo .label text,#mermaid-svg-IFhhyBTa3mldrGyo span{fill:#333;color:#333;}#mermaid-svg-IFhhyBTa3mldrGyo .node rect,#mermaid-svg-IFhhyBTa3mldrGyo .node circle,#mermaid-svg-IFhhyBTa3mldrGyo .node ellipse,#mermaid-svg-IFhhyBTa3mldrGyo .node polygon,#mermaid-svg-IFhhyBTa3mldrGyo .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-IFhhyBTa3mldrGyo .rough-node .label text,#mermaid-svg-IFhhyBTa3mldrGyo .node .label text,#mermaid-svg-IFhhyBTa3mldrGyo .image-shape .label,#mermaid-svg-IFhhyBTa3mldrGyo .icon-shape .label{text-anchor:middle;}#mermaid-svg-IFhhyBTa3mldrGyo .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-IFhhyBTa3mldrGyo .rough-node .label,#mermaid-svg-IFhhyBTa3mldrGyo .node .label,#mermaid-svg-IFhhyBTa3mldrGyo .image-shape .label,#mermaid-svg-IFhhyBTa3mldrGyo .icon-shape .label{text-align:center;}#mermaid-svg-IFhhyBTa3mldrGyo .node.clickable{cursor:pointer;}#mermaid-svg-IFhhyBTa3mldrGyo .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-IFhhyBTa3mldrGyo .arrowheadPath{fill:#333333;}#mermaid-svg-IFhhyBTa3mldrGyo .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-IFhhyBTa3mldrGyo .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-IFhhyBTa3mldrGyo .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IFhhyBTa3mldrGyo .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-IFhhyBTa3mldrGyo .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IFhhyBTa3mldrGyo .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-IFhhyBTa3mldrGyo .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-IFhhyBTa3mldrGyo .cluster text{fill:#333;}#mermaid-svg-IFhhyBTa3mldrGyo .cluster span{color:#333;}#mermaid-svg-IFhhyBTa3mldrGyo div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-IFhhyBTa3mldrGyo .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-IFhhyBTa3mldrGyo rect.text{fill:none;stroke-width:0;}#mermaid-svg-IFhhyBTa3mldrGyo .icon-shape,#mermaid-svg-IFhhyBTa3mldrGyo .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IFhhyBTa3mldrGyo .icon-shape p,#mermaid-svg-IFhhyBTa3mldrGyo .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-IFhhyBTa3mldrGyo .icon-shape .label rect,#mermaid-svg-IFhhyBTa3mldrGyo .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IFhhyBTa3mldrGyo .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-IFhhyBTa3mldrGyo .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-IFhhyBTa3mldrGyo :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-IFhhyBTa3mldrGyo .step>*{fill:#F3E5F5!important;stroke:#9013FE!important;color:#333!important;}#mermaid-svg-IFhhyBTa3mldrGyo .step span{fill:#F3E5F5!important;stroke:#9013FE!important;color:#333!important;}#mermaid-svg-IFhhyBTa3mldrGyo .step tspan{fill:#333!important;} 第 1 步

理解 Java

异常体系
第 2 步

写一段满屏

try/catch 的代码
第 3 步

学 @ControllerAdvice

  • @ExceptionHandler
    第 4 步

设计业务异常码

  • 统一响应体
    第 5 步

改造业务代码

把 try/catch 全删掉
第 6 步

review 代码

(信息泄露/异步)

  1. 第 1 步 :先理解 Java 异常体系(Error / Exception / RuntimeException
  2. 第 2 步:写一段满屏 try/catch 的反例代码(亲身体验痛点)
  3. 第 3 步 :学 @ControllerAdvice + @ExceptionHandler 标准方案
  4. 第 4 步 :设计业务异常码 + 统一响应体(Result<T> + BizException
  5. 第 5 步:改造业务代码,把 try/catch 全删掉
  6. 第 6 步:review 代码(信息泄露 / 异步异常 / 顺序)

💡 过来人的金句先写满屏 try/catch 体验痛点,再学全局异常处理才有感觉。 一上来就 ControllerAdvice 容易"知其然不知其所以然"。


八、结尾

写到这里,这篇博客已经 6000+ 字了。最后做个小结:

全局异常处理不可怕,它就是"把 try/catch 从业务代码里抽出来"的一种工程化方案。

  • 实习生要学的 :能看懂 @ControllerAdvice,能给业务方法抛 BizException
  • 中级要学的:设计业务异常码体系、处理异步/MQ 异常、防信息泄露
  • 高级要学的:i18n 化、异常链路追踪、监控告警分级

最后送大家一句话,也是 mentor 告诉我的:

业务方法只做一件事------告诉调用方"成"或"不成"。 怎么告诉、告诉成什么样,交给框架处理。


📮 评论区聊聊:你设计的业务异常码是怎么分段的?你在异常处理上踩过什么坑?

我是 程序员小八777 ,一个喜欢把实习踩过的坑写成博客的计算机博主。如果这篇博客对你有帮助,欢迎 点赞 👍、收藏 ⭐、关注 🔔 ------你的支持就是我继续肝下去的最大动力!


📚 推荐阅读

相关推荐
风中的小甜瓜1 小时前
ASP.NET Page 那点事
java·前端·asp.net
爱敲代码的小鱼1 小时前
springsecurity:
java·开发语言·数据库
豆沙小饼干1 小时前
MySQL 小白循序渐进实操路线(纯动手,跟着敲就能练,全程生活化案例:外卖平台)
java
Wang's Blog1 小时前
Java框架快速入门: Spring Bean核心配置与实例化详解
java·开发语言·spring
爱敲代码的小鱼2 小时前
springboot原理篇:
java·spring boot·spring
zzzll11112 小时前
如何学习AI Agent?
java·人工智能·学习·程序员·大模型
阿pin2 小时前
Java随笔-GC收集器工作机制
java·开发语言·jvm·gc
2601_962440842 小时前
计算机毕业设计之jsp旅游网站管理系统
java·开发语言·算法·课程设计·旅游·推荐算法
默默敲代码的徐哥儿2 小时前
八股文整理——后端
java·开发语言·spring boot·后端·学习