服务异常和错误码规范
背景
目前公司的服务端返回的异常不规范,导致接口各个场景返回的异常不统一,使得项目不好维护。并且代码看起来不优雅,目前公司项目遇到了国际化的问题,服务端的异常也需要做出相关调整。所以对服务端的异常错误码制定一个规范。
思考
- 需要制定那些异常?
- 错误码如何设计?如何做到可读性、可维护、灵活可控?
- 新的异常规范,如何设计才会对当前项目侵入性小?
异常分类
根据之前的工作经验,异常分为两个大分类,未知异常、已知异常,结合这两个分类对异常进行拆分,直到拆分到不能拆分为止。拆分结果如下:
异常拆分一定要结合整体服务业务,有些公司服务的异常可能其他公司永远都用不上。比如电商的项目有些异常场景可能做crm系统的永远用不上。所以不要盲目借鉴。
异常介绍
- 未知异常
未知异常指的是服务端不可感知和控制的异常,如引入的jar包中抛出的异常,或者数据库链接异常,或者代码bug出现的空指针异常和badsql,这些异常是服务端可能出现的情况,上面列举的几个场景也确实是服务器异常,所以针对这种情况,服务端接口返回的http状态码是500 无需将相关异常栈信息返回,增加代码安全性。
这里指的安全性是比如写了一个badsql 如果把异常栈抛出去则会导致数据表泄漏
- 已知异常
已知异常是服务端知道可能出现异常的场景,目前拆分为前置异常、外部异常、自定义异常,对于这些异常http状态码是200,并且可以返回响应的错误信息,具体描述如下
- 前置异常:指的是代码还没有到真正业务逻辑前抛出的异常,比如参数校验失败异常、用户没有权限异常、token过期异常、未找到资源异常 等等,
- 外部异常:指的是因为外部因素导致的接口异常而非正常业务逻辑错误或者数据错误导致的,比如第三方服务商异常、接口被限流、接口重复请求
- 自定义异常:指的是程序员自己抛出来的异常,这种异常是因为代码在相关业务逻辑中,不满足业务条件并且必须手动抛出的异常,比如:子表记录找不到对应父表的记录,这种正常逻辑是不可能存在场景,是需要手动抛出异常进行排查的并且这种问题你也没办法兼容。或者举行一个秒杀活动,在活动未开始之前就有请求到服务端了,这是不现实的情况,并且于这种场景需要以来服务端的响应作出对应的展示文案,所有服务端需要自定义相关的异常返回,前段解析相关的错误码作出相关的文案展示。对于这种异常称作自定义异常。
错误码的设计
错误码的作用
- 程序员根据错误码能够快速定位问题
- 通过不同的错误码可以区分出是什么原因导致的问题(传参错误,没有权限,服务异常)
- 明确清晰的展示错误信息,方便程序员快速定位问题
错误码的定义
- 数字类型error_code标识
- 长度可控,节省传输带宽
- 可读性差,需要相关文档
- 数字范围分段,表示不通分类,不同业务的异常
- 字符串类型的error_code标识
1 2 3 4 5 6
| { 'error_code':401, 'error_str_code':'UnAuthorized', 'error_msg':'user un Authorized: user_id:{xxx}', 'error_reason':'用户未授权' }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| public interface ErrorCodeInterface { Integer getErrorCode(); String getStrErrorCode(); String getMsg();
String getReason(); } @AllArgsConstructor public enum xxxxErrorCode implements ErrorCodeInterface{ ENCODE_SDK_LICENSE_OVERDUE(1001, "ActivityNotStarted", "Current Activity not started","当前活动暂未开始"), Integer errorCode; String strErrorCode; String msg; String reason ; @Override public Integer getErrorCode() { return errorCode; } @Override public String getMsg() { return msg; }
@Override public String getStrMsg() { return strMsg; } @Override public String getReason() { return reason; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| public class DataSimbaRuntimeException extends RuntimeException { private String reason;
public DataSimbaRuntimeException(String msg) { this(msg, ""); }
public DataSimbaRuntimeException(String msg, Throwable cause) { this(msg, "", cause); }
public DataSimbaRuntimeException(String msg, String reason) { super(msg); this.reason = reason; }
public DataSimbaRuntimeException(String msg, String reason, Throwable cause) { super(msg, cause); this.reason = reason; }
public String getReason() { return this.reason == null ? "" : this.reason; }
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| /*** * 请求校验异常 */ public class RequestValidateException extends DataSimbaRuntimeException {
public RequestValidateException(String msg, Throwable cause) { super(msg, cause); }
public RequestValidateException(String msg) { super(msg); }
public RequestValidateException(String msg, String reason) { super(msg, reason); }
public RequestValidateException(String msg, String reason, Throwable cause) { super(msg, reason, cause); }
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| /*** * 没有权限异常 */ public class UnAuthorizedException extends DataSimbaRuntimeException {
public UnAuthorizedException(String msg, Throwable cause) { super(msg, cause); }
public UnAuthorizedException(String msg) { super(msg); }
public UnAuthorizedException(String msg, String reason) { super(msg, reason); }
public UnAuthorizedException(String msg, String reason, Throwable cause) { super(msg, reason, cause); }
}
|
如何应用和error_code 约定
- 比如 请求参数不对,需要抛出请求参数异常则
1
| throw new RequestValidateException("lack of user id","lack of user id")
|
- 在服务端创建异常拦截器,对异常统一处理
- 对于未知异常,http状态码是500, 已知异常和自定义异常http状态码为200
- 前置异常 error_code :400~499
- 外部异常 error_code :600~699
- 自定义异常 不同的自定义error_code长度从都从1000开始,如果业务需要可以跳号到2000或者到3000开始等等
总结
本次整理服务端异常和错误码的规范结合了之前工作的经验,回想了开发以来出现的异常场景,之前工作中虽然熟知如何合理的抛出异常,但是大前提是一个团队需要对异常作出响应的规范,如果没有规范每个人都抛出自己方式的异常,对于前端展示和问题排查都是非常不友好的,并且不够灵活,比如目前工作中遇到的国际化问题,必须服务端的异常非常准确,后面做国际化才能非常简单。