ApiCode 定义
参考来源
- alibaba/p3c: Alibaba Java Coding Guidelines pmd implements and IDE plugin (github.com)
- lvyahui8/feego-common: 基于 spring-boot 的脚手架,封装基础能力。如日志、加签验签、分布式锁、文件上传、JWT、异常处理、响应封装等等 (github.com)
- 错误码参考 - SQL 标准错误码说明 - 《华为 openGauss (GaussDB) v1.1 使用手册》 - 书栈网 · BookStack
- Errno 错误码 | 微信开放文档 (qq.com)
- System Error Codes (0-499) (WinError.h) - Win32 apps | Microsoft Learn
定义规范
语法标准
以下来源自
Java开发手册(黄山版)
- 【强制】错误码的制定原则:快速溯源、沟通标准化。
- 说明:错误码想得过于完美和复杂,就像康熙字典的生僻字一样,用词似乎精准,但是字典不容易随身携带且简单易懂。
- 正例:错误码回答的问题是谁的错?错在哪?
- 错误码必须能够快速知晓错误来源,可快速判断是谁的问题。
- 错误码必须能够进行清晰地比对(代码中容易 equals)。
- 错误码有利于团队快速对错误原因达到一致认知。
- 【强制】错误码不体现版本号和错误等级信息。
- 说明:错误码以不断追加的方式进行兼容。错误等级由日志和错误码本身的释义来决定。
- 【强制】全部正常,但不得不填充错误码时返回五个零:00000。
- 【强制】错误码为字符串类型,共 5 位,分成两个部分:错误产生来源+四位数字编号。
- 说明:错误产生来源分为 A/B/C,
- A 表示错误来源于用户,比如参数错误,用户安装版本过低,用户支付超时等问题;
- B 表示错误来源于当前系统,往往是业务逻辑出错,或程序健壮性差等问题;
- C 表示错误来源于第三方服务,比如 CDN 服务出错,消息投递超时等问题;
- 四位数字编号从 0001 到 9999,大类之间的步长间距预留 100,参考参考值表。
- 说明:错误产生来源分为 A/B/C,
- 【强制】编号不与公司业务架构,更不与组织架构挂钩,以先到先得的原则在统一平台上进行,审批生效,编号即被永久固定。
- 【强制】错误码使用者避免随意定义新的错误码。
- 说明:尽可能在原有错误码附表中找到语义相同或者相近的错误码在代码中使用即可。
- 【强制】错误码不能直接输出给用户作为提示信息使用。
- 说明:堆栈(stack_trace)、错误信息(error_message) 、错误码(error_code)、提示信息(user_tip)是一个有效关联并互相转义的和谐整体,但是请勿互相越俎代庖。
- 【推荐】错误码之外的业务信息由 error_message 来承载,而不是让错误码本身涵盖过多具体业务属性。
- 【推荐】在获取第三方服务错误码时,向上抛出允许本系统转义,由 C 转为 B,并且在错误信息上带上原有的第三方错误码。
- 【参考】错误码分为一级宏观错误码、二级宏观错误码、三级宏观错误码。
- 说明:在无法更加具体确定的错误场景中,可以直接使用一级宏观错误码,分别是:A0001(用户端错误)、B0001(系统执行出错)、C0001(调用第三方服务出错)。
- 正例:调用第三方服务出错是一级,中间件错误是二级,消息服务出错是三级。
- 【参考】错误码的后三位编号与 HTTP 状态码没有任何关系。
- 【参考】错误码有利于不同文化背景的开发者进行交流与代码协作。
- 说明:英文单词形式的错误码不利于非英语母语国家(如阿拉伯语、希伯来语、俄罗斯语等)之间的开发者互相协作。
- 【参考】错误码即人性,感性认知+口口相传,使用纯数字来进行错误码编排不利于感性记忆和分类。
- 说明:数字是一个整体,每位数字的地位和含义是相同的。
- 反例:一个五位数字 12345,第 1 位是错误等级,第 2 位是错误来源,345 是编号,人的大脑不会主动地拆开并分辨每位数字的不同含义。
参考值表
以下来源自
Java开发手册(黄山版)
| 错误码 | 中文描述 | 说明 |
|---|---|---|
| 0 | 一切 ok | 正确执行后的返回 |
| A0001 | 用户端错误 | 一级宏观错误码 |
| A0100 | 用户注册错误 | 二级宏观错误码 |
| A0101 | 用户未同意隐私协议 | |
| A0102 | 注册国家或地区受限 | |
| A0110 | 用户名校验失败 | |
| A0111 | 用户名已存在 | |
| A0112 | 用户名包含敏感词 | |
| A0113 | 用户名包含特殊字符 | |
| A0120 | 密码校验失败 | |
| A0121 | 密码长度不够 | |
| A0122 | 密码强度不够 | |
| A0130 | 校验码输入错误 | |
| A0131 | 短信校验码输入错误 | |
| A0132 | 邮件校验码输入错误 | |
| A0133 | 语音校验码输入错误 | |
| A0140 | 用户证件异常 | |
| A0141 | 用户证件类型未选择 | |
| A0142 | 大陆身份证编号校验非法 | |
| A0143 | 护照编号校验非法 | |
| A0144 | 军官证编号校验非法 | |
| A0150 | 用户基本信息校验失败 | |
| A0151 | 手机格式校验失败 | |
| A0152 | 地址格式校验失败 | |
| A0153 | 邮箱格式校验失败 | |
| A0200 | 用户登录异常 | 二级宏观错误码 |
| A0201 | 用户账户不存在 | |
| A0202 | 用户账户被冻结 | |
| A0203 | 用户账户已作废 | |
| A0210 | 用户密码错误 | |
| A0211 | 用户输入密码错误次数超限 | |
| A0220 | 用户身份校验失败 | |
| A0221 | 用户指纹识别失败 | |
| A0222 | 用户面容识别失败 | |
| A0223 | 用户未获得第三方登录授权 | |
| A0230 | 用户登录已过期 | |
| A0240 | 用户验证码错误 | |
| A0241 | 用户验证码尝试次数超限 | |
| A0300 | 访问权限异常 | 二级宏观错误码 |
| A0301 | 访问未授权 | |
| A0302 | 正在授权中 | |
| A0303 | 用户授权申请被拒绝 | |
| A0310 | 因访问对象隐私设置被拦截 | |
| A0311 | 授权已过期 | |
| A0312 | 无权限使用 API | |
| A0320 | 用户访问被拦截 | |
| A0321 | 黑名单用户 | |
| A0322 | 账号被冻结 | |
| A0323 | 非法 IP 地址 | |
| A0324 | 网关访问受限 | |
| A0325 | 地域黑名单 | |
| A0330 | 服务已欠费 | |
| A0340 | 用户签名异常 | |
| A0341 | RSA 签名错误 | |
| A0400 | 用户请求参数错误 | 二级宏观错误码 |
| A0401 | 包含非法恶意跳转链接 | |
| A0402 | 无效的用户输入 | |
| A0410 | 请求必填参数为空 | |
| A0411 | 用户订单号为空 | |
| A0412 | 订购数量为空 | |
| A0413 | 缺少时间戳参数 | |
| A0414 | 非法的时间戳参数 | |
| A0420 | 请求参数值超出允许的范围 | |
| A0421 | 参数格式不匹配 | |
| A0422 | 地址不在服务范围 | |
| A0423 | 时间不在服务范围 | |
| A0424 | 金额超出限制 | |
| A0425 | 数量超出限制 | |
| A0426 | 请求批量处理总个数超出限制 | |
| A0427 | 请求 JSON 解析失败 | |
| A0430 | 用户输入内容非法 | |
| A0431 | 包含违禁敏感词 | |
| A0432 | 图片包含违禁信息 | |
| A0433 | 文件侵犯版权 | |
| A0440 | 用户操作异常 | |
| A0441 | 用户支付超时 | |
| A0442 | 确认订单超时 | |
| A0443 | 订单已关闭 | |
| A0500 | 用户请求服务异常 | 二级宏观错误码 |
| A0501 | 请求次数超出限制 | |
| A0502 | 请求并发数超出限制 | |
| A0503 | 用户操作请等待 | |
| A0504 | WebSocket 连接异常 | |
| A0505 | WebSocket 连接断开 | |
| A0506 | 用户重复请求 | |
| A0600 | 用户资源异常 | 二级宏观错误码 |
| A0601 | 账户余额不足 | |
| A0602 | 用户磁盘空间不足 | |
| A0603 | 用户内存空间不足 | |
| A0604 | 用户 OSS 容量不足 | |
| A0605 | 用户配额已用光 | 蚂蚁森林浇水数或每天抽奖数 |
| A0700 | 用户上传文件异常 | 二级宏观错误码 |
| A0701 | 用户上传文件类型不匹配 | |
| A0702 | 用户上传文件太大 | |
| A0703 | 用户上传图片太大 | |
| A0704 | 用户上传视频太大 | |
| A0705 | 用户上传压缩文件太大 | |
| A0800 | 用户当前版本异常 | 二级宏观错误码 |
| A0801 | 用户安装版本与系统不匹配 | |
| A0802 | 用户安装版本过低 | |
| A0803 | 用户安装版本过高 | |
| A0804 | 用户安装版本已过期 | |
| A0805 | 用户 API 请求版本不匹配 | |
| A0806 | 用户 API 请求版本过高 | |
| A0807 | 用户 API 请求版本过低 | |
| A0900 | 用户隐私未授权 | 二级宏观错误码 |
| A0901 | 用户隐私未签署 | |
| A0902 | 用户摄像头未授权 | |
| A0903 | 用户相机未授权 | |
| A0904 | 用户图片库未授权 | |
| A0905 | 用户文件未授权 | |
| A0906 | 用户位置信息未授权 | |
| A0907 | 用户通讯录未授权 | |
| A1000 | 用户设备异常 | 二级宏观错误码 |
| A1001 | 用户相机异常 | |
| A1002 | 用户麦克风异常 | |
| A1003 | 用户听筒异常 | |
| A1004 | 用户扬声器异常 | |
| A1005 | 用户 GPS 定位异常 | |
| B0001 | 系统执行出错 | 一级宏观错误码 |
| B0100 | 系统执行超时 | 二级宏观错误码 |
| B0101 | 系统订单处理超时 | |
| B0200 | 系统容灾功能被触发 | 二级宏观错误码 |
| B0210 | 系统限流 | |
| B0220 | 系统功能降级 | |
| B0300 | 系统资源异常 | 二级宏观错误码 |
| B0310 | 系统资源耗尽 | |
| B0311 | 系统磁盘空间耗尽 | |
| B0312 | 系统内存耗尽 | |
| B0313 | 文件句柄耗尽 | |
| B0314 | 系统连接池耗尽 | |
| B0315 | 系统线程池耗尽 | |
| B0320 | 系统资源访问异常 | |
| B0321 | 系统读取磁盘文件失败 | |
| C0001 | 调用第三方服务出错 | 一级宏观错误码 |
| C0100 | 中间件服务出错 | 二级宏观错误码 |
| C0110 | RPC 服务出错 | |
| C0111 | RPC 服务未找到 | |
| C0112 | RPC 服务未注册 | |
| C0113 | 接口不存在 | |
| C0120 | 消息服务出错 | |
| C0121 | 消息投递出错 | |
| C0122 | 消息消费出错 | |
| C0123 | 消息订阅出错 | |
| C0124 | 消息分组未查到 | |
| C0130 | 缓存服务出错 | |
| C0131 | key 长度超过限制 | |
| C0132 | value 长度超过限制 | |
| C0133 | 存储容量已满 | |
| C0134 | 不支持的数据格式 | |
| C0140 | 配置服务出错 | |
| C0150 | 网络资源服务出错 | |
| C0151 | VPN 服务出错 | |
| C0152 | CDN 服务出错 | |
| C0153 | 域名解析服务出错 | |
| C0154 | 网关服务出错 | |
| C0200 | 第三方系统执行超时 | 二级宏观错误码 |
| C0210 | RPC 执行超时 | |
| C0220 | 消息投递超时 | |
| C0230 | 缓存服务超时 | |
| C0240 | 配置服务超时 | |
| C0250 | 数据库服务超时 | |
| C0300 | 数据库服务出错 | 二级宏观错误码 |
| C0311 | 表不存在 | |
| C0312 | 列不存在 | |
| C0321 | 多表关联中存在多个相同名称的列 | |
| C0331 | 数据库死锁 | |
| C0341 | 主键冲突 | |
| C0400 | 第三方容灾系统被触发 | 二级宏观错误码 |
| C0401 | 第三方系统限流 | |
| C0402 | 第三方功能降级 | |
| C0500 | 通知服务出错 | 二级宏观错误码 |
| C0501 | 短信提醒服务失败 | |
| C0502 | 语音提醒服务失败 | |
| C0503 | 邮件提醒服务失败 |
实现代码样例
使用编译时枚举类实现
简单,易用,不可自定义规则,系统编译时校验
ApiCodeByEnumerate.java
java@Getter public enum ApiCodeByEnumerate { // 正确执行 SUCCESS("00000", "正确执行", "Success"), // 用户端错误 一级宏观错误码 USER_SIDE_ERROR("A0001", "用户端错误", "User-side error"), // 用户注册错误 二级宏观错误码 USER_SIDE_REGISTER_ERROR("A0100", "用户注册错误 ", "User-side register error"), // 用户未同意隐私协议 USER_SIDE_REGISTER_NOT_AGREE_ARGEEMENT_ERROR("A0101", "用户未同意隐私协议 ", "User do not agree the Privacy Agreement"); private String code; private String msgZh; private String msgEn; ApiCodeByEnumerate(String code, String msgZh, String msgEn) { this.code = code; this.msgZh = msgZh; this.msgEn = msgEn; } }Main.java
javapublic class Main { public static void main(String[] args) { // 00000 System.out.println(ApiCodeByEnumerate.SUCCESS.getCode()); // 正确执行 System.out.println(ApiCodeByEnumerate.SUCCESS.getMsgZh()); // Success System.out.println(ApiCodeByEnumerate.SUCCESS.getMsgEn()); } }
使用运行时枚举类实现
[《Java 开发手册(泰山版)》定义了统一的错误码方案,是不是太理想化了? - Feego 的回答 - 知乎 https://www.zhihu.com/question/389789766/answer/1179593687 > feego-common/feego-common-web/feego-common-web/src/main/java/io/github/lvyahui8/web/code at master · lvyahui8/feego-common
复杂,有一定扩展性,可自定义规则,运行时校验
ApiCodeByAnnotation.java
javapublic interface ApiCodeByAnnotation { public enum General implements MsgCode { @ApiCode(code = "00000", msgZh = "成功", msgEn = "Success") SUCCESS, } @ApiCodePrefix({"A"}) public enum USER_SIDE implements MsgCode { // Leave 1 @ApiCode(code = "0001", msgZh = "用户端错误", msgEn = "User-side error") UNIVERSAL_ERROR, // Leave 2 @ApiCode(code = "0100", msgZh = "用户注册错误", msgEn = "User-side register error") REGISTER_ERROR, } @ApiCodePrefix({"A", "01"}) public enum USER_SIDE_REGISTER implements MsgCode { @ApiCode(code = "01", msgZh = "用户未同意隐私协议", msgEn = "User do not agree the Privacy Agreement") NOT_AGREE_ARGEEMENT_ERROR, } }Main.java
javapublic class Main { public static void main(String[] args) { // A0101 System.out.println(ApiCodeByAnnotation.USER_SIDE_REGISTER.NOT_AGREE_ARGEEMENT_ERROR.getCode()); // 用户未同意隐私协议 System.out.println(ApiCodeByAnnotation.USER_SIDE_REGISTER.NOT_AGREE_ARGEEMENT_ERROR.getMsgZh()); // User do not agree the Privacy Agreement System.out.println(ApiCodeByAnnotation.USER_SIDE_REGISTER.NOT_AGREE_ARGEEMENT_ERROR.getMsgEn()); } }
ApiCode.java (定义编码注解)
java@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface ApiCode { String code(); String msgZh(); String msgEn(); }ApiCodePrefix.java (定义编码前缀注解)
java@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface ApiCodePrefix { String[] value(); }Code.java (定义枚举包含字段)
java@Data class Code { private String code; private String msgZh; private String msgEn; }MsgCode.java (通用接口)
javapublic interface MsgCode { default String getCode() { return CodeRepository.get(this).getCode(); } default String getMsgZh() { return CodeRepository.get(this).getMsgZh(); } default String getMsgEn() { return CodeRepository.get(this).getMsgEn(); } }CodeRepository.java (核心注册类)
javaclass CodeRepository { private static final Map<Object, Code> CODE_MAP = new ConcurrentHashMap<>(); static Code get(Object object) { // 单例 if (CODE_MAP.containsKey(object)) { return CODE_MAP.get(object); } try { Enum<?> codeEnum = (Enum<?>) object; // 获取前缀注解 ApiCodePrefix apiCodePrefix = object.getClass().getAnnotation(ApiCodePrefix.class); // 获取注解 ApiCode apiCode = object.getClass().getField(codeEnum.name()).getAnnotation(ApiCode.class); Code codeInst = new Code(); // 拼接 Code StringBuffer codeStrBuf = new StringBuffer(); if (apiCodePrefix != null) { for (String item : apiCodePrefix.value()) { codeStrBuf.append(item); } } codeStrBuf.append(apiCode.code()); codeInst.setCode(codeStrBuf.toString()); // 设置提示文本 codeInst.setMsgZh(apiCode.msgZh()); codeInst.setMsgEn(apiCode.msgEn()); CODE_MAP.put(object, codeInst); return codeInst; } catch (NoSuchFieldException e) { throw new Error(e); } } }