一、前言
1、设计“标准错误信息结构”的背景和意义
考虑到如下几个方面:
1)便于使用方(大众用户)知道错误的原因
2)便于使用方(程序 或 程序员)知道错误的原因
3)便于知道错误的原因,以及可能的排查和恢复措施
4)知道错误的类型,便于对其进行监控(分类和统计)或者 触发特定动作
设计策略:首先考虑2)3)4)。
针对于 面向 程序处理 或者 程序员使用的数据,跟 面向于 普通大众用户的数据,其错误码(code)和错误信息(msg)的设计可能是有区别的,因为对于普通大众用户,他们可能看不懂程序报错,而且直接给底层错误信息,影响用户体验。但是没关系,这些在用户端(UI层)都可以处理,从而提高用户体验的。
所以,我们这个规范,暂时不考虑面向一线普通用户,若要面向一线用户,需要客户端自行处理和包装。
错误可能有千千万万,该怎么分类?
很显然,错误的分类,一定是“多级分类”,那到底该有多少级?
首先,对于代码而言,基本上都会给错误类型取一个名字,例如 IOException。这可以看做是一级分类。
但是仅仅只有一级分类,还是不足以区分错误信息,例如仅仅知道 RpcException,而不清楚是 网络错误、请求超时、参数错误、业务方异常等类型,很难了解和统计真实情况。
然而,具体的错误如何分类,分多少层级,可能需要业务方自己去定义 。
综上分析,制定如下标准:标准的返回(Response)数据结构中,至少应该包含三个字段:
字段1(status):处理的结果(这个字段用于快速判断 处理结果是否成功,若失败,失败的最高一级类型是什么)
字段2(code):处理结果的二级分类(对应 处理失败的错误码,或者对于成功的进一步描述)。
字段3(msg):对处理结果的附加说明(对应 处理失败时的错误描述信息)
有时候,程序并不能简单的返回成功,而可能是“部分成功” 或者 “不知道是否最终成功”。当然,涉及到这种的业务场景很少,绝大部分的情况是要么成功、要么失败,所以我们习惯性的用true表示成功,false表示失败,或者0代表成功,非0代表失败。(这是使用习惯问题,也得考虑其中)
我的建议是,调用方优先只判断是否为“绝对的成功”(意味着服务端已经明确表示:请求已经按照API的定义得到了执行),然后再考虑其他“非绝对成功”的特殊情况。程序表示如下:
if (result.status==0) { // 请求已经被服务端成功执行!!! } else { // 其他情况都需要特殊处理 }
为了适配大众的习惯,定义 status 0~200 都代表成功,其中0和200代表“成功”(普通的成功),1~199则为保留字段,若有需要,业务方可以自行扩展。所以,status是一个 int32 字段,这样的字段类型处理性能比较好,而且可以方便的做“大于”、“小于”运算,例如 :
if (result.status >= 400 && result.status <500) { alter("客户端异常") }
对于第二个字段(code),主要用于对失败时,对错误进行分类。前面说到,分类可以是多级,所以这个code字段,为了灵活性,是string类型,而且其格式是相对自由的。下面给出了几种code的示例:
Windows错误代码风格:0x8007007B、0xc000000e
简单 纯数字 风格:780003、631001
唯一 短url字符串 风格:ea0fc、287c3
字母+数字 分类风格:E0001、C0002、S0017
时间戳 + 随机字符 组合风格:ivh32-1560232921
错误名称风格:FileNotFound、IllegalArguments、Unauthorized
对于程序员来说,其实什么风格无所谓,关键是:
1)能否知道错误的原因,以及可能的排查和恢复措施
2)根据错误类型,可以对其进行监控(分类和统计)或者 触发特定动作
一个错误代码,它不能替代错误详情,其设计应该是简洁易用的。我们约定,错误代码总长度不超过32。
下面是一种建议:
由于 status(int32)字段只能表示高层级的错误类型,所以 code(string)字段 务必要能定位到具体的异常信息,也就是说 code建议是 “唯一码”,或者是 非常细化的错误类型。可以这样设计:
status——最高一级的错误类型,code——最低一级(或二级)的错误类型。
当msg字段能够代表 最低一级的完整错误信息时,code可以作为倒数第二级的错误类型;当msg字段无法完整反映错误信息时,code务必作为最低一级的错误类型(能够直接定位到错误日志中完整的错误信息)。
1、返回(Result)数据由以下字段组成
名称 | 类型 | 含义 | 必须 | 备注 |
status | int32 | 处理成功与否的标志 | 是 | 200、400、500等,见后文 |
data | Object | 自定义返回数据 | 否 | 返回的数据主体,可为空 |
msg | String | 自定义返回消息 | 否 | 对返回结果的附加描述,长3k以内 |
code | String | 自定义返回码 | 否 | 处理结果的业务编码,长32以内 |
meta | Map<String,String> | 通用附加信息 | 否 | 通用附加信息,与具体业务无关 |
说明:
status字段
0~200—处理成功
400~499—客户端错误,
400——请求无法解析(参数错误、协议错误等)
401*——未认证(未经过身份验证)
402*——客户端错误(统称)
403*——被禁止(拒绝请求)
404*——未找到(没有服务响应该请求)
405*——请求方法不正确
408*——接收请求数据超时
500~599——服务端错误,
500——未预料的异常
501*——内部暂时没有实现该请求功能
502*、504*——作为网关或者代理工作的服务器尝试执行请求时,从上游服务器接收到无效的响应(504对应超时)。
503*——由于临时的服务器维护或者过载,服务器当前无法处理请求。这个状况是临时的,并且将在一段时间以后恢复。
600——未定义、未预料的异常(可能是客户端原因也可能是服务端原因)
备注:在没有更详细的一级分类标准之前,建议status取值只为200、400、500、600之一。
meta字段
可以根据平台需求增加“额外的meta信息”,通常meta信息被框架、中间件层使用(类似于HTTP的Header信息)。
2、请求(Request)数据由以下字段组成
名称 | 类型 | 含义 | 必须 | 备注 |
data | Object | 请求携带的数据主体 | 否 | 返回的数据主体,可为空 |
meta | Map<String, String> | 中间平台附加信息 | 否 | 通用附加信息,与具体业务无关 |
3、错误码(Error Code)设计实践
前文已经说了:
由于 status(int32)字段只能表示高层级的错误类型,所以 code(string)字段 务必要能定位到具体的异常信息,也就是说 code建议是 “唯一码”,或者是 非常细化的错误类型。“code代表最低一级(或二级)的错误类型” 。
所以,code需要由业务方自己定义。
实践参考:
通过工具类(例如Java的enum类),可以很方便的定义错误码,如下所示:
// status=200, errorCode = "0" SUCCESS(200, "0") // status=400, code="C0001", msg(en_US)="Arguments can not be empty: {}" PARAM_EMPTY(400, "C0001", null, "Arguments can not be empty: {}") PARAM_INVALID(400, "C0002", "无效的参数:{}", "Illegal arguments: {}") // 默认status=400,code=PASSWD_EMPTY PASSWD_EMPTY("密码不能为空") MENU_NAME_EMPTY(400, "菜单名称不能为空") // status=600,code=根据error msg和server id计算出来的唯一码 UNKNOWN_ERR(600, newServCode(msg), msg)
通过这样的方式,能事先定义好的错误信息,都有明确的error code,通过error code+msg,就能确定错误原因(不需要查看日志)。不能事先定义好的错误信息,则可以通过算法生成唯一的error code,通过该唯一code在日志中检索即可定位到详细的错误信息。举两个例子来说明:
例1,执行某Controller时,报了未知的RuntimeException,此时外层拦截器捕获后自动生成了唯一error code并记录到日志中,code的生成算法类似于:getCode(String msg, String serverInstanceId),其中msg来源于异常堆栈。
例2,调用Dubbo时发生了NETWORK Exception,此时错误类型是明确的,但是如果没有错误信息,也很难排查问题,所以,此时也需要生成唯一error code,只不过该error code可以将明确的错误类型作为前缀,code的生成算法类似于:getCode(String type, String msg, String serverInstanceId);其中type来源于异常类型,通常等于异常类的类名。