errorCode.adoc 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. = 错误码规范
  2. =====
  3. :Shoulder: Specifications
  4. :Author: lym
  5. :Email: <cn_lym@foxmail.com>
  6. :Date: 2020
  7. :Revision: 1.0
  8. :doctype: book
  9. :revdate: {docdate}
  10. :sectanchors:
  11. :xrefstyle: full
  12. :anchor:
  13. :toc: left
  14. :toclevels: 3
  15. :sectnumlevels: 5
  16. // tag::main[]
  17. == 术语定义:
  18. * 错误码
  19. ** 特定的错误的标识,可用于自动化处理或或便于排查等。
  20. * 程序处理状态
  21. ** 成功
  22. *** 服务器理解请求的意图,并正确执行了该请求。
  23. ** 正常失败
  24. *** 请求或处理过程种与服务器所期待的状态不一致,且服务器知晓这类错误的原因(在编码时便考虑到了),拒绝执行了该请求。如:请求参数不合法、无权限等。这类错误通常需要提醒使用者修改输入。
  25. ** 异常失败
  26. *** 请求已被服务器接收理解,认为其合法,但在执行时未能正确处理请求,如:`NullPointException`、assert 失败等(在编码时未考虑到或认为不应该发生的错误)。这类错误运行时本不应该发生,需要维护者来排查。
  27. == 规范约束
  28. WARN 级别及以上日志中必须要记录错误码。
  29. === 错误码格式约束
  30. 错误码分段定义: `32` 位的无符号整型数字,由两部分构成:
  31. * 应用标识码,前 `16` 位:用于标识那个应用、服务的错误、其中 `0` 保留。
  32. * 错误类型码,后 `16` 位;用于标识该应用内错误类型
  33. 特殊的,错误码 `0` 表示成功(所有位为0)。
  34. === 错误码输出约束
  35. 错误码输出时:
  36. * 采用 `16` 进制
  37. * 全小写字符
  38. * 以 `0x` 开头
  39. * 长度为10个字符
  40. 例:`0x00aa0f01`:除了 `0x` 外,前 4 个字符标识了哪个应用出错,后 4 个字符标识具体错误类型。
  41. [TIP]
  42. ====
  43. 不对代码中错误码类型定义做限制,如可以是数字,也可以是字符串,还可以枚举等。
  44. ====
  45. === 错误类型标识码方案建议
  46. 错误类型标识码共16位,可以将前几位标识类型,后几位位标识具体错误,使得错误码更容易读,如前8位标识错误类别,后8位表示该类别下的具体错误。
  47. 分类举例:
  48. [cols="1,2", width="50%"]
  49. |===
  50. | 编码 | 分类
  51. | 0 | 公共错误
  52. | 1 | 认证错误
  53. | 2 | 系统错误
  54. | 3 | 中间件(如 数据库、MQ)错误
  55. | 4 | 标准协议(如 HTTP)错误
  56. | 5 | 参数错误
  57. | 6 | 服务异常
  58. | 7 | 网络错误(调用其他服务等)
  59. | 8 | 其他错误
  60. |===
  61. == 错误码翻译项
  62. 为了更好的用户体验,错误码通常需要对应的描述信息与建议,而在不同语言环境下,还需要不同的翻译内容,因此需要对应的多语言key。
  63. 为了方便建立错误码排查系统,需要规范这部分内容,推荐多语言相关翻译key定义如下:
  64. * 错误描述:`err.<错误码>.desc`
  65. * 排查建议:`err.<错误码>.sug`
  66. == 编码注意事项:接口全局异常处理建议
  67. [TIP]
  68. ====
  69. 建议不要到处无意义的 `try..catch`、`if null` 等,这样并不是所谓 *健壮性* 代码,反而大大降低代码的 *可读性* 。
  70. 大部分时候产生某个异常,`catch` 并不能做什么,单纯增加了代码量;而 null 如果允许则进行判断,否则应该检查为什么出现 null 而不是用 if 掩盖问题。,不应掩盖问题,而应让及时暴露问题。
  71. 提前进行异常分类,定义 `BaseRuntimeException` 做好全局异常处理,根据不同类型的异常,记录相应级别的日志,返回对应的状态码等。
  72. ====
  73. === 异常分类
  74. * 第三方 jar包中的 Runtime 类异常
  75. ** warn/error 级别记录。因为这是开发设计过程意料之外的错误(未捕获)。
  76. * [.line-through]需要记录 INFO 级别日志的异常,且 HTTP 状态码为 200 *(废弃)*
  77. ** [.line-through]参数正确,但当前时间或当前系统状态拒绝执行该操作。 *(废弃)*
  78. * 需要记录 INFO 级别日志的异常,且 HTTP 状态码为 400
  79. ** 异常发生在具体的业务场景,这类异常往往由使用者的输入直接或间接导致,因此需要让使用者知道错误产生原因与解决/避免该类错误的方法。
  80. ** 服务端记录 info 日志,返回 `400` HTTP 状态码, `Body` 可为 `{"code":"0x12345678", "msg":"用户名最长支持18个字符,当前%s个字符", "data":[20]}` 这种,UI层一般不展示错误码,而是根据错误码或msg来进行提示。
  81. ** 可以返回统一的参数缺失、参数格式错误不正确等对应的错误码,返回多语言key;也可以直接返回具体某个参数错误的错误码,调用方根据错误码翻译(可能会有大量的错误码产生)。
  82. * 场景举例:接口入参校验不通过,查询数据库中不存在的数据,不合法的枚举值等,
  83. * 需要记录 WARN 级别日志的异常,且 HTTP 状态码为 500
  84. * 需要记录 ERROR 级别日志的异常,且 HTTP 状态码为 500
  85. ** 基础通用异常: 一般不会发生的问题,一旦发生,通常需要人工排查和修复,抛出该异常代表服务器无法继续处理或完成业务处理,错误信息无法体现业务场景,这类异常不需要让用户知道细节和产生错误的原因。
  86. ** 需要记录 error 日志,返回 500 HTTP 状态码。UI层一般直接提示为服务器异常,请稍后再试(选择性展示错误码xxx,调用链xxx),可能需要触发日志错误码告警。
  87. ** 场景举例:json序列化异常、加解密异常、调用其他服务接口异常、数据库连接失败、
  88. ==== 异常对象属性与返回值对应关系
  89. * `code` (错误码)对应返回值的 `code`
  90. * `msg` 对应返回值的 `msg`
  91. * `param` 对应返回值的 `data`,一般用于多语言翻译填充
  92. ==== 异常日志级别与HTTP响应状态码分类
  93. *种类:*
  94. * [.line-through]INFO 200
  95. * INFO 400
  96. * WARN 500
  97. * ERROR 500
  98. *特殊:*
  99. * 401
  100. * 403
  101. * 404
  102. * 405
  103. ==== 日志、异常、错误码、HTTP 状态码关系
  104. * 异常后要记录日志,但通常情况下,异常后日志级别往往为 `WARN` 或以上,因此若将错误码绑定到异常中,使用者记录日志将大幅简化。代价:异常类依赖错误码。
  105. * 自定义异常往往需要错误码,但如果把这些异常全部定义出来,开发和维护成本都将升高,如果定义一个通用的异常类,只需要定义一些错误码,抛出时用错误码抛,使用者代码将大幅简化。代价:错误码依赖异常类。
  106. * 记录日志时,若抛出了异常,则只需要将异常放入记录即可,将大幅简化代码。代价:日志依赖异常类。
  107. * 记录日志时,若未抛出异常,但要记录错误码,如果使用 `log.xx(ErrorCode)` 这种方式,将大幅简化使用者的代码。代价:日志依赖错误码。
  108. * 若可以通过异常来确定返回的HTTP错误码,便可以通过全局异常处理来简化使用者的代码。代价:异常依赖 `HTTP响应状态码`
  109. * 缺点:可能限制使用者二次框架的开发。
  110. 出于以上原因,可以看到日志、异常、错误码这三者的相互耦合可以大幅简化使用者的书写方式,由于使用方式足够简化,且在一个系统中风格一般为统一,这样设计未见明显弊端,故可以这么设计。
  111. 做法:
  112. * 自定义日志接口
  113. * 自定义几种异常类(为了使用方便,采用运行时异常),也可以定义附带多个字段的单个基础异常类
  114. * 自定义错误码接口(可选,能简化使用)
  115. * 由于相互耦合需要将三者在一个模块内定义
  116. 简化日志的使用
  117. * 日志接口直接集成 `Sl4j` 的接口,以兼容主流日志框架
  118. * 可参考 `lombok`,将 `Sl4j` 转化为自己定义的日志 `Logger` 类,或新定义类似注解,注入自己定义的日志 `Logger` 类
  119. * 推荐使用者只关心错误码接口,其他自动化完成,减少上手难度
  120. ==== 代码中其他设计注意点
  121. * 由于一个应用内的所有错误码前缀都是相同的,可以在代码中不体现这部分,而是在输出时进行统一的拼接,节省程序内存也简化了维护和管理。
  122. == 错误码带来的效果(倒叙)
  123. 上面给出了结论和落地方式,这里来将为什么用错误码。
  124. === 错误码的好处
  125. 核心有两个思想 **索引**、**封装**
  126. 使用错误码的优势体现在下面几个方面:
  127. * 快速定位问题
  128. ** 无论是代码或者日志,直接搜索错误码是非常快速的
  129. * 利于自动化处理
  130. ** 根据日志中的错误码,进行自动化处理或告警
  131. ** 使得接口调用者可以根据错误码做出一定的处理,若只有提示信息,则不能很好的进行。
  132. * 降低沟通成本、减少信息在沟通、传递时的损耗
  133. ** 出错时,如果只根据错误信息描述,在沟通时可能出现偏差,在描述错误信息时可能会出现损耗而出现歧义或疏漏,但使用错误码不会。
  134. * 利于流程管理
  135. ** 可以根据错误码生产文档,可能产生的错误一目了然
  136. * 提高程序性能
  137. ** 由于错误码占用空间一般远小于错误提示信息占用空间,因此无论在网络传输或是程序处理时,效率远远高于错误提示信息
  138. * 利于版本迭代、升级
  139. ** 随着软件升级、版本的迭代,只要错误码还是不变的,即便修改错误描述信息,也不会引起混乱。
  140. * 利于差异化展示错误信息
  141. ** 不同的国家、地区、语言的用户,对于文字的偏好不同,使用错误码可以更轻松的针对用户的喜好修改提示信息。
  142. * 封装的思想
  143. ** 隐藏内部实现、敏感信息,降低软件边界的耦合
  144. * 利于代码维护
  145. ** 如果直接将提示信息写在代码中,一是增加代码体积;二是当软件功能增多时,维护成本将大大提升,而使用错误码时,维护成本增加的没那么快。
  146. ** 对于多人协作的开发模式而言,编码和交互可以由不同的人员专门负责,职责单一,专人办专事。
  147. ** 提醒调用者区分调用异常 / 业务异常
  148. === 错误码的代价
  149. 错误码的代价
  150. * 引入额外的开发成本
  151. ** 需要定义错误码,且不能与已有错误码重复
  152. ** 开发者需要使用错误码表达错误信息,就需要额外维护他们的映射关系以及思维上的转变。
  153. * 错误信息隐藏
  154. ** 无法直接从错误码中获取详细的信息,必须借助错误码文档等映射工具才能获取有用信息。
  155. 其实引入错误码的弊端是和好处对应的,在解决一些问题的同时也引入了另外的的问题。
  156. == 结论
  157. 虽然看到错误码的优势是远远大于弊端的,但对于个人开发者或者生命周期短的小型软件而言,上面的优势并不明显,直接输出提示则更好。
  158. == 如何管理错误码
  159. 当软件的功能越来越多时,他的成本也因此升高,因此需要更高效的使用错误码。
  160. === 规范错误码格式
  161. 按照一定格式将错误码的分类有利于减少错误码重复/冲突,方便定义错误码,方便快速定位问题。
  162. === 使用从左到右依次分类、细化
  163. 如HTTP协议中第一位是大类分类,剩下的是递增的子类型错误,如看到4xx便关注调用者的错误,看到5xx更关注服务提供者的错误。
  164. === 压缩错误码长度
  165. 大型软件中往往以十六进制串作为错误码,因为同样长度的16进制比10进制的数看起来更简短,更短即更快,无论是表达和转述或是存储。
  166. === 严格区分错误类型
  167. 调用者务必注意区分:业务失败,未知状态,禁止统一处理。
  168. * 若为失败:需要结合具体业务决定是否提供降速重试等机制;
  169. * 若为未知:需要先尝试通过查询获取当前状态,再根据业务决定调用取消或重试。
  170. === 示例
  171. * 最高一位固定0,使用数字数表示时永远为正数,表达式中没有负号只有数字,避免歧义
  172. * 第二位0表示系统意料之中的用户输入导致的异常,为1表示系统由于所依赖的基础设施(如网络、系统、其他软件)不能正确响应而导致的错误。
  173. * 第3-13位标识某一个应用
  174. * 剩余位数由应用内部定义,可以按照模块等划分
  175. 所有错误码都统一按照相同规范划分完毕后,一看到错误码,就可以立刻定位到是系统哪里发生了问题,然后便可以安排对应部分负责人排查。
  176. === 代码中如何选择错误码的类型
  177. 在一定规范前提下,C、C++这类基础的程序常常位于底层,与其他程序交互较少或作为提供者,与之交互的系统也通常位数字表达状态,因此使用数字类型更好,如int32比字符串或者枚举占用更少的内存。
  178. 而更上层的一些语言,经常与其他系统打交道,更适合用String以获取更好的兼容性,由于一个服务中不同错误码有大量的位重复,有的语言利用了字典树,反而可能更节省空间
  179. // end::main[]
  180. == 通用错误码举例
  181. [cols="1,1,2,4"]
  182. |====
  183. | 错误码 | 错误分类 | 错误原因 | 错误原因-英文
  184. | 0x | 参数错误 | 必填参数为空 | The required parameter %s is blank.
  185. | 0x | 参数错误 | 参数范围不正确 | The value of parameter %s is out of range.
  186. | 0x | 参数错误 | 参数格式不正确 | The format of parameter %s is not correct.
  187. | 0x | 参数错误 | 未指定分页大小或者分页过大导致返回报文过长 | Return message too long, please setting paging size.
  188. | 0x | 服务错误 | 服务性能已达上限 | Service performance reaches the upper limit.
  189. | 0x | 服务错误 | 服务异常 | Service error.
  190. | 0x | 服务错误 | 服务响应超时 | Service response timeout.
  191. | 0x | 服务错误 | 服务不可用 | Service unavailable.
  192. | 0x | 资源异常 | 资源访问未授权 | Resource unauthorized.
  193. | 0x | 资源异常 | 资源不存在 | Invalid resource.
  194. | 0x | 其他错误 | 其他未知错误 | Other error.
  195. |====