摘要
本文深入解析了阿里巴巴编码规范中关于错误码的制定与管理原则,强调错误码应便于快速溯源和沟通标准化,避免过于复杂。介绍了错误码的命名与设计示例,推荐采用模块前缀、错误类型码和业务编号的结构。同时,探讨了项目错误信息管理机制,建议采用分层但统一的管理方式。强调错误码不能直接作为用户提示信息,使用者应避免随意定义新错误码,业务信息应由错误信息承载而非错误码本身。还讨论了获取第三方服务错误码时的处理方式,以及错误码的宏观分类、与HTTP状态码的关系、对跨文化协作的帮助等。最后,涉及异常体系的设计,包括常见异常分类、统一异常基类设计、统一异常处理器、标准错误返回结构等。
1. 【强制】错误码的制定原则:快速溯源、沟通标准化。 说明:错误码想得过于完美和复杂,就像康熙字典的生僻字一样,用词似乎精准,但是字典不容易随身携带且简单易懂。
正例:错误码回答的问题是谁的错?错在哪?
- 错误码必须能够快速知晓错误来源,可快速判断是谁的问题。
- 错误码必须能够进行清晰地比对(代码中容易 equals)。
- 错误码有利于团队快速对错误原因达到一致认知。
1.1. 错误码的制定原则
快速溯源
- 错误码应该一眼就能知道是哪个系统、模块、接口、哪一类错误。
- 不能出现“模糊”、“需要查源码”、“要问人”的情况。
沟通标准化
- 错误码不仅是给开发看的,也是给测试、运维、客服看的。
- 应避免个性化描述,比如“老王说接口挂了” vs “支付系统-用户认证失败(P100201)”。
不要康熙字典式复杂
- 错误描述越精准、越细分,并不代表越好。
- 错误码是为“快速识别 + 标准沟通”服务的,要简单、可识别、可比对(如 equals) 。
1.2. 错误码命名与设计示例
正确示例(推荐结构):
模块前缀 + 错误类型码 + 业务编号
例如:U010001,P100201
| 错误码 | 模块 | 描述 |
|---|---|---|
U010001 | 用户系统 | 用户未登录 |
U010002 | 用户系统 | 用户Token已失效 |
P100201 | 支付系统 | 支付渠道认证失败 |
O020301 | 订单系统 | 订单不存在 |
R030004 | 风控系统 | 命中黑名单规则 |
- 前缀如:
U=User模块、P=Payment、O=Order、R=Risk - 中间两位如:
01代表“认证错误”,02是“资源问题”,03是“参数非法” - 后三位为具体错误编号
错误示例
| 错误码 | 问题说明 |
|---|---|
10001 | 没有上下文,完全不知是哪个系统的错 |
ERR_USER_003_INVALID_TOKEN | 太长了,不利于代码中 equals 比对 |
1001-AB-XYZ | 非结构化格式,难以管理和比对 |
A0001 | 没有语义,无法直观看出模块来源 |
2. 【强制】项目错误信息是使用代码的全局统一error类处理还是每一层有自己error处理,还是直接写入代码里面?
推荐采用 “全局统一错误信息管理 + 层内扩展” 的方式,而不是“随便写”或“每层乱管”。
2.1. 最佳实践建议:分层但统一的错误信息管理机制
推荐结构:
- 统一错误码定义(全局 Enum / Code 类)
-
- 定义所有错误码、错误信息(用于开发、测试、运维统一查阅)
- 每个模块一个子枚举或前缀,方便归类
- 按模块/层封装异常类(可继承通用异常)
-
- 各层有自己的异常类型,但共享统一的错误码体系
- 每层处理只关心与其职责相关的错误(解耦)
- 异常中间层(统一异常处理器)
-
- 如 Spring 中的
@ControllerAdvice+ExceptionHandler - 负责把业务异常转换为 API 规范响应格式(例如统一返回 JSON 包含 code/message)
- 如 Spring 中的
2.2. 三种常见做法对比
| 做法 | 优点 | 缺点 / 风险 | 推荐程度 |
|---|---|---|---|
| ✅ 统一 error 定义 + 分层封装 | 清晰、可追踪、标准化,便于维护 | 初期定义需要一定规划 | 强烈推荐 |
| ⚠️ 每一层自己定义 error 信息 | 模块清晰 | 错误码不统一,可能重复、歧义、难排查 | 不推荐 |
| ❌ 直接写死在代码中(写字符串) | 快捷开发 | 混乱、无法比对、无法维护、代码耦合严重 | 禁止使用 |
2.3. 推荐的错误管理设计示例
2.3.1. 全局错误码定义(推荐用 Enum)
public enum ErrorCode {
SUCCESS("000000", "成功"),
USER_NOT_LOGIN("U010001", "用户未登录"),
USER_TOKEN_EXPIRED("U010002", "用户Token已过期"),
ORDER_NOT_FOUND("O020301", "订单不存在"),
SYSTEM_ERROR("S999999", "系统异常,请联系管理员");
private final String code;
private final String message;
// 构造、getter略
}
2.3.2. 定义通用业务异常类
public class BizException extends RuntimeException {
private final String code;
public BizException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.code = errorCode.getCode();
}
public String getCode() {
return code;
}
}
2.3.3. 统一异常处理器
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BizException.class)
public ResponseEntity<ApiResult> handleBizException(BizException ex) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(ApiResult.fail(ex.getCode(), ex.getMessage()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResult> handleException(Exception ex) {
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResult.fail(ErrorCode.SYSTEM_ERROR));
}
}
2.4. 项目中error信息打印总结
| 要点 | 建议 |
|---|---|
| 错误码管理 | 全局统一 ErrorCode,可枚举化、标准化 |
| 每层处理异常 | 封装业务异常类,使用统一错误码 |
| 对外返回 | 使用统一响应结构,统一异常处理器输出 |
| 禁止 | 直接在代码中写 "用户不存在"、"xxx失败"这样的魔法字符串 |
3. 【强制】错误码不能直接输出给用户作为提示信息使用。
说明:堆栈(stack_trace)、错误信息(error_message) 、错误码(error_code)、提示信息(user_tip)是一个有效关联并互相转义的和谐整体,但是请勿互相越俎代庖。
3.1. 规则深度理解
系统中的异常响应,通常包含这些字段:
| 字段 | 作用 | 使用对象 |
|---|---|---|
error_code | 错误编号,唯一定位错误来源 | 开发、测试、运维 |
error_message | 系统级说明,记录异常详细信息 | 开发排查 |
stack_trace | 调试信息,展示调用栈 | 仅开发环境调试时使用 |
user_tip | 提示给用户看的友好语言 | 最终用户 / 客户 |
这四者应彼此关联,但不得混用。
例如不能把 "U010001" 或 "NullPointerException" 直接显示给用户!
3.2. 异常返回错误示例(越俎代庖)
{
"error_code": "U010001",
"message": "用户未登录,请登录系统",
"stack_trace": "...NullPointerException at...",
"user_tip": "U010001" ←❌ 错:直接暴露错误码给用户
}
或者:
{
"error_code": "500",
"message": "java.lang.IllegalStateException: user is null", ←❌ 直接暴露底层异常信息
"user_tip": "user is null" ←❌ 不友好,普通用户看不懂
}
3.3. 异常返回正确示例(职责分离)
{
"error_code": "U010001",
"error_message": "用户未登录,token为空",
"stack_trace": null, // 生产环境不展示
"user_tip": "登录已过期,请重新登录"
}
- error_code:给技术人员看,一眼知道是用户系统的问题
- error_message:可写入日志,便于问题溯源
- stack_trace:仅调试使用,生产环境屏蔽
- user_tip:简洁友好的提示,告诉用户“该做什么”
4. 【强制】错误码使用者避免随意定义新的错误码。 说明:尽可能在原有错误码附表中找到语义相同或者相近的错误码在代码中使用即可。
❗不要每次遇到错误就随便新定义一个错误码,而是优先复用已有语义相近的错误码。
原因:
- 控制错误码数量:错误码越多,管理成本越高,排查难度越大。
- 提高语义一致性:同一种错误使用同一个错误码,减少团队歧义。
- 避免重复定义:很多错误其实是“变种”,可以共用一个错误码。
4.1. 错误示例:重复造码
// 登录校验失败
throw new BizException("U010001", "用户未登录");
// 注册校验失败
throw new BizException("U010005", "未登录用户无法注册");
实际上这两种情况都是“用户未登录”,本质一样,却用了两个不同的错误码,属于“重复造码”。
4.2. 正确示例:共用语义相近错误码
// 统一使用已定义的“用户未登录”错误码
throw new BizException(ErrorCode.USER_NOT_LOGIN);
登录页、注册页、敏感操作,都可以复用 U010001。
5. 【推荐】错误码之外的业务信息由 error_message 来承载,而不是让错误码本身涵盖过多具体业务属性。
5.1. 规则理解
不要让错误码承担太多含义,它的职责只是:标识错误的“类型”或“分类”
而像:
- 错误发生时的具体数据(如“用户ID”、“订单号”)
- 具体错误描述(如“商品【123】库存不足,仅剩余1件”)
这些内容都不应该写入错误码本身,而是通过 error_message 或其他字段承载。
5.2. 错误码 vs 错误信息职责分工
| 字段 | 作用 | 示例 | 特性 |
|---|---|---|---|
error_code | 错误类别编号 | P10001(支付失败) | 稳定、可比对 |
error_message | 具体业务场景的解释 | 订单12345支付失败,余额不足 | 动态、可读 |
user_tip(如有) | 面向用户的友好提示 | 支付失败,请检查账户余额 | 本地化、用户可见 |
5.3. 反例:错误码“负载过多信息”
{
"error_code": "P10001_OUT_OF_STOCK_ITEM_12345",
"error_message": "商品12345库存不足"
}
❌ 错误码中包含了具体商品ID,这是 动态业务属性,不利于归类、比对和复用。
5.4. 正例:错误码用于分类,信息分离
{
"error_code": "P10001",
"error_message": "商品 [12345] 库存不足,仅剩余 [1] 件",
"user_tip": "库存不足,请减少购买数量"
}
✅ 错误码 P10001 表示“通用库存不足”
✅ error_message 根据具体情况动态拼接,便于日志排查
✅ user_tip 可做国际化提示给用户
5.5. 实际工程建议
| 项目做法 | 建议 |
|---|---|
| 错误码只承载语义分类 | 如:参数错误、鉴权失败、业务失败等 |
| 不携带动态 ID、金额、字段名等 | 这些应放在 message |
| message 和 tip 支持模板填充 | 例如 "商品 {0} 库存不足",运行时动态填值 |
| 使用枚举或错误码常量类 | 限制错误码的随意性 |
6. 【推荐】在获取第三方服务错误码时,向上抛出允许本系统转义,由 C 转为 B, 并且在错误信息上带上原 有的第三方错误码。
当你调用第三方系统(C系统),它报错了,你不能直接把它的错误码暴露给用户或前端(B系统) ,而是要在自己系统内“转义”一层,抛出你本系统标准的错误码,并将原始错误码保留下来用于日志排查或内部分析。
6.1. 规则理解
场景角色:
- C系统 :第三方接口,例如支付平台、银行接口、OCR识别服务等。
- B系统 :你的调用方,比如前端、业务方、APP。
- 你自己 :中间的服务系统(通常是网关 / 后端接口 / 业务中台)。
规则拆解:
- 不要让第三方系统的错误码直接向上冒泡 (因为对方的码可能不规范、不稳定、无法识别)。
- 要“转义”:用你系统的统一错误码替代它
- 同时,原始的第三方错误码 + 错误信息保留用于排查或打日志,不让它丢失。
6.2. 错误示例:直接抛出第三方错误码
{
"error_code": "4003", ← 这是支付平台的错误码
"error_message": "签名无效"
}
问题:
- 前端和用户不知道“4003”代表啥
- 错误码不属于你系统的体系,破坏统一性
- 后续如果支付平台换服务、换码,前端也得改
6.3. 正确示例:本系统转义后抛出
{
"error_code": "P100201", ← 本系统定义:支付认证失败
"error_message": "支付渠道认证失败,请稍后重试",
"extra_info": {
"third_error_code": "4003",
"third_error_msg": "签名无效"
}
}
处理方式:
throw new BizException(ErrorCode.PAY_CHANNEL_AUTH_FAIL)
.withExtra("third_error_code", response.getCode())
.withExtra("third_error_msg", response.getMsg());
优势:
- 前端拿到的
P100201可读可识别,统一标准 - 原始错误码保留,方便你打日志、出问题溯源
6.4. 实际应用建议
| 场景 | 建议做法 |
|---|---|
| 调用第三方支付、银行、OCR等 | 不要直接返回对方错误码 |
| 把第三方错误码转成系统内错误码 | 使用枚举或错误码映射 |
| 原始错误信息不要丢 | 记录在日志、extra字段、监控平台 |
| 抛出时用系统统一 BizException | 方便统一处理和返回结构 |
6.5. 进阶:错误码映射表(推荐)
你可以为每个对接的第三方准备一张 错误码映射表 :
| 第三方错误码 | 本系统错误码 | 含义 |
|---|---|---|
4003 | P100201 | 支付签名无效 |
1002 | P100202 | 商户余额不足 |
0001 | P100203 | 授权过期 |
当第三方接口返回错误时,从映射表中找出对应的“本地错误码”抛出。
7. 【参考】错误码分为一级宏观错误码、 二级宏观错误码、 三级宏观错误码。
说明: 在无法更加具体确定的错误场景中, 可以直接使用一级宏观错误码, 分别是: A0001(用户端错误) 、 B0001(系统执行出错) 、 C0001(调用第三方服务出错)。
正例:调用第三方服务出错是一级,中间件错误是二级,消息服务出错是三级。
8. 【参考】错误码的后三位编号与 HTTP 状态码没有任何关系。
9. 【参考】错误码有利于不同文化背景的开发者进行交流与代码协作。
说明:英文单词形式的错误码不利于非英语母语国家(如阿拉伯语、希伯来语、俄罗斯语等)之间的开发者互相协作。
10. 【参考】错误码即人性,感性认知+口口相传,使用纯数字来进行错误码编排不利于感性记忆和分类。
说明:数字是一个整体,每位数字的地位和含义是相同的。
反例:一个五位数字 12345,第 1 位是错误等级,第 2 位是错误来源,345 是编号,人的大脑不会主动地拆开并分辨每位数字的不同含义。
10.1. 规则理解
纯数字错误码(如 12345)虽然简洁,但不具备“人性化、可读性、可记忆性”,也不利于团队协作、语义理解和错误分类。
10.2. 推荐做法:半结构化 + 可读的错误码设计
使用【字符 + 分类 + 数字】的组合模式,让错误码更易识别和传播。
[A][B][NNNNN]
| 字段 | 含义 | 示例 | 说明 |
|---|---|---|---|
| A | 业务域缩写(系统模块) | U | 用户系统 |
| B | 错误级别 / 类型标识 | 1 | 1 代表“校验类错误” |
| NNNN | 错误编号(可读、递增) | 0001 | 同类错误内递增编号 |
| 错误码 | 含义说明 |
|---|---|
U10001 | 用户未登录 |
U10002 | 用户权限不足 |
P20001 | 支付失败,余额不足 |
O30001 | OCR识别失败,图像模糊 |
这种格式即使不看文档,也能凭直觉猜出模块与大概的错误方向。
11. 【强制】Exception(异常)体系的设计
在一个中大型项目中,设计一套清晰、统一、可扩展的异常体系(Exception Architecture)是非常关键的,它决定了:
- 错误信息的可追踪性
- 用户提示的友好性
- 系统稳定性与可维护性
- 日志告警与监控的精确性
异常体系设计目标
| 目标 | 说明 |
|---|---|
| 职责单一 | 每类异常只处理一类问题 |
| 分级处理 | 明确哪些是业务异常,哪些是系统异常 |
| 统一格式 | 所有错误响应结构一致 |
| 便于扩展 | 后续新模块、新错误类型可无缝接入 |
| 可追踪溯源 | 包括错误码、日志、traceId、堆栈等信息 |
11.1. 常见异常分类(强烈推荐)
11.1.1. BizException(业务异常)
- 用户操作不符合业务规则,如余额不足、权限不足
- 是可预期、需要提示用户的异常
throw new BizException("BALANCE_NOT_ENOUGH", "余额不足");
11.1.2. SystemException(系统异常)
- 系统内部出错,如数据库连接失败、服务不可用
- 不直接提示用户,返回通用友好提示即可
throw new SystemException("数据库连接失败: " + e.getMessage(), e);
11.1.3. ParamException / ValidationException(参数异常)
- 请求参数格式错误、缺失、越界等
- 返回明确提示,前端能快速改正
throw new ParamException("手机号不能为空");
11.1.4. RemoteCallException(远程服务异常)
- 第三方/下游系统异常
- 支持带入原始返回码和消息
throw new RemoteCallException("ALIPAY_ERROR_001", "支付宝支付失败", aliPayResponse);
11.1.5. 衍生类建议:
如需更细分:
DbExceptionCacheExceptionThirdPartyException
11.2. 统一异常基类设计
public abstract class BaseException extends RuntimeException {
private final String errorCode;
private final String errorMessage;
public BaseException(String code, String msg) {
super(msg);
this.errorCode = code;
this.errorMessage = msg;
}
public String getErrorCode() { return errorCode; }
public String getErrorMessage() { return errorMessage; }
}
所有异常都继承自 BaseException,便于统一处理。
11.3. 统一异常处理器(Spring)
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BizException.class)
public ResponseEntity<ErrorResponse> handleBiz(BizException e) {
return ResponseEntity.ok(ErrorResponse.fail(e.getErrorCode(), e.getErrorMessage()));
}
@ExceptionHandler(ParamException.class)
public ResponseEntity<ErrorResponse> handleParam(ParamException e) {
return ResponseEntity.badRequest().body(ErrorResponse.fail(e.getErrorCode(), e.getErrorMessage()));
}
@ExceptionHandler(SystemException.class)
public ResponseEntity<ErrorResponse> handleSystem(SystemException e) {
log.error("系统异常", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ErrorResponse.fail("SYS_ERROR", "系统开小差了,请稍后再试"));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleOther(Exception e) {
log.error("未知异常", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ErrorResponse.fail("UNKNOWN_ERROR", "系统错误,请联系管理员"));
}
}
ResponseEntity 是 Spring 框架提供的类,无需你自己编写。它的作用是封装 HTTP 响应的状态码 + 响应体内容 + 响应头信息,你只需要使用它,不用自己定义。
org.springframework.http.ResponseEntity
11.4. ErrorResponse —— 你需要自己定义这个类,建议写法如下:
示例:通用错误响应类
public class ErrorResponse<T> {
private String code; // 响应码,例如 "SUCCESS", "PARAM_ERROR"
private String message; // 响应消息
private T data; // 泛型数据,支持任意类型
public ErrorResponse() {}
public ErrorResponse(String code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
// 工厂方法 - 成功
public static <T> ErrorResponse<T> success(T data) {
return new ErrorResponse<>("SUCCESS", "请求成功", data);
}
// 工厂方法 - 失败
public static <T> ErrorResponse<T> fail(String code, String message) {
return new ErrorResponse<>(code, message, null);
}
// Getter & Setter
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
11.5. ✅ 最终返回示例 JSON
例如:
{
"code": "PARAM_ERROR",
"message": "请求参数无效"
}
或者带 data 时:
{
"code": "BIZ_ERROR",
"message": "用户名已存在",
"data": {
"username": "alice"
}
}
11.6. ✅ 推荐:加上 Lombok 简化代码(可选)
如果你在项目中用了 Lombok,可以大大简化:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ErrorResponse<T> {
private String code;
private String message;
private T data;
public static <T> ErrorResponse<T> success(T data) {
return new Response<>("SUCCESS", "请求成功", data);
}
public static <T> ErrorResponse<T> fail(String code, String message) {
return new Response<>(code, message, null);
}
}
11.7. 标准错误返回结构(统一响应体)
@Data
@AllArgsConstructor(staticName = "fail")
public class ErrorResponse {
private String errorCode;
private String errorMessage;
}
⚠️ 可以进一步扩展 userTip, traceId, timestamp, debugMessage 等字段。
11.8. 异常体系设计总结
| 模块 | 内容 |
|---|---|
| 异常分类 | BizException, SystemException, ParamException, RemoteCallException等 |
| 统一基类 | BaseException 提供统一字段与结构 |
| 统一响应结构 | 统一返回 errorCode + errorMessage |
| 全局拦截器 | @RestControllerAdvice处理所有异常 |
| 统一错误码系统 | 错误码与异常结合使用,便于识别与追踪 |
11.9. 异常体系设计图(示意)
12. 【强制】Java 类库中定义的可以通过预检查方式规避的 RuntimeException 异常不应该通过 catch 的方 式来处理,比如:NullPointerException,IndexOutOfBoundsException 等等。
说明:无法通过预检查的异常除外,比如,在解析字符串形式的数字时,可能存在数字格式错误,不得不通过 catch NumberFormatException 来实现。能通过代码逻辑“预检查”来避免的异常,不应该使用 try-catch 来处理,而应该通过条件判断等方式在异常发生前就避免它发生。
12.1. ❌不推荐使用 catch 处理可预见的异常(如 NullPointerException)
try {
obj.method(); // 可能会抛出 NullPointerException
} catch (NullPointerException e) {
// 做一些补救
}
上面的做法是反模式 —— NullPointerException 是完全可以通过 if 语句来避免的,比如这样:
if (obj != null) {
obj.method();
}
这是“预检查”的概念,即程序可以事先判断并避免异常发生。
12.2. ✅推荐通过预检查方式规避异常
这种预防式编程更清晰、安全,也不会带来异常处理的性能开销。例如:
List<String> list = ...;
if (index >= 0 && index < list.size()) {
String value = list.get(index); // 安全访问,不会抛 IndexOutOfBoundsException
}
而不是:
try {
String value = list.get(index);
} catch (IndexOutOfBoundsException e) {
// 不推荐这样处理
}
12.3. 特殊:无法预知的异常可以使用 try-catch
比如 NumberFormatException:
try {
int value = Integer.parseInt(input);
} catch (NumberFormatException e) {
// 输入的字符串不是数字,这种异常难以通过判断规避
System.out.println("请输入合法数字");
}
因为你无法“预先判断一个字符串是否能被正确转换为整数”,所以这种异常不得不通过 catch 来处理。
异常捕获后不要用来做流程控制,条件控制。
说明:异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多。
13. 【强制】异常捕获后不要用来做流程控制,条件控制。
说明:异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多。异常的本质是用来处理“意外情况”的,不应该被用来当作“流程控制/条件判断”的手段使用。简单来说就是不用try/catch来做if判断。
13.1. ❌什么是“用异常做流程控制”?(反例)
来看一个典型的反例:
try {
int index = list.indexOf("target");
list.get(index); // 如果找不到会抛 IndexOutOfBoundsException
// 找到了,继续做其他处理
} catch (IndexOutOfBoundsException e) {
// 没找到,执行备用逻辑
}
这种写法的问题是:
- 本来你可以通过
list.contains("target")或index >= 0判断是否存在; - 却用
catch来判断是否成功找到了目标元素; - 这相当于:让程序主动抛异常,然后用异常当 if 条件用,这是一种反模式
13.2. ✅正确做法是“条件判断”控制流程(正例)
int index = list.indexOf("target");
if (index >= 0) {
list.get(index); // 安全获取
// 找到了,继续处理
} else {
// 没找到,执行备用逻辑
}
优点:
- 更清晰地表达了业务意图;
- 执行效率高(异常机制开销大);
- 更符合异常的语义(异常 = 不正常情况,而不是判断条件)。
14. 【强制】catch 时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的 catch 尽可能进行区分异常类型,再做对应的异常处理。
说明:对大段代码进行 try-catch,使程序无法根据不同的异常做出正确的应激反应,也不利于定位问题,这是一种不负责任的表现。正例:用户注册的场景中,如果用户输入非法字符,或用户名称已存在,或用户输入密码过于简单,在程序上作出分门别类的判断,并提示给用户。
在 try-catch 块中,要区分“稳定代码”和“非稳定代码”,并尽量精确捕获和分类处理异常,而不是“一锅端”。不能一整段直接上try/catch逻辑。
14.1. ❌反例:大段 try-catch 包裹所有代码,异常处理一刀切
try {
logger.info("开始注册用户");
String username = req.getUsername();
validateUsername(username); // 非稳定
saveToDatabase(username); // 非稳定
sendWelcomeEmail(username); // 非稳定
logger.info("注册成功");
} catch (Exception e) {
System.out.println("注册失败");
e.printStackTrace();
}
问题在于:
- 你不知道是哪一步出错了;
- 所有异常都进了一个通用 catch,无法做针对性处理;
- 无法给用户提供准确反馈(如“用户名已存在” vs “系统异常”);
- 不利于日志排查和定位问题。
14.2. ✅正例:精细化处理异常,明确哪些代码会抛什么异常
logger.info("开始注册用户");
String username = req.getUsername();
try {
validateUsername(username); // 可能抛自定义异常
} catch (InvalidUsernameException e) {
return "用户名格式非法";
}
try {
saveToDatabase(username); // 可能抛 DuplicateUserException
} catch (DuplicateUserException e) {
return "用户名已存在";
} catch (SQLException e) {
logger.error("数据库错误", e);
return "系统异常,请稍后重试";
}
try {
sendWelcomeEmail(username); // 可能抛 MailSendException
} catch (MailSendException e) {
logger.warn("欢迎邮件发送失败", e);
// 不影响注册主流程
}
logger.info("注册成功");
return "注册成功";
这样处理的优点:
- 问题定位清晰,异常一眼看出是哪块逻辑抛的;
- 业务反馈准确,如格式错误、重复用户分别给出不同提示;
- 日志有价值,只记录真正需要排查的异常;
- 稳中带韧,允许部分非关键失败而不中断主流程(如欢迎邮件失败也不影响注册成功)。
14.3. 规约落地建议:
| 要点 | 说明 |
|---|---|
| try 块尽量小 | 不要整段包住,应只包裹非稳定代码 |
| 精确 catch 异常类型 | 不要只写 catch (Exception e),应具体到异常种类 |
| 稳定代码无需放入 try 块 | 如日志打印、字符串拼接等 |
| 异常后应有应对策略 | 如记录日志、返回明确提示、回滚等 |
File file = new File(path);
List<String> lines;
try {
lines = Files.readAllLines(file.toPath());
} catch (IOException e) {
log.error("读取配置文件失败", e);
return;
}
try {
configService.saveConfig(lines);
} catch (ConfigSaveException e) {
log.error("保存配置失败", e);
return;
}
log.info("配置文件加载成功");
15. 【强制】捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。
捕获异常就必须处理异常。如果当前层不想处理,请继续向上传递。最外层必须兜底处理,并转换成用户能理解的结果。
15.1. ❌反例 1:什么都不做,吞掉异常
try {
riskyOperation();
} catch (Exception e) {
// 什么都不做
}
这是非常危险的行为,原因如下:
- 出错了你却“假装没事”,代码行为变得不可预测;
- 没有日志、没有报错,排查困难;
- 会掩盖程序bug,使问题难以暴露。
15.2. ❌反例 2:只打印异常,不做处理也不抛出
try {
doSomething();
} catch (Exception e) {
e.printStackTrace(); // 控制台输出,但实际业务逻辑继续往下走了
}
虽然比完全吞掉好一点,但这也不是处理,只是“把它写出来”而已。实际业务仍然忽略了异常的影响,比如数据没写入、事务未提交等问题。
15.3. ✅推荐做法情况一:当前层能处理,就处理异常
try {
userService.registerUser(user);
} catch (DuplicateUserException e) {
return Result.fail("用户名已存在");
} catch (InvalidInputException e) {
return Result.fail("输入不合法");
}
15.4. ✅情况二:当前层不能处理,就继续抛出
public void process() throws IOException {
readFile(); // 抛出 IOException
}
或者:
try {
doSomething();
} catch (IOException e) {
throw new RuntimeException("文件读取失败", e);
}
15.5. ✅最外层:必须处理异常,向用户输出友好的提示
@RestController
public class UserController {
@PostMapping("/register")
public Result register(@RequestBody User user) {
try {
userService.register(user);
return Result.success();
} catch (DuplicateUserException e) {
return Result.fail("用户名已存在");
} catch (Exception e) {
// 最后的兜底逻辑
log.error("系统异常", e);
return Result.fail("系统繁忙,请稍后重试");
}
}
}
16. 【强制】事务场景中,抛出异常被 catch 后,如果需要回滚,一定要注意手动回滚事务。
在事务方法中,如果你捕获(catch)了异常,那么事务不会自动回滚,你必须手动回滚事务,否则事务就会提交成功,即使中间逻辑其实出错了。Spring 的事务机制是基于 AOP 代理 + 异常传播机制 实现的。默认情况下,只有当事务方法抛出一个 运行时异常( RuntimeException 及其子类)或 Error,Spring 才会回滚事务。如果你把异常 catch 住了,Spring 就感知不到异常,自然也不会回滚事务。
16.1. ❌错误示例(事务不会回滚)
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Transactional
public void createOrder() {
try {
orderMapper.insertOrder(); // 插入订单
int i = 1 / 0; // 这里会抛异常
} catch (Exception e) {
// 异常被捕获了,没有再往外抛
System.out.println("异常处理:" + e.getMessage());
}
// 方法正常结束,事务提交,insertOrder() 数据成功插入!
}
}
结果:
insertOrder()执行了;- 异常被
catch住,没有传播; - Spring 认为方法执行正常,于是事务提交;
- 实际逻辑出错了,但数据库数据却保留了!
16.2. ✅ 正确示例 1:捕获后手动回滚
import org.springframework.transaction.interceptor.TransactionAspectSupport;
@Transactional
public void createOrder() {
try {
orderMapper.insertOrder();
int i = 1 / 0;
} catch (Exception e) {
// 手动回滚事务
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
System.out.println("异常处理并手动回滚:" + e.getMessage());
}
}
解释:
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();让当前事务标记为“只回滚”;- 即使异常被
catch了,Spring 在最后还是会回滚事务。
16.3. ✅ 正确示例 2:重新抛出异常让 Spring 感知
@Transactional
public void createOrder() {
try {
orderMapper.insertOrder();
int i = 1 / 0;
} catch (Exception e) {
System.out.println("日志记录:" + e.getMessage());
throw e; // 重新抛出异常,Spring 能感知并自动回滚
}
}
16.4. ✅ 正确示例 3:抛出自定义运行时异常(推荐)
public class BusinessException extends RuntimeException {
public BusinessException(String message) {
super(message);
}
}
@Transactional
public void createOrder() {
try {
orderMapper.insertOrder();
int i = 1 / 0;
} catch (Exception e) {
throw new BusinessException("业务异常,回滚事务");
}
}
16.5. 小结
| 异常处理方式 | 事务是否回滚 | 是否推荐 |
|---|---|---|
| 未捕获异常 | ✅ 回滚 | ✅ 推荐 |
| 捕获但不处理 | ❌ 不回滚 | ❌ 危险 |
| 捕获后手动回滚 | ✅ 回滚 | ✅ 推荐 |
| 捕获后重新抛出 | ✅ 回滚 | ✅ 推荐 |
| 捕获后抛出运行时异常 | ✅ 回滚 | ✅ 推荐 |
17. 【强制】finally 块必须对资源对象、 流对象进行关闭,有异常也要做 try-catch。
说明:如果 JDK7,可以使用 try-with-resources 方式。
不管是否发生异常,finally 块中一定要关闭资源或流对象,并且关闭操作本身也可能抛异常,所以关闭时也要使用 try-catch 来保证安全执行。如果使用的是 JDK7 及以上版本,推荐使用 try-with-resources 语法来自动关闭资源。
为什么这么规定?
- 资源泄漏风险大:比如文件流、数据库连接、Socket 等资源如果不关闭,会导致内存泄漏或连接耗尽等严重问题。
- 关闭操作本身可能抛异常:比如关闭文件流时,文件已被删除、磁盘错误等,可能抛异常,如果不 try-catch,可能导致程序终止或无法释放其他资源。
17.1. ❌错误示例(未关闭资源 or 关闭异常未处理)
public void readFile(String fileName) {
FileInputStream fis = null;
try {
fis = new FileInputStream(fileName);
// 读取文件内容
} catch (IOException e) {
e.printStackTrace();
} finally {
// 错误:关闭操作没有 try-catch,如果关闭时抛异常程序仍然可能出错
fis.close();
}
}
17.2. ✅ 正确示例(传统 try-finally,关闭时 try-catch)
public void readFile(String fileName) {
FileInputStream fis = null;
try {
fis = new FileInputStream(fileName);
// 读取文件内容
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace(); // 或写日志,不应再抛出
}
}
}
}
17.3. ✅ 更现代的做法:try-with-resources(推荐,JDK7+)
public void readFile(String fileName) {
try (FileInputStream fis = new FileInputStream(fileName)) {
// 读取文件内容
} catch (IOException e) {
e.printStackTrace();
}
}
try-with-resources 会在 try 结束后自动调用 close(),且自动处理关闭时抛出的异常,非常适合对实现了 AutoCloseable 接口的资源类。
18. 【强制】不要在 finally 块中使用 return
说明:try 块中的 return 语句执行成功后,并不马上返回,而是继续执行 finally 块中的语句,如果此处存在 return 语句,则会在此直接返回,无情丢弃掉 try 块中的返回点。
不要在 finally 中写 return 语句,因为它会覆盖掉 try 或 catch 中原本要返回的结果,导致程序行为异常、逻辑混乱,甚至出现难以发现的 bug。
为什么不能在 finally 里用 return ?
Java 的执行顺序是这样的:
try块中有return→ 暂时记录返回值;- 执行
finally块; - 如果
finally中也有return,就会丢弃原来的返回值,使用 finally 中的返回值返回。
这就意味着:finally 中的 return 会无情覆盖掉try 或 catch 中的 return,造成逻辑混乱,甚至隐藏异常。
18.1. ❌错误示例(返回值被覆盖)
public int test() {
try {
return 1;
} finally {
return 2; // 原来返回 1,现在被强制变成 2
}
}
调用 test() 会返回 2,而不是看上去的 1,很容易让人误解和踩坑。
18.2. ❌更复杂的情况(异常也被吞掉)
public int test() {
try {
int a = 1 / 0; // 抛出 ArithmeticException
return 1;
} finally {
return 2; // 异常被"吞了",方法居然返回 2
}
}
结果:不会抛出异常,而是返回了 2!这可能会导致错误数据进入系统,还不容易发现!
18.3. 正确示例(不要在 finally 中 return)
public int test() {
try {
return 1;
} finally {
System.out.println("finally 执行清理,但不 return");
}
}
输出:
finally 执行清理,但不 return
返回值:1
18.4. ✅ 正确做法总结:
finally主要用于资源释放或日志记录,不要做控制流操作(如 return、break、continue)- 永远不要在
finally中写return - 异常、返回逻辑都应该写在
try或catch中。
19. 【强制】捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类。
说明:如果预期对方抛的是绣球,实际接到的是铅球,就会产生意外情况。在 Java 中捕获异常( catch )时,应当确保捕获的是代码中实际抛出的异常类型,或者它的父类,否则可能会导致程序对异常处理不当,出现意料之外的错误。
19.1. ✅ 正确示例:捕获抛出的异常或其父类
public void processFile(String filePath) {
try {
readFile(filePath); // 这个方法声明抛 IOException
} catch (IOException e) { // 捕获的是抛出异常的父类或本身
System.err.println("读取文件失败:" + e.getMessage());
}
}
public void readFile(String filePath) throws FileNotFoundException {
throw new FileNotFoundException("文件不存在:" + filePath);
}
分析:
readFile抛出的是FileNotFoundException,这是IOException的子类。catch(IOException e)是可以捕获的,属于“捕获的是抛异常的父类”。
19.2. ❌ 错误示例:捕获的异常与实际不匹配
public void processFile(String filePath) {
try {
readFile(filePath); // 这个方法声明抛 IOException
} catch (SQLException e) { // ❌ 完全无关的异常类型
System.err.println("数据库异常:" + e.getMessage());
}
}
public void readFile(String filePath) throws IOException {
throw new FileNotFoundException("文件不存在:" + filePath);
}
分析:
catch(SQLException e)无法捕获IOException或FileNotFoundException。- 编译器直接报错:异常不匹配,必须捕获 IOException 或其父类。
- 说明你“预期接绣球,其实对方扔来的是铅球”,代码逻辑无法处理,甚至直接编译失败。
20. 【强制】在调用 RPC、二方包、或动态生成类的相关方法时,捕捉异常使用 Throwable 类进行拦截。
说明:通过反射机制来调用方法,如果找不到方法,抛出 NoSuchMethodException。什么情况会抛出
NoSuchMethodError 呢?二方包在类冲突时, 仲裁机制可能导致引入非预期的版本使类的方法签名不匹配, 或者在字节码修改框架(比如:ASM)动态创建或修改类时,修改了相应的方法签名。这些情况,即使代码编译期是正确的,但在代码运行期时,会抛出 NoSuchMethodError。
反例:足迹服务引入了高版本的 spring,导致运行到某段核心逻辑时,抛出 NoSuchMethodError 错误,catch 用的类却是 Exception,堆栈向上抛,影响到上层业务。这是一个非核心功能点影响到核心应用的典型反例。
在处理“高风险调用”(如 RPC、二方包、反射、动态类生成等)时,必须用 Throwable 来 catch 异常,以防止运行时出现 Error 类型(如 NoSuchMethodError ) 而漏捕。
20.1. 为什么不能只用 Exception?
Java 异常体系中:
Exception只能捕获 检查时异常 和 运行时异常(RuntimeException)- 但像
NoSuchMethodError是Error,属于 JVM 层面的严重问题,不是Exception的子类。 - 所以你用
catch (Exception e)是捕不到Error的,这就可能让严重异常传到上层影响主流程。
20.2. ✅ 正确示例:使用 Throwable 捕获所有异常和错误
public Object callRemoteService(Object proxy, Method method, Object[] args) {
try {
return method.invoke(proxy, args); // 反射调用远程服务
} catch (Throwable t) { // ✅ 捕获所有异常,包括 NoSuchMethodError、InvocationTargetException 等
log.error("远程服务调用失败", t);
return getDefaultResponse();
}
}
20.3. ❌ 错误示例:只捕获 Exception,导致漏掉严重错误
public Object callRemoteService(Object proxy, Method method, Object[] args) {
try {
return method.invoke(proxy, args);
} catch (Exception e) { // ❌ 无法捕获 NoSuchMethodError 等 Error
log.error("远程服务调用失败", e);
return getDefaultResponse();
}
}
- 如果底层类因版本冲突或字节码增强导致方法签名不匹配,运行时就会抛出
NoSuchMethodError。 catch (Exception e)捕不到,异常继续抛出,可能让调用该方法的上层业务整个失败。
21. 【推荐】定义时区分 unchecked / checked 异常,避免直接抛出 new RuntimeException(),更不允许抛出 Exception 或者 Throwable,应使用有业务含义的自定义异常。 推荐业界已定义过的自定义异常,如:DAOException / ServiceException 等。
这条推荐的核心思想是:在定义和使用异常时,应明确区分 Checked 和 Unchecked 异常,并使用具有业务含义的自定义异常类,避免使用通用的 RuntimeException 、 Exception 、 Throwable 。
21.1. 为什么这样推荐?
好处:
- 提高代码可读性与可维护性:当你看到
ServiceException或DAOException,你就能大致知道异常发生在哪一层、是哪类错误。 - 更好的异常分层与控制:使用业务自定义异常可以实现按模块、按层处理错误,简化统一异常处理逻辑。
- 避免误捕获和误抛出:抛出
Exception或Throwable容易造成不清晰的语义,同时也会干扰异常捕获机制。
21.2. Checked vs. Unchecked 异常
| 类型 | 父类 | 是否强制处理 | 使用场景 |
|---|---|---|---|
| Checked 异常 | Exception | 编译器强制处理 | 外部环境错误(如 IO、网络) |
| Unchecked 异常 | RuntimeException | 非强制处理 | 编程逻辑错误(如参数非法、状态错误) |
21.3. 推荐使用的异常类结构(示例)
定义基础异常类
public class BaseException extends RuntimeException {
private final String errorCode;
public BaseException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public String getErrorCode() {
return errorCode;
}
}
DAO 层自定义异常(用于持久层)
public class DAOException extends BaseException {
public DAOException(String message) {
super("DAO_ERROR", message);
}
public DAOException(String message, Throwable cause) {
super("DAO_ERROR", message);
initCause(cause);
}
}
Service 层自定义异常
public class ServiceException extends BaseException {
public ServiceException(String message) {
super("SERVICE_ERROR", message);
}
public ServiceException(String message, Throwable cause) {
super("SERVICE_ERROR", message);
initCause(cause);
}
}
21.4. ✅使用示例
21.4.1. DAO 层抛异常
public UserEntity findById(Long id) {
try {
return jdbcTemplate.queryForObject(...);
} catch (DataAccessException e) {
throw new DAOException("查询用户失败,ID:" + id, e);
}
}
21.4.2. Service 层处理 DAO 异常
public UserDTO getUser(Long id) {
try {
UserEntity entity = userDAO.findById(id);
return convert(entity);
} catch (DAOException e) {
throw new ServiceException("获取用户信息失败,ID:" + id, e);
}
}
21.5. ❌错误做法示例(⚠️不推荐)
throw new RuntimeException("出错了");
throw new Exception("不应该直接抛出 Exception");
throw new Throwable("严重错误"); // 绝不推荐
21.6. BizException异常设计与DAO、service层异常关系
你提到的 BizException(业务异常),在企业项目中非常常见,它通常是自定义异常体系中的核心一环,用于标识“业务逻辑相关的异常”。我们可以将它理解为:
- ✅ 表达业务语义的 RuntimeException
- ✅ 位于
Service层或Domain层(有时也跨越到Controller层) - ✅ 是一类语义明确的 Unchecked 异常
21.6.1. 方式一:BizException 是顶层业务异常,ServiceException / DAOException 是更具体的分类
java
复制编辑
public class BizException extends RuntimeException {
private final String code;
public BizException(String code, String message) {
super(message);
this.code = code;
}
public String getCode() {
return code;
}
}
再往下,定义更细化的业务异常:
java
复制编辑
public class ServiceException extends BizException {
public ServiceException(String message) {
super("SERVICE_ERROR", message);
}
}
public class DAOException extends BizException {
public DAOException(String message) {
super("DAO_ERROR", message);
}
}
👉 这种设计的优点是可以统一捕获 BizException ,再根据 code 做分类处理。
21.6.2. ✅ ****方式二:BizException 专指“纯业务异常”,DAOException/ServiceException 是系统层异常
在一些更严谨的分层系统中,会做如下区分:
BizException: 表示 业务逻辑无法继续执行 (如余额不足、用户未实名认证等)DAOException,ServiceException: 表示 系统故障 ,不属于正常的业务流程(如数据库访问失败、远程调用超时)
示例结构:
public class BizException extends RuntimeException {
public BizException(String message) {
super(message);
}
}
然后在业务代码中这样用:
if (account.getBalance() < withdrawAmount) {
throw new BizException("账户余额不足");
}
21.7. 统一异常处理建议
在全局异常处理器中(如 Spring Boot 的 @ControllerAdvice ),你可以统一处理:
@ExceptionHandler(BizException.class)
public ResponseEntity<ApiResult> handleBizException(BizException e) {
return ResponseEntity.ok(ApiResult.fail("BIZ_ERROR", e.getMessage()));
}
@ExceptionHandler(DAOException.class)
public ResponseEntity<ApiResult> handleDAOException(DAOException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResult.fail("DAO_ERROR", "系统数据库错误"));
}
21.8. 总结关系图(推荐结构)
RuntimeException
└── BizException // 业务逻辑错误(如余额不足、状态不合法)
├── ServiceException // 可选,服务层错误包装
└── DAOException // 可选,数据库/持久化层异常
21.9. 一句话总结
BizException 是业务语义的表达核心,通常位于异常体系中上层,用于标识“用户行为合法但业务无法完成”的场景。 DAOException 、 ServiceException 可以是其子类,也可以是并列分类,依据系统架构风格而定。
22. 【参考】对于公司外的 http / api 开放接口必须使用错误码,而应用内部推荐异常抛出;跨应用间RPC 调用优先考虑使用 Result 方式, 封装 isSuccess() 方法、错误码、错误简短信息;应用内部推荐异常抛出。
说明:关于 RPC 方法返回方式使用 Result 方式的理由:
- 使用抛异常返回方式, 调用方如果没有捕获到就会产生运行时错误。
- 如果不加栈信息, 只是 new 自定义异常, 加入自己的理解的 error message, 对于调用端解决问题的帮助不会太多。如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输的性能损耗也是问题。
22.1.1. 异常未捕获会导致运行时错误,影响整个调用链
- 如果 RPC 服务端抛出了异常(即使是自定义异常),调用方如果没有 catch 到,会直接报错,影响主流程。
- 在分布式系统中,这种行为不可靠,可能导致服务雪崩或不可控失败。
// 服务端
public User getUser(Long id) {
throw new BizException("用户不存在");
}
// 调用端
userService.getUser(id); // 如果没 try-catch,这里直接崩
22.1.2. 加栈信息的异常传输消耗性能,不加又调试困难
- 带堆栈信息的异常,序列化时体积大,影响 RPC 性能,尤其在高频调用或链路长的系统中。
- 不加堆栈只抛
new BizException("xxx"),调用方无法准确判断哪里错了,反而对排查帮助不大。
22.1. 推荐的RPC Result返回结构
22.1.1. 标准结构示例:
public class Result<T> {
private boolean success;
private String errorCode;
private String message;
private T data;
// 静态工厂方法
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.success = true;
result.data = data;
return result;
}
public static <T> Result<T> failure(String errorCode, String message) {
Result<T> result = new Result<>();
result.success = false;
result.errorCode = errorCode;
result.message = message;
return result;
}
public boolean isSuccess() {
return success;
}
}
22.1.2. 使用示例(服务端返回):
public Result<UserDTO> getUser(Long id) {
User user = userRepository.findById(id);
if (user == null) {
return Result.failure("USER_NOT_FOUND", "用户不存在");
}
return Result.success(convert(user));
}
22.1.3. 调用方使用:
Result<UserDTO> result = userApi.getUser(1L);
if (!result.isSuccess()) {
log.warn("调用失败,错误码:{},原因:{}", result.getErrorCode(), result.getMessage());
return;
}
UserDTO user = result.getData();
在应用内部调用中推荐使用“异常抛出 + 异常处理”机制,保持代码简洁清晰;但在跨服务 RPC 调用或开放 API 接口中,为了避免运行时错误传播、增强健壮性与可维护性,强烈建议使用统一的 Result 返回结构,封装错误码与信息,让调用方优雅处理所有结果。
22.2. Result 方式和spring的responeEntity关联关系
Result 方式与 Spring 的 ResponseEntity 完全兼容,常见于构建 RESTful 接口时的标准响应结构。下面是详细的设计方式和最佳实践。
22.2.1. 核心思路
| 组件 | 作用 |
|---|---|
Result<T> | 自定义业务响应体,封装 isSuccess、errorCode、message和 data |
ResponseEntity | Spring 提供的 HTTP 响应封装,包含 HTTP 状态码、Header、Body |
设计理念:Result 是业务结果,ResponseEntity 是 HTTP 层封装,两者可以搭配使用。
22.2.2. ✅ 推荐的 Result<T> 结构定义
public class Result<T> {
private boolean success;
private String code; // 错误码或成功码
private String message; // 提示信息
private T data;
// 工厂方法
public static <T> Result<T> ok(T data) {
Result<T> r = new Result<>();
r.success = true;
r.code = "SUCCESS";
r.message = "请求成功";
r.data = data;
return r;
}
public static <T> Result<T> fail(String code, String message) {
Result<T> r = new Result<>();
r.success = false;
r.code = code;
r.message = message;
r.data = null;
return r;
}
// 省略 getter/setter
}
22.3. ✅ Spring Controller 中使用方式
成功返回
@GetMapping("/user/{id}")
public ResponseEntity<Result<UserDTO>> getUser(@PathVariable Long id) {
UserDTO user = userService.getUser(id);
return ResponseEntity.ok(Result.ok(user));
}
失败返回(手动)
@GetMapping("/user/{id}")
public ResponseEntity<Result<UserDTO>> getUser(@PathVariable Long id) {
UserDTO user = userService.getUser(id);
if (user == null) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(Result.fail("USER_NOT_FOUND", "用户不存在"));
}
return ResponseEntity.ok(Result.ok(user));
}
22.3.1. ✅ 统一异常处理 + Result 封装
使用 @ControllerAdvice 实现全局异常捕获,并封装为 Result
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BizException.class)
public ResponseEntity<Result<Void>> handleBizException(BizException ex) {
return ResponseEntity
.badRequest()
.body(Result.fail(ex.getCode(), ex.getMessage()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Result<Void>> handleOtherException(Exception ex) {
// 打印日志
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Result.fail("SYSTEM_ERROR", "系统繁忙,请稍后再试"));
}
}
这样你就可以在业务代码中直接:
if (account.getBalance() < amount) {
throw new BizException("BALANCE_NOT_ENOUGH", "账户余额不足");
}
最终返回前端是:
{
"success": false,
"code": "BALANCE_NOT_ENOUGH",
"message": "账户余额不足",
"data": null
}
22.3.2. ✅ 拓展建议
如果你想让所有接口统一返回 Result<T>,可以:
- 封装一个
BaseController提供统一封装方法 - 或使用 Spring MVC 的
ResponseBodyAdvice统一封装所有响应(需排除 Swagger、文件流、静态资源等)
22.3.3. ✅ 总结结构图
Controller
└── ResponseEntity<Result<T>>
├── HTTP 状态码(如 200, 400)
└── Result<T>
├── success
├── code(业务码)
├── message
└── data
22.3.4. 示例结果 JSON
{
"success": true,
"code": "SUCCESS",
"message": "请求成功",
"data": {
"id": 1,
"name": "张三"
}
}
博文参考
《阿里巴巴java规范》