接口规范设计:返回体 + 错误码 + 异常处理

文章目录

  • 一、企业级统一返回体通常包含哪些字段
    • 常用字段说明
      • [1. `success`](#1. success)
      • [2. `code`](#2. code)
      • [3. `message`](#3. message)
      • [4. `data`](#4. data)
      • [5. `timestamp`](#5. timestamp)
      • [6. `traceId`](#6. traceId)
      • [7. 可选扩展字段](#7. 可选扩展字段)
  • 二、企业级统一返回体有哪些常见"类型"
    • [1. 普通成功返回](#1. 普通成功返回)
    • [2. 无数据成功返回](#2. 无数据成功返回)
    • [3. 列表返回](#3. 列表返回)
    • [4. 分页返回](#4. 分页返回)
    • [5. 业务失败返回](#5. 业务失败返回)
    • [6. 参数校验失败返回](#6. 参数校验失败返回)
    • [7. 系统异常返回](#7. 系统异常返回)
  • 三、企业级错误码体系通常有哪些分类
    • [1. 按大类划分](#1. 按大类划分)
    • [2. 常见错误码明细](#2. 常见错误码明细)
    • [A 类:客户端错误](#A 类:客户端错误)
    • [B 类:业务错误](#B 类:业务错误)
    • [C 类:系统错误](#C 类:系统错误)
    • [D 类:第三方服务错误](#D 类:第三方服务错误)
  • 四、错误码体系设计原则
    • [1. 全局唯一](#1. 全局唯一)
    • [2. 可读可分段](#2. 可读可分段)
    • [3. 不要把 HTTP 状态码当业务码](#3. 不要把 HTTP 状态码当业务码)
    • [4. 业务码稳定,message 可变化](#4. 业务码稳定,message 可变化)
    • [5. 错误码集中维护](#5. 错误码集中维护)
  • [五、HTTP 状态码和业务错误码如何配合](#五、HTTP 状态码和业务错误码如何配合)
    • [方案一:HTTP 状态码表达协议层,业务码表达业务层](#方案一:HTTP 状态码表达协议层,业务码表达业务层)
    • [方案二:HTTP 永远 200,业务码区分全部状态](#方案二:HTTP 永远 200,业务码区分全部状态)
  • 六、企业级全局异常处理器通常处理哪些异常
    • [1. 业务异常 `BusinessException`](#1. 业务异常 BusinessException)
    • [2. 参数校验异常](#2. 参数校验异常)
    • [3. 认证异常](#3. 认证异常)
    • [4. 授权异常](#4. 授权异常)
    • [5. 404 / 方法不支持 / 媒体类型不支持](#5. 404 / 方法不支持 / 媒体类型不支持)
    • [6. 系统运行时异常](#6. 系统运行时异常)
    • [7. 第三方接口异常](#7. 第三方接口异常)
  • 七、企业级代码结构通常怎么设计
    • [1. 返回体对象](#1. 返回体对象)
    • [2. 错误码枚举](#2. 错误码枚举)
    • [3. 自定义业务异常](#3. 自定义业务异常)
    • [4. 全局异常处理器](#4. 全局异常处理器)
  • 八、企业里常见的增强设计
    • [1. 字段级错误明细对象](#1. 字段级错误明细对象)
    • [2. 统一日志打印](#2. 统一日志打印)
    • [3. 敏感信息脱敏](#3. 敏感信息脱敏)
    • [4. 国际化](#4. 国际化)
    • [5. 与网关、监控打通](#5. 与网关、监控打通)
  • 九、不推荐的做法
    • [1. Controller 到处 try-catch](#1. Controller 到处 try-catch)
    • [2. 直接把 Exception.getMessage() 返回前端](#2. 直接把 Exception.getMessage() 返回前端)
    • [3. 业务异常和系统异常不区分](#3. 业务异常和系统异常不区分)
    • [4. message 当主判断依据](#4. message 当主判断依据)
    • [5. 错误码随手写](#5. 错误码随手写)
  • 十、推荐模板
  • 十一、目录结构
  • 十二、理解

企业级一般会把这三块一起设计:

  1. 统一返回体
  2. 错误码体系
  3. 全局异常处理器

它们是配套的,不是孤立的。

你可以理解成:

  • 统一返回体:接口"长什么样"
  • 错误码体系:错误"怎么编号"
  • 全局异常处理器:异常"怎么统一收口并返回"

下面我按企业项目常见做法给你拆开讲。


一、企业级统一返回体通常包含哪些字段

最常见的是这套:

json 复制代码
{
  "success": true,
  "code": "00000",
  "message": "操作成功",
  "data": {},
  "timestamp": 1735800000000,
  "traceId": "a1b2c3d4"
}

常用字段说明

1. success

是否成功,布尔值。

json 复制代码
"success": true

作用:

  • 前端快速判断
  • 日志、监控、网关便于统一识别

有些团队不加它,只看 code,也可以。

但企业里加上以后,可读性更强。


2. code

业务状态码,最核心字段。

json 复制代码
"code": "00000"

作用:

  • 区分成功/失败类型
  • 前端按码处理业务分支
  • 便于文档化、监控统计、问题排查

一般建议:

  • 字符串 比用 int 更稳
  • 便于保留前导 0
  • 便于分段编码

3. message

给调用方看的说明信息。

json 复制代码
"message": "参数校验失败"

作用:

  • 给前端提示
  • 给联调人员快速定位问题
  • 给日志补充语义

注意:

  • 面向用户的提示不要暴露内部实现
  • 系统异常别直接把堆栈信息返回前端

4. data

实际业务数据。

json 复制代码
"data": {
  "id": 1,
  "name": "Ethan"
}

特点:

  • 成功时放业务结果
  • 失败时一般为 null
  • 尽量始终保留这个字段,避免前端判空混乱

5. timestamp

响应时间戳。

json 复制代码
"timestamp": 1735800000000

作用:

  • 排查问题
  • 和日志时间做对齐
  • 便于客户端诊断

6. traceId

链路追踪 ID。

json 复制代码
"traceId": "a1b2c3d4"

作用:

  • 微服务排障核心字段
  • 前端报错时把 traceId 带给后端,定位很快

企业项目里这个字段非常常见。


7. 可选扩展字段

有些项目还会加:

  • requestId
  • path
  • errors:字段级校验错误明细
  • extra:扩展信息

例如参数校验失败时:

json 复制代码
{
  "success": false,
  "code": "A0400",
  "message": "参数校验失败",
  "data": null,
  "errors": [
    { "field": "name", "message": "姓名不能为空" },
    { "field": "age", "message": "年龄必须大于0" }
  ],
  "timestamp": 1735800000000,
  "traceId": "a1b2c3d4"
}

二、企业级统一返回体有哪些常见"类型"

这里说的不是结构变种,而是业务使用场景

1. 普通成功返回

json 复制代码
{
  "success": true,
  "code": "00000",
  "message": "操作成功",
  "data": {
    "id": 1001,
    "username": "ethan"
  }
}

2. 无数据成功返回

比如删除、修改状态。

json 复制代码
{
  "success": true,
  "code": "00000",
  "message": "删除成功",
  "data": null
}

3. 列表返回

json 复制代码
{
  "success": true,
  "code": "00000",
  "message": "查询成功",
  "data": [
    { "id": 1, "name": "A" },
    { "id": 2, "name": "B" }
  ]
}

4. 分页返回

企业里一般会单独定义分页对象:

json 复制代码
{
  "success": true,
  "code": "00000",
  "message": "查询成功",
  "data": {
    "records": [
      { "id": 1, "name": "A" },
      { "id": 2, "name": "B" }
    ],
    "total": 120,
    "pageNum": 1,
    "pageSize": 10,
    "pages": 12
  }
}

5. 业务失败返回

json 复制代码
{
  "success": false,
  "code": "B2001",
  "message": "库存不足",
  "data": null
}

6. 参数校验失败返回

json 复制代码
{
  "success": false,
  "code": "A0400",
  "message": "参数校验失败",
  "data": null,
  "errors": [
    { "field": "mobile", "message": "手机号格式不正确" }
  ]
}

7. 系统异常返回

json 复制代码
{
  "success": false,
  "code": "C5000",
  "message": "系统繁忙,请稍后重试",
  "data": null,
  "traceId": "a1b2c3d4"
}

三、企业级错误码体系通常有哪些分类

企业项目里,错误码不会乱写,一般会分层、分段、分域


1. 按大类划分

一个常见设计:

分类 示例 含义
00000 成功 请求成功
Axxxx 客户端错误 参数、认证、权限等
Bxxxx 业务错误 库存不足、订单状态异常
Cxxxx 系统错误 服务异常、数据库异常
Dxxxx 第三方错误 调用支付、短信、物流失败

这是企业里很实用的一种分法。


2. 常见错误码明细

A 类:客户端错误

错误码 含义
A0001 用户端错误
A0100 用户注册错误
A0200 用户登录错误
A0300 访问权限错误
A0400 请求参数错误
A0401 必填参数缺失
A0402 参数格式错误
A0403 参数值超出范围
A0410 请求方法不支持
A0415 媒体类型不支持
A0500 请求过于频繁

B 类:业务错误

错误码 含义
B0001 业务处理失败
B1001 用户不存在
B1002 用户已禁用
B2001 库存不足
B2002 订单不存在
B2003 订单状态不允许取消
B3001 优惠券不存在
B3002 优惠券已过期
B3003 优惠券已使用

C 类:系统错误

错误码 含义
C0001 系统执行出错
C1001 数据库操作失败
C1002 数据库连接异常
C2001 缓存服务异常
C3001 文件上传失败
C4001 消息队列异常
C5000 未知系统异常

D 类:第三方服务错误

错误码 含义
D1001 支付调用失败
D1002 短信发送失败
D1003 物流接口调用失败
D1004 OCR 服务异常

四、错误码体系设计原则

1. 全局唯一

不能两个完全不同的问题共用一个码。


2. 可读可分段

看到错误码就大概知道归属:

  • A:客户端
  • B:业务
  • C:系统
  • D:第三方

3. 不要把 HTTP 状态码当业务码

比如不要直接:

  • 200 = 成功
  • 500 = 失败
  • 404 = 用户不存在

因为 HTTP 状态码和业务码是两套体系。


4. 业务码稳定,message 可变化

前端联调应该依赖 code,不要依赖 message 文案。

因为提示语后续可能改。


5. 错误码集中维护

建议统一放在:

  • 枚举 enum
  • 常量类
  • 错误码配置中心

不要散落在每个 service 里硬编码。


五、HTTP 状态码和业务错误码如何配合

企业项目里常见两种风格。

方案一:HTTP 状态码表达协议层,业务码表达业务层

这是更推荐的。

成功

  • HTTP 200
  • code = 00000

参数错误

  • HTTP 400
  • code = A0400

未登录

  • HTTP 401
  • code = A0201

无权限

  • HTTP 403
  • code = A0301

资源不存在

  • HTTP 404
  • code = B2002A0404

系统异常

  • HTTP 500
  • code = C5000

这套最清晰。


方案二:HTTP 永远 200,业务码区分全部状态

有些老项目会这样做。

例如:

json 复制代码
{
  "success": false,
  "code": "A0301",
  "message": "无访问权限",
  "data": null
}

HTTP 仍返回 200。

这种做法不是不能用,但问题是:

  • 不利于网关治理
  • 不利于监控告警
  • 不符合 REST 语义
  • 第三方调用方体验差

所以企业新项目一般更偏向HTTP + 业务码并用


六、企业级全局异常处理器通常处理哪些异常

全局异常处理器的目的,是把各种散乱异常统一转成标准返回体。

在 Spring Boot 里,一般用:

  • @RestControllerAdvice
  • @ExceptionHandler

来做。

通常要处理这些异常。


1. 业务异常 BusinessException

这是最重要的一类。

你自己主动抛出的异常,表示"业务不满足"。

例如:

  • 库存不足
  • 订单状态非法
  • 用户已被冻结

返回:

  • 指定业务码
  • 指定 message
  • HTTP 一般 200/400/409,取决于团队规范

2. 参数校验异常

常见包括:

  • MethodArgumentNotValidException
  • BindException
  • ConstraintViolationException
  • HttpMessageNotReadableException

对应场景:

  • @RequestBody 参数校验失败
  • 表单对象绑定失败
  • @RequestParam / @PathVariable 校验失败
  • JSON 格式错误

这类异常通常统一返回:

  • A0400 参数校验失败
  • 附带字段错误列表

3. 认证异常

例如:

  • 未登录
  • token 无效
  • token 过期

如果用了 Spring Security,还可能涉及:

  • AuthenticationException

返回:

  • HTTP 401
  • A0201 / A0202

4. 授权异常

例如:

  • 没有访问某接口的权限
  • 没有某角色/某资源权限

常见异常:

  • AccessDeniedException

返回:

  • HTTP 403
  • A0301

5. 404 / 方法不支持 / 媒体类型不支持

例如:

  • NoHandlerFoundException
  • HttpRequestMethodNotSupportedException
  • HttpMediaTypeNotSupportedException

分别对应:

  • 资源不存在
  • GET/POST 用错
  • Content-Type 不对

6. 系统运行时异常

例如:

  • NullPointerException
  • SQLException
  • RuntimeException

这类不能直接把原始异常暴露给前端。

应该:

  • 后端日志打印完整堆栈
  • 前端只返回统一兜底信息

例如:

  • C5000
  • "系统繁忙,请稍后重试"

7. 第三方接口异常

比如:

  • 支付网关超时
  • 短信服务失败
  • 调用外部接口 502

可以封装成统一异常:

  • ThirdPartyException
  • D1001 / D1002

七、企业级代码结构通常怎么设计

一个比较常见的分层如下。

1. 返回体对象

java 复制代码
public class ApiResponse<T> {
    private boolean success;
    private String code;
    private String message;
    private T data;
    private Long timestamp;
    private String traceId;

    public static <T> ApiResponse<T> success(T data) {
        ApiResponse<T> resp = new ApiResponse<>();
        resp.setSuccess(true);
        resp.setCode("00000");
        resp.setMessage("操作成功");
        resp.setData(data);
        resp.setTimestamp(System.currentTimeMillis());
        return resp;
    }

    public static <T> ApiResponse<T> fail(String code, String message) {
        ApiResponse<T> resp = new ApiResponse<>();
        resp.setSuccess(false);
        resp.setCode(code);
        resp.setMessage(message);
        resp.setData(null);
        resp.setTimestamp(System.currentTimeMillis());
        return resp;
    }
}

2. 错误码枚举

java 复制代码
public enum ErrorCode {

    SUCCESS("00000", "操作成功"),

    PARAM_ERROR("A0400", "参数校验失败"),
    PARAM_MISSING("A0401", "必填参数缺失"),
    PARAM_FORMAT_ERROR("A0402", "参数格式错误"),

    UNAUTHORIZED("A0201", "未登录或登录已过期"),
    FORBIDDEN("A0301", "无访问权限"),

    USER_NOT_FOUND("B1001", "用户不存在"),
    STOCK_NOT_ENOUGH("B2001", "库存不足"),
    ORDER_STATUS_INVALID("B2003", "订单状态异常"),

    SYSTEM_ERROR("C5000", "系统繁忙,请稍后重试"),
    DB_ERROR("C1001", "数据库异常"),
    THIRD_PARTY_ERROR("D1001", "第三方服务异常");

    private final String code;
    private final String message;

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

    public String code() {
        return code;
    }

    public String message() {
        return message;
    }
}

3. 自定义业务异常

java 复制代码
public class BusinessException extends RuntimeException {

    private final String code;
    private final String message;

    public BusinessException(ErrorCode errorCode) {
        super(errorCode.message());
        this.code = errorCode.code();
        this.message = errorCode.message();
    }

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

    public String getCode() {
        return code;
    }

    @Override
    public String getMessage() {
        return message;
    }
}

4. 全局异常处理器

java 复制代码
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ApiResponse<Void>> handleBusinessException(BusinessException ex) {
        return ResponseEntity
                .badRequest()
                .body(ApiResponse.fail(ex.getCode(), ex.getMessage()));
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ApiResponse<List<String>>> handleValidException(MethodArgumentNotValidException ex) {
        List<String> errors = ex.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(err -> err.getField() + ":" + err.getDefaultMessage())
                .toList();

        ApiResponse<List<String>> resp = ApiResponse.fail(ErrorCode.PARAM_ERROR.code(), ErrorCode.PARAM_ERROR.message());
        resp.setData(errors);
        return ResponseEntity.badRequest().body(resp);
    }

    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<ApiResponse<Void>> handleConstraintViolationException(ConstraintViolationException ex) {
        return ResponseEntity
                .badRequest()
                .body(ApiResponse.fail(ErrorCode.PARAM_ERROR.code(), ex.getMessage()));
    }

    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public ResponseEntity<ApiResponse<Void>> handleMethodNotSupported(HttpRequestMethodNotSupportedException ex) {
        return ResponseEntity
                .status(HttpStatus.METHOD_NOT_ALLOWED)
                .body(ApiResponse.fail("A0410", "请求方法不支持"));
    }

    @ExceptionHandler(AccessDeniedException.class)
    public ResponseEntity<ApiResponse<Void>> handleAccessDeniedException(AccessDeniedException ex) {
        return ResponseEntity
                .status(HttpStatus.FORBIDDEN)
                .body(ApiResponse.fail(ErrorCode.FORBIDDEN.code(), ErrorCode.FORBIDDEN.message()));
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse<Void>> handleException(Exception ex) {
        // 这里应该记录完整日志和堆栈
        return ResponseEntity
                .status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(ApiResponse.fail(ErrorCode.SYSTEM_ERROR.code(), ErrorCode.SYSTEM_ERROR.message()));
    }
}

八、企业里常见的增强设计

1. 字段级错误明细对象

不要只返回一句"参数错误",要能指出哪个字段错了。

例如:

java 复制代码
public class FieldErrorInfo {
    private String field;
    private Object rejectedValue;
    private String message;
}

2. 统一日志打印

全局异常处理器里通常要配合日志:

  • 业务异常:warn
  • 系统异常:error
  • 打印 traceId
  • 打印请求路径、参数、用户信息

3. 敏感信息脱敏

不能把这些直接返回前端:

  • SQL 语句
  • 堆栈
  • 数据库表名
  • 文件路径
  • 第三方密钥信息

4. 国际化

message 有时会走 i18n:

  • code 固定
  • message 根据 locale 翻译

5. 与网关、监控打通

通过 code + traceId + path + httpStatus 做:

  • 错误统计
  • 告警
  • SLA 分析
  • 问题定位

九、不推荐的做法

1. Controller 到处 try-catch

java 复制代码
try {
   ...
} catch (Exception e) {
   return xxx;
}

问题:

  • 重复代码太多
  • 风格不统一
  • 容易漏处理

应该交给全局异常处理器。


2. 直接把 Exception.getMessage() 返回前端

这很危险,容易泄露内部实现。


3. 业务异常和系统异常不区分

比如全部返回"操作失败"。

这样前端没法处理,排障也困难。


4. message 当主判断依据

前端不能写这种逻辑:

java 复制代码
if (message.equals("库存不足")) { ... }

应该判断 code


5. 错误码随手写

例如:

  • 1001
  • err_1
  • fail_stock
  • 500

混在一起非常难维护。


十、推荐模板

如果你是 Java / Spring Boot 项目,我比较推荐这套:

返回体

json 复制代码
{
  "success": true,
  "code": "00000",
  "message": "操作成功",
  "data": {},
  "timestamp": 1735800000000,
  "traceId": "xxx"
}

错误码分层

  • 00000:成功
  • Axxxx:客户端错误
  • Bxxxx:业务错误
  • Cxxxx:系统错误
  • Dxxxx:第三方错误

异常分类

  • BusinessException
  • 参数校验异常
  • 认证异常
  • 授权异常
  • 方法不支持异常
  • 媒体类型异常
  • 系统兜底异常

HTTP 规范

  • 200:成功
  • 400:参数错误
  • 401:未认证
  • 403:无权限
  • 404:资源不存在
  • 409:状态冲突
  • 500:系统异常

十一、目录结构

text 复制代码
common
 ├─ response
 │   ├─ ApiResponse.java
 │   ├─ PageResponse.java
 │   └─ ResultUtils.java
 ├─ error
 │   ├─ ErrorCode.java
 │   ├─ BusinessException.java
 │   ├─ ThirdPartyException.java
 │   └─ ErrorCodes.java
 ├─ exception
 │   └─ GlobalExceptionHandler.java
 └─ model
     └─ FieldErrorInfo.java

十二、理解

为了解决这些问题:

  • 前后端联调成本高
  • 错误处理不一致
  • 日志追踪困难
  • 微服务排障复杂
  • 文档和测试难统一

所以它的核心价值是:

规范接口契约,统一错误语义,降低系统复杂度。

相关推荐
阿Y加油吧2 小时前
LeetCode 二叉搜索树双神题通关!有序数组转平衡 BST + 验证 BST,小白递归一把梭
java·算法·leetcode
项目帮2 小时前
Java毕设选题推荐:基于springboot区块链的电子病历数据共享平台设计与实现【附源码、mysql、文档、调试+代码讲解+全bao等】
java·spring boot·课程设计
心有—林夕2 小时前
两个事务间的传播机制
java·事务
疯狂成瘾者2 小时前
什么是多 Agent,多Agent是如何协作的?
java
he___H2 小时前
Spring中的设计模式
java·spring·设计模式
liuyao_xianhui2 小时前
优选算法_最小基因变化_bfs_C++
java·开发语言·数据结构·c++·算法·哈希算法·宽度优先
做一个AK梦2 小时前
计算机系统概论知识点(软件设计师)
java·开发语言
東雪木3 小时前
Java学习——一访问修饰符(public/protected/default/private)的权限控制本质
java·开发语言·学习·java面试
两点王爷3 小时前
docker 创建和使用存储卷相关内容
java·docker·容器