123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270 |
- = 错误码规范
- =====
- :Shoulder: Specifications
- :Author: lym
- :Email: <cn_lym@foxmail.com>
- :Date: 2020
- :Revision: 1.0
- :doctype: book
- :revdate: {docdate}
- :sectanchors:
- :xrefstyle: full
- :anchor:
- :toc: left
- :toclevels: 3
- :sectnumlevels: 5
- // tag::main[]
- == 术语定义:
- * 错误码
- ** 特定的错误的标识,可用于自动化处理或或便于排查等。
- * 程序处理状态
- ** 成功
- *** 服务器理解请求的意图,并正确执行了该请求。
- ** 正常失败
- *** 请求或处理过程种与服务器所期待的状态不一致,且服务器知晓这类错误的原因(在编码时便考虑到了),拒绝执行了该请求。如:请求参数不合法、无权限等。这类错误通常需要提醒使用者修改输入。
- ** 异常失败
- *** 请求已被服务器接收理解,认为其合法,但在执行时未能正确处理请求,如:`NullPointException`、assert 失败等(在编码时未考虑到或认为不应该发生的错误)。这类错误运行时本不应该发生,需要维护者来排查。
- == 规范约束
- WARN 级别及以上日志中必须要记录错误码。
- === 错误码格式约束
- 错误码分段定义: `32` 位的无符号整型数字,由两部分构成:
- * 应用标识码,前 `16` 位:用于标识那个应用、服务的错误、其中 `0` 保留。
- * 错误类型码,后 `16` 位;用于标识该应用内错误类型
- 特殊的,错误码 `0` 表示成功(所有位为0)。
- === 错误码输出约束
- 错误码输出时:
- * 采用 `16` 进制
- * 全小写字符
- * 以 `0x` 开头
- * 长度为10个字符
- 例:`0x00aa0f01`:除了 `0x` 外,前 4 个字符标识了哪个应用出错,后 4 个字符标识具体错误类型。
- [TIP]
- ====
- 不对代码中错误码类型定义做限制,如可以是数字,也可以是字符串,还可以枚举等。
- ====
- === 错误类型标识码方案建议
- 错误类型标识码共16位,可以将前几位标识类型,后几位位标识具体错误,使得错误码更容易读,如前8位标识错误类别,后8位表示该类别下的具体错误。
- 分类举例:
- [cols="1,2", width="50%"]
- |===
- | 编码 | 分类
- | 0 | 公共错误
- | 1 | 认证错误
- | 2 | 系统错误
- | 3 | 中间件(如 数据库、MQ)错误
- | 4 | 标准协议(如 HTTP)错误
- | 5 | 参数错误
- | 6 | 服务异常
- | 7 | 网络错误(调用其他服务等)
- | 8 | 其他错误
- |===
- == 错误码翻译项
- 为了更好的用户体验,错误码通常需要对应的描述信息与建议,而在不同语言环境下,还需要不同的翻译内容,因此需要对应的多语言key。
- 为了方便建立错误码排查系统,需要规范这部分内容,推荐多语言相关翻译key定义如下:
- * 错误描述:`err.<错误码>.desc`
- * 排查建议:`err.<错误码>.sug`
- == 编码注意事项:接口全局异常处理建议
- [TIP]
- ====
- 建议不要到处无意义的 `try..catch`、`if null` 等,这样并不是所谓 *健壮性* 代码,反而大大降低代码的 *可读性* 。
- 大部分时候产生某个异常,`catch` 并不能做什么,单纯增加了代码量;而 null 如果允许则进行判断,否则应该检查为什么出现 null 而不是用 if 掩盖问题。,不应掩盖问题,而应让及时暴露问题。
- 提前进行异常分类,定义 `BaseRuntimeException` 做好全局异常处理,根据不同类型的异常,记录相应级别的日志,返回对应的状态码等。
- ====
- === 异常分类
- * 第三方 jar包中的 Runtime 类异常
- ** warn/error 级别记录。因为这是开发设计过程意料之外的错误(未捕获)。
- * [.line-through]需要记录 INFO 级别日志的异常,且 HTTP 状态码为 200 *(废弃)*
- ** [.line-through]参数正确,但当前时间或当前系统状态拒绝执行该操作。 *(废弃)*
- * 需要记录 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序列化异常、加解密异常、调用其他服务接口异常、数据库连接失败、
- ==== 异常对象属性与返回值对应关系
- * `code` (错误码)对应返回值的 `code`
- * `msg` 对应返回值的 `msg`
- * `param` 对应返回值的 `data`,一般用于多语言翻译填充
- ==== 异常日志级别与HTTP响应状态码分类
- *种类:*
- * [.line-through]INFO 200
- * INFO 400
- * WARN 500
- * ERROR 500
- *特殊:*
- * 401
- * 403
- * 404
- * 405
- ==== 日志、异常、错误码、HTTP 状态码关系
- * 异常后要记录日志,但通常情况下,异常后日志级别往往为 `WARN` 或以上,因此若将错误码绑定到异常中,使用者记录日志将大幅简化。代价:异常类依赖错误码。
- * 自定义异常往往需要错误码,但如果把这些异常全部定义出来,开发和维护成本都将升高,如果定义一个通用的异常类,只需要定义一些错误码,抛出时用错误码抛,使用者代码将大幅简化。代价:错误码依赖异常类。
- * 记录日志时,若抛出了异常,则只需要将异常放入记录即可,将大幅简化代码。代价:日志依赖异常类。
- * 记录日志时,若未抛出异常,但要记录错误码,如果使用 `log.xx(ErrorCode)` 这种方式,将大幅简化使用者的代码。代价:日志依赖错误码。
- * 若可以通过异常来确定返回的HTTP错误码,便可以通过全局异常处理来简化使用者的代码。代价:异常依赖 `HTTP响应状态码`
- * 缺点:可能限制使用者二次框架的开发。
- 出于以上原因,可以看到日志、异常、错误码这三者的相互耦合可以大幅简化使用者的书写方式,由于使用方式足够简化,且在一个系统中风格一般为统一,这样设计未见明显弊端,故可以这么设计。
- 做法:
- * 自定义日志接口
- * 自定义几种异常类(为了使用方便,采用运行时异常),也可以定义附带多个字段的单个基础异常类
- * 自定义错误码接口(可选,能简化使用)
- * 由于相互耦合需要将三者在一个模块内定义
- 简化日志的使用
- * 日志接口直接集成 `Sl4j` 的接口,以兼容主流日志框架
- * 可参考 `lombok`,将 `Sl4j` 转化为自己定义的日志 `Logger` 类,或新定义类似注解,注入自己定义的日志 `Logger` 类
- * 推荐使用者只关心错误码接口,其他自动化完成,减少上手难度
- ==== 代码中其他设计注意点
- * 由于一个应用内的所有错误码前缀都是相同的,可以在代码中不体现这部分,而是在输出时进行统一的拼接,节省程序内存也简化了维护和管理。
- == 错误码带来的效果(倒叙)
- 上面给出了结论和落地方式,这里来将为什么用错误码。
- === 错误码的好处
- 核心有两个思想 **索引**、**封装**
- 使用错误码的优势体现在下面几个方面:
- * 快速定位问题
- ** 无论是代码或者日志,直接搜索错误码是非常快速的
- * 利于自动化处理
- ** 根据日志中的错误码,进行自动化处理或告警
- ** 使得接口调用者可以根据错误码做出一定的处理,若只有提示信息,则不能很好的进行。
- * 降低沟通成本、减少信息在沟通、传递时的损耗
- ** 出错时,如果只根据错误信息描述,在沟通时可能出现偏差,在描述错误信息时可能会出现损耗而出现歧义或疏漏,但使用错误码不会。
- * 利于流程管理
- ** 可以根据错误码生产文档,可能产生的错误一目了然
- * 提高程序性能
- ** 由于错误码占用空间一般远小于错误提示信息占用空间,因此无论在网络传输或是程序处理时,效率远远高于错误提示信息
- * 利于版本迭代、升级
- ** 随着软件升级、版本的迭代,只要错误码还是不变的,即便修改错误描述信息,也不会引起混乱。
- * 利于差异化展示错误信息
- ** 不同的国家、地区、语言的用户,对于文字的偏好不同,使用错误码可以更轻松的针对用户的喜好修改提示信息。
- * 封装的思想
- ** 隐藏内部实现、敏感信息,降低软件边界的耦合
- * 利于代码维护
- ** 如果直接将提示信息写在代码中,一是增加代码体积;二是当软件功能增多时,维护成本将大大提升,而使用错误码时,维护成本增加的没那么快。
- ** 对于多人协作的开发模式而言,编码和交互可以由不同的人员专门负责,职责单一,专人办专事。
- ** 提醒调用者区分调用异常 / 业务异常
- === 错误码的代价
- 错误码的代价
- * 引入额外的开发成本
- ** 需要定义错误码,且不能与已有错误码重复
- ** 开发者需要使用错误码表达错误信息,就需要额外维护他们的映射关系以及思维上的转变。
- * 错误信息隐藏
- ** 无法直接从错误码中获取详细的信息,必须借助错误码文档等映射工具才能获取有用信息。
- 其实引入错误码的弊端是和好处对应的,在解决一些问题的同时也引入了另外的的问题。
- == 结论
- 虽然看到错误码的优势是远远大于弊端的,但对于个人开发者或者生命周期短的小型软件而言,上面的优势并不明显,直接输出提示则更好。
- == 如何管理错误码
- 当软件的功能越来越多时,他的成本也因此升高,因此需要更高效的使用错误码。
- === 规范错误码格式
- 按照一定格式将错误码的分类有利于减少错误码重复/冲突,方便定义错误码,方便快速定位问题。
- === 使用从左到右依次分类、细化
- 如HTTP协议中第一位是大类分类,剩下的是递增的子类型错误,如看到4xx便关注调用者的错误,看到5xx更关注服务提供者的错误。
- === 压缩错误码长度
- 大型软件中往往以十六进制串作为错误码,因为同样长度的16进制比10进制的数看起来更简短,更短即更快,无论是表达和转述或是存储。
- === 严格区分错误类型
- 调用者务必注意区分:业务失败,未知状态,禁止统一处理。
- * 若为失败:需要结合具体业务决定是否提供降速重试等机制;
- * 若为未知:需要先尝试通过查询获取当前状态,再根据业务决定调用取消或重试。
- === 示例
- * 最高一位固定0,使用数字数表示时永远为正数,表达式中没有负号只有数字,避免歧义
- * 第二位0表示系统意料之中的用户输入导致的异常,为1表示系统由于所依赖的基础设施(如网络、系统、其他软件)不能正确响应而导致的错误。
- * 第3-13位标识某一个应用
- * 剩余位数由应用内部定义,可以按照模块等划分
- 所有错误码都统一按照相同规范划分完毕后,一看到错误码,就可以立刻定位到是系统哪里发生了问题,然后便可以安排对应部分负责人排查。
- === 代码中如何选择错误码的类型
- 在一定规范前提下,C、C++这类基础的程序常常位于底层,与其他程序交互较少或作为提供者,与之交互的系统也通常位数字表达状态,因此使用数字类型更好,如int32比字符串或者枚举占用更少的内存。
- 而更上层的一些语言,经常与其他系统打交道,更适合用String以获取更好的兼容性,由于一个服务中不同错误码有大量的位重复,有的语言利用了字典树,反而可能更节省空间
- // end::main[]
- == 通用错误码举例
- [cols="1,1,2,4"]
- |====
- | 错误码 | 错误分类 | 错误原因 | 错误原因-英文
- | 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.
- |====
|