📌 术语定义
- 错误码
-
特定的错误的标识,可用于自动化处理或或便于排查等。
- 程序处理状态
-
-
成功(Success): 服务器理解请求的意图,并正确执行了该请求。
-
正常失败(Failure): 请求或处理过程种与服务器所期待的状态不一致,且服务器知晓这类错误的原因(在编码时便考虑到了),拒绝执行了该请求。如:请求参数不合法、无权限等。这类错误通常需要提醒使用者修改输入。
-
异常失败(Unknown): 请求已被服务器接收理解,认为其合法,但在执行时未能正确处理请求,如:
NullPointException
、assert 失败等(在编码时未考虑到或认为不应该发生的错误)。这类错误运行时本不应该发生,需要维护者来排查。
-
🔗 规范约束
🌍 多语言 & 国际化
为了更好的用户体验,错误码通常需要对应的描述信息与建议,而在不同语言环境下,还需要不同的翻译内容,因此需要对应的多语言key。 为了方便建立错误码排查系统,需要规范这部分内容,推荐多语言相关翻译key定义如下:
-
错误描述:
err.<错误码>.desc
-
排查建议:
err.<错误码>.sug
💰 价值与成本
上面给出了结论和落地方式,这里来将为什么用错误码。
错误码的好处
核心有两个思想 索引、封装
使用错误码的优势体现在下面几个方面:
-
快速定位问题
-
无论是代码或者日志,直接搜索错误码是非常快速的
-
-
利于自动化处理
-
根据日志中的错误码,进行自动化处理或告警
-
使得接口调用者可以根据错误码做出一定的处理,若只有提示信息,则不能很好的进行。
-
-
降低沟通成本、减少信息在沟通、传递时的损耗
-
出错时,如果只根据错误信息描述,在沟通时可能出现偏差,在描述错误信息时可能会出现损耗而出现歧义或疏漏,但使用错误码不会。
-
-
利于流程管理
-
可以根据错误码生产文档,可能产生的错误一目了然
-
-
提高程序性能
-
由于错误码占用空间一般远小于错误提示信息占用空间,因此无论在网络传输或是程序处理时,效率远远高于错误提示信息
-
-
利于版本迭代、升级
-
随着软件升级、版本的迭代,只要错误码还是不变的,即便修改错误描述信息,也不会引起混乱。
-
-
利于差异化展示错误信息
-
不同的国家、地区、语言的用户,对于文字的偏好不同,使用错误码可以更轻松的针对用户的喜好修改提示信息。
-
-
封装的思想
-
隐藏内部实现、敏感信息,降低软件边界的耦合
-
-
利于代码维护
-
如果直接将提示信息写在代码中,一是增加代码体积;二是当软件功能增多时,维护成本将大大提升,而使用错误码时,维护成本增加的没那么快。
-
对于多人协作的开发模式而言,编码和交互可以由不同的人员专门负责,职责单一,专人办专事。
-
提醒调用者区分调用异常 / 业务异常
-
📊 管理错误码
当软件的功能越来越多时,他的成本也因此升高,因此需要更高效的使用错误码。 === 规范错误码格式 按照一定格式将错误码的分类有利于减少错误码重复/冲突,方便定义错误码,方便快速定位问题。
严格区分错误类型
调用者务必注意区分:业务失败,未知状态,禁止统一处理。
-
若为失败:需要结合具体业务决定是否提供降速重试等机制;
-
若为未知:需要先尝试通过查询获取当前状态,再根据业务决定调用取消或重试。
🔍 设计细节
接口全局异常处理
建议不要到处无意义的 大部分时候产生某个异常, 提前进行异常分类,定义 |
异常分类
-
第三方 jar包中的 Runtime 类异常
-
warn/error 级别记录。因为这是开发设计过程意料之外的错误(未捕获)。
-
-
需要记录 INFO 级别日志的异常,且 HTTP 状态码为 200 (废弃)
-
参数正确,但当前时间或当前系统状态拒绝执行该操作。 (废弃)
-
-
需要记录 INFO 级别日志的异常,且 HTTP 状态码为 400
-
异常发生在具体的业务场景,这类异常往往由使用者的输入直接或间接导致,因此需要让使用者知道错误产生原因与解决/避免该类错误的方法。
-
服务端记录 info 日志,返回
400
HTTP 状态码,Body
可为{"code":"0x12345678", "msg":"用户名最长支持18个字符,当前%s个字符", "data":[20]}
这种,UI层一般不展示错误码,而是根据错误码或msg来进行提示。 -
可以返回统一的参数缺失、参数格式错误不正确等对应的错误码,返回多语言key;也可以直接返回具体某个参数错误的错误码,调用方根据错误码翻译(可能会有大量的错误码产生)。
-
-
场景举例:接口入参校验不通过,查询数据库中不存在的数据,不合法的枚举值等,
-
需要记录 WARN 级别日志的异常,且 HTTP 状态码为 500
-
需要记录 ERROR 级别日志的异常,且 HTTP 状态码为 500
-
基础通用异常: 一般不会发生的问题,一旦发生,通常需要人工排查和修复,抛出该异常代表服务器无法继续处理或完成业务处理,错误信息无法体现业务场景,这类异常不需要让用户知道细节和产生错误的原因。
-
需要记录 error 日志,返回 500 HTTP 状态码。UI层一般直接提示为服务器异常,请稍后再试(选择性展示错误码xxx,调用链xxx),可能需要触发日志错误码告警。
-
场景举例:json序列化异常、加解密异常、调用其他服务接口异常、数据库连接失败、
-
日志、异常、错误码、HTTP 状态码关系
-
异常后要记录日志,但通常情况下,异常后日志级别往往为
WARN
或以上,因此若将错误码绑定到异常中,使用者记录日志将大幅简化。代价:异常类依赖错误码。 -
自定义异常往往需要错误码,但如果把这些异常全部定义出来,开发和维护成本都将升高,如果定义一个通用的异常类,只需要定义一些错误码,抛出时用错误码抛,使用者代码将大幅简化。代价:错误码依赖异常类。
-
记录日志时,若抛出了异常,则只需要将异常放入记录即可,将大幅简化代码。代价:日志依赖异常类。
-
记录日志时,若未抛出异常,但要记录错误码,如果使用
log.xx(ErrorCode)
这种方式,将大幅简化使用者的代码。代价:日志依赖错误码。 -
若可以通过异常来确定返回的HTTP错误码,便可以通过全局异常处理来简化使用者的代码。代价:异常依赖
HTTP响应状态码
-
缺点:可能限制使用者二次框架的开发。
出于以上原因,可以看到日志、异常、错误码这三者的相互耦合可以大幅简化使用者的书写方式,由于使用方式足够简化,且在一个系统中风格一般为统一,这样设计未见明显弊端,故可以这么设计。
做法:
-
自定义日志接口
-
自定义几种异常类(为了使用方便,采用运行时异常),也可以定义附带多个字段的单个基础异常类
-
自定义错误码接口(可选,能简化使用)
-
由于相互耦合需要将三者在一个模块内定义
简化日志的使用
-
日志接口直接集成
Sl4j
的接口,以兼容主流日志框架 -
可参考
lombok
,将Sl4j
转化为自己定义的日志Logger
类,或新定义类似注解,注入自己定义的日志Logger
类 -
推荐使用者只关心错误码接口,其他自动化完成,减少上手难度
📋 通用错误码列举
错误码 | 错误分类 | 错误原因 | 错误原因-英文 |
---|---|---|---|
0x |
参数错误 |
必填参数为空 |
The required parameter %s is blank. |
0x |
参数错误 |
参数范围不正确 |
The value of parameter %s is out of range. |
0x |
参数错误 |
参数格式不正确 |
The format of parameter %s is not correct. |
0x |
参数错误 |
未指定分页大小或者分页过大导致返回报文过长 |
Return message too long, please setting paging size. |
0x |
服务错误 |
服务性能已达上限 |
Service performance reaches the upper limit. |
0x |
服务错误 |
服务异常 |
Service error. |
0x |
服务错误 |
服务响应超时 |
Service response timeout. |
0x |
服务错误 |
服务不可用 |
Service unavailable. |
0x |
资源异常 |
资源访问未授权 |
Resource unauthorized. |
0x |
资源异常 |
资源不存在 |
Invalid resource. |
0x |
其他错误 |
其他未知错误 |
Other error. |