如何自研一个全局异常处理器
------一个 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 步 :先理解 Java 异常体系(
Error/Exception/RuntimeException) - 第 2 步:写一段满屏 try/catch 的反例代码(亲身体验痛点)
- 第 3 步 :学
@ControllerAdvice + @ExceptionHandler标准方案 - 第 4 步 :设计业务异常码 + 统一响应体(
Result<T>+BizException) - 第 5 步:改造业务代码,把 try/catch 全删掉
- 第 6 步:review 代码(信息泄露 / 异步异常 / 顺序)
💡 过来人的金句 :先写满屏 try/catch 体验痛点,再学全局异常处理才有感觉。 一上来就 ControllerAdvice 容易"知其然不知其所以然"。
八、结尾
写到这里,这篇博客已经 6000+ 字了。最后做个小结:
全局异常处理不可怕,它就是"把 try/catch 从业务代码里抽出来"的一种工程化方案。
- 实习生要学的 :能看懂
@ControllerAdvice,能给业务方法抛BizException - 中级要学的:设计业务异常码体系、处理异步/MQ 异常、防信息泄露
- 高级要学的:i18n 化、异常链路追踪、监控告警分级
最后送大家一句话,也是 mentor 告诉我的:
业务方法只做一件事------告诉调用方"成"或"不成"。 怎么告诉、告诉成什么样,交给框架处理。
📮 评论区聊聊:你设计的业务异常码是怎么分段的?你在异常处理上踩过什么坑?
我是 程序员小八777 ,一个喜欢把实习踩过的坑写成博客的计算机博主。如果这篇博客对你有帮助,欢迎 点赞 👍、收藏 ⭐、关注 🔔 ------你的支持就是我继续肝下去的最大动力!
📚 推荐阅读
- 📖 Spring 官方文档 - Exception Handling --- 最权威的入门资料
- 📖 Spring Boot 统一异常处理最佳实践 --- Baeldung 经典教程
- 📖 阿里巴巴 Java 开发手册 - 异常处理规约 --- 国内大厂规范
- 📺 Spring Boot 异常处理实战(B 站) --- 中文视频教程
- 📖 ControllerAdvice 与 ResponseStatusException 的区别 --- 进阶阅读
- 📖 Spring Boot Error Handling 完整指南 --- 涵盖 404/500 等场景