📌 术语定义

错误码

特定的错误的标识,可用于自动化处理或或便于排查等。

程序处理状态
  1. 成功(Success): 服务器理解请求的意图,并正确执行了该请求。

  2. 正常失败(Failure): 请求或处理过程种与服务器所期待的状态不一致,且服务器知晓这类错误的原因(在编码时便考虑到了),拒绝执行了该请求。如:请求参数不合法、无权限等。这类错误通常需要提醒使用者修改输入。

  3. 异常失败(Unknown): 请求已被服务器接收理解,认为其合法,但在执行时未能正确处理请求,如:NullPointException、assert 失败等(在编码时未考虑到或认为不应该发生的错误)。这类错误运行时本不应该发生,需要维护者来排查。

🔗 规范约束

WARN 级别及以上日志中必须要记录错误码。

错误码格式约束

错误码分段定义: 32 位的无符号整型数字,由两部分构成:

  • 应用标识码,前 16 位:用于标识那个应用、服务的错误、其中 0 保留。

  • 错误类型码,后 16 位;用于标识该应用内错误类型

特殊的,错误码 0 表示成功(所有位为0)。

错误码输出约束

错误码输出时:

  • 采用 16 进制

  • 全小写字符

  • 0x 开头

  • 长度为10个字符

例:0x00aa0f01:除了 0x 外,前 4 个字符标识了哪个应用出错,后 4 个字符标识具体错误类型。

不对代码中错误码类型定义做限制,如可以是数字,也可以是字符串,还可以枚举等。

错误类型标识码方案建议

错误类型标识码共16位,可以将前几位标识类型,后几位位标识具体错误,使得错误码更容易读,如前8位标识错误类别,后8位表示该类别下的具体错误。

分类举例:

编码

分类

0

公共错误

1

认证错误

2

系统错误

3

中间件(如 数据库、MQ)错误

4

标准协议(如 HTTP)错误

5

参数错误

6

服务异常

7

网络错误(调用其他服务等)

8

其他错误

🌍 多语言 & 国际化

为了更好的用户体验,错误码通常需要对应的描述信息与建议,而在不同语言环境下,还需要不同的翻译内容,因此需要对应的多语言key。 为了方便建立错误码排查系统,需要规范这部分内容,推荐多语言相关翻译key定义如下:

  • 错误描述:err.<错误码>.desc

  • 排查建议:err.<错误码>.sug

💰 价值与成本

上面给出了结论和落地方式,这里来将为什么用错误码。

错误码的好处

核心有两个思想 索引封装

使用错误码的优势体现在下面几个方面:

  • 快速定位问题

    • 无论是代码或者日志,直接搜索错误码是非常快速的

  • 利于自动化处理

    • 根据日志中的错误码,进行自动化处理或告警

    • 使得接口调用者可以根据错误码做出一定的处理,若只有提示信息,则不能很好的进行。

  • 降低沟通成本、减少信息在沟通、传递时的损耗

    • 出错时,如果只根据错误信息描述,在沟通时可能出现偏差,在描述错误信息时可能会出现损耗而出现歧义或疏漏,但使用错误码不会。

  • 利于流程管理

    • 可以根据错误码生产文档,可能产生的错误一目了然

  • 提高程序性能

    • 由于错误码占用空间一般远小于错误提示信息占用空间,因此无论在网络传输或是程序处理时,效率远远高于错误提示信息

  • 利于版本迭代、升级

    • 随着软件升级、版本的迭代,只要错误码还是不变的,即便修改错误描述信息,也不会引起混乱。

  • 利于差异化展示错误信息

    • 不同的国家、地区、语言的用户,对于文字的偏好不同,使用错误码可以更轻松的针对用户的喜好修改提示信息。

  • 封装的思想

    • 隐藏内部实现、敏感信息,降低软件边界的耦合

  • 利于代码维护

    • 如果直接将提示信息写在代码中,一是增加代码体积;二是当软件功能增多时,维护成本将大大提升,而使用错误码时,维护成本增加的没那么快。

    • 对于多人协作的开发模式而言,编码和交互可以由不同的人员专门负责,职责单一,专人办专事。

    • 提醒调用者区分调用异常 / 业务异常

错误码的代价

错误码的代价

  • 引入额外的开发成本

    • 需要定义错误码,且不能与已有错误码重复

    • 开发者需要使用错误码表达错误信息,就需要额外维护他们的映射关系以及思维上的转变。

  • 错误信息隐藏

    • 无法直接从错误码中获取详细的信息,必须借助错误码文档等映射工具才能获取有用信息。

其实引入错误码的弊端是和好处对应的,在解决一些问题的同时也引入了另外的的问题。

结论

虽然看到错误码的优势是远远大于弊端的,但对于个人开发者或者生命周期短的小型软件而言,上面的优势并不明显,直接输出提示则更好。

📊 管理错误码

当软件的功能越来越多时,他的成本也因此升高,因此需要更高效的使用错误码。 === 规范错误码格式 按照一定格式将错误码的分类有利于减少错误码重复/冲突,方便定义错误码,方便快速定位问题。

使用从左到右依次分类、细化

如HTTP协议中第一位是大类分类,剩下的是递增的子类型错误,如看到4xx便关注调用者的错误,看到5xx更关注服务提供者的错误。

压缩错误码长度

大型软件中往往以十六进制串作为错误码,因为同样长度的16进制比10进制的数看起来更简短,更短即更快,无论是表达和转述或是存储。

严格区分错误类型

调用者务必注意区分:业务失败,未知状态,禁止统一处理。

  • 若为失败:需要结合具体业务决定是否提供降速重试等机制;

  • 若为未知:需要先尝试通过查询获取当前状态,再根据业务决定调用取消或重试。

示例

  • 最高一位固定0,使用数字数表示时永远为正数,表达式中没有负号只有数字,避免歧义

  • 第二位0表示系统意料之中的用户输入导致的异常,为1表示系统由于所依赖的基础设施(如网络、系统、其他软件)不能正确响应而导致的错误。

  • 第3-13位标识某一个应用

  • 剩余位数由应用内部定义,可以按照模块等划分

所有错误码都统一按照相同规范划分完毕后,一看到错误码,就可以立刻定位到是系统哪里发生了问题,然后便可以安排对应部分负责人排查。

代码中如何选择错误码的类型

在一定规范前提下,C、C++这类基础的程序常常位于底层,与其他程序交互较少或作为提供者,与之交互的系统也通常位数字表达状态,因此使用数字类型更好,如int32比字符串或者枚举占用更少的内存。

而更上层的一些语言,经常与其他系统打交道,更适合用String以获取更好的兼容性,由于一个服务中不同错误码有大量的位重复,有的语言利用了字典树,反而可能更节省空间

🔍 设计细节

接口全局异常处理

建议不要到处无意义的 try..catchif null 等,这样并不是所谓 健壮性 代码,反而大大降低代码的 可读性

大部分时候产生某个异常,catch 并不能做什么,单纯增加了代码量;而 null 如果允许则进行判断,否则应该检查为什么出现 null 而不是用 if 掩盖问题。,不应掩盖问题,而应让及时暴露问题。

提前进行异常分类,定义 BaseRuntimeException 做好全局异常处理,根据不同类型的异常,记录相应级别的日志,返回对应的状态码等。

异常分类

  • 第三方 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序列化异常、加解密异常、调用其他服务接口异常、数据库连接失败、

异常对象属性与返回值对应关系

  • code (错误码)对应返回值的 code

  • msg 对应返回值的 msg

  • param 对应返回值的 data,一般用于多语言翻译填充

异常日志级别与HTTP响应状态码分类

种类:

  • INFO 200

  • INFO 400

  • WARN 500

  • ERROR 500

特殊:

  • 401

  • 403

  • 404

  • 405

日志、异常、错误码、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.