写点什么

错误码设计思考

作者:木小风
  • 2022 年 3 月 24 日
  • 本文字数:2601 字

    阅读完需:约 9 分钟

错误码设计思考

在微服务化的今天,服务间的交互越来越复杂,统一异常处理规范作为框架的基础,一旦上线后很难再更改,如果设计不好,会导致后期的维护成本越来越来大。 对于错误码的设计,不同的开发团队有不同的风格习惯。本文分享作者从实践中总结的经验及对应的思考,期望对读者有所启发。


本文中涉及的源码:https://github.com/sofn/app-engine/tree/master/common-error

什么是错误码

引自阿里巴巴《Java 开发手册》- 异常日志-错误码


错误码的制定原则:快速溯源、简单易记、沟通标准化。


正例:错误码回答的问题是谁的错?错在哪?


1)错误码必须能够快速知晓错误来源,可快速判断是谁的问题。


2)错误码易于记忆和比对(代码中容易 equals)。


3)错误码能够脱离文档和系统平台达到线下轻量化地自由沟通的目的。


那么用 Java 异常能表示出来吗?答案显然是否定的


  • 必须能够快速知晓错误来源:异常类因为复用性不能很快的定位,异常类和代码行数也不是一个稳定的值

  • 必须易于记忆和对比:异常类不具有可比性,且不利于前后端交互

  • 能够脱离代码沟通:异常类只能存在于 Java 代码中

错误码设计

错误码的设计是比较简单的,一般只需要定义一个数字和描述信息即可。不过想设计一套完善错误码系统还有很多需要考虑的场景。

1、错误码的分层

大部分项目错误码设计分为 3 级能满足业务场景,即项目、模块、错误编码。比如错误码是 6 位,前两位是项目码、中间两位是模块码,最后两位是异常编号。以下是错误码 10203 的对应说明:


2、错误的表示方法:枚举 or 类

推荐使用枚举,因为枚举具有不可变性,且所有值都在一个文件里描述。

3、多模块错误码定义及接口定义

最原始的错误定义方法是项目中所有的错误码都定义在一个类里,但是这样会随着业务的发展错误码越来越多,最终导致难以维护,推荐的做法是按照项目+模块粒度定义成多个错误码枚举类。有两个问题需要考虑:


(1)项目编码、模块编码的维护:推荐另建一个枚举类统一维护


(2)异常类的统一引用:定义接口,枚举类实现接口


示例:


//异常接口定义public interface ErrorCode {}//模块定义public enum UserProjectCodes {    LOGIN(1, 1, "登录模块"),    USER(1, 2, "用户模块")}//登录模块异常码定义public enum LoginErrorCodes implements ErrorCode {    USER_NOT_EXIST(0, "用户名不存在"), //错误码: 10100    PASSWORD_ERROR(1, "密码错误");    //错误码: 10101        private final int nodeNum;    private final String msg;
UserLoginErrorCodes(int nodeNum, String msg) { this.nodeNum = nodeNum; this.msg = msg; ErrorManager.register(UserProjectCodes.LOGIN, this); }}
复制代码

4、防重设计

错误码本质上就是一个数字,且每一个都需要由 RD 编码定义,在错误码多的项目很容易重复。最佳实践是在枚举的构造方法里调用 Helper 类,Helper 类统一维护所有的异常码,如有重复则枚举初始化失败。

5、错误扩展信息

只有错误码是不够的,还需要反馈给调用方详细的错误信息以方便修正。固定的错误信息字符串在某些场景写也是不够的,这里推荐使用 slf4j 打日志时使用的动态参数,这种方式相比于 String.format 格式的好处是不需要关心参数的类型以及记忆 %s、%d 等的区别,且打印日志时经常使用,降低了团队成员的学习成本。


示例:


//错误码定义PARAM_ERROR(17, "参数非法,期望得到:{},实际得到:{}")//错误码使用ErrorCodes.PARAM_ERROR.format(arg1, arg2);
复制代码


实现方式:


org.slf4j.helpers.MessageFormatter.arrayFormat(this.message, args).getMessage()  
复制代码

错误码和异常

在日常业务开发中,对于异常使用最多的还是抛出 Java 异常(Exception),异常又分为受检查异常(Exception)和不受检查异常(RuntimeException):


  • 受检查的异常:这种在编译时被强制检查的异常称为"受检查的异常"。即在方法的声明中声明的异常。

  • 不受检查的异常:在方法的声明中没有声明,但在方法的运行过程中发生的各种异常被称为"不被检查的异常"。这种异常是错误,会被自动捕获。

1、异常绑定错误码

定义两个父类,分别用于首检查异常和非受检查异常。可支持传入错误码,同时需要支持原始的异常传参,这种场景会赋予一个默认的错误码,比如:500 服务器内部异常


//父类定义public abstract class BaseException extends Exception {
protected BaseException(String message) {...}
protected BaseException(String message, Throwable cause) {...}
protected BaseException(Throwable cause) {...}
protected BaseException(ErrorInfo errorInfo) {...}
protected BaseException(ErrorCode errorCode) {...}
protected BaseException(ErrorCode errorCode, Object... args) {...}}
复制代码

2、部分异常

使用异常能适用于大部分场景,不过对于多条目的场景不是很适合,比如需要批量保存 10 条记录,某些成功、某些失败,这种场景就不适合直接抛出异常。


在 Node.js 和 Go 语言中异常处理采用多返回值方式处理,第一个值是异常,如果为 null 则表示无异常。在 Java 里建议采用 vavr 库中的 Either 来实现,通常使用左值表示异常,而右值表示正常调用后的返回结果,即: Either<ErrorCode, T>


注意不推荐 Pair、Tuple 来实现,因为 Either 只能设置一个左值或右值,而 Pair、Tuple 无此限制。

错误码和统一返回值

在前后端的交互中,后端一般使用 JSON 方式返回结果,整合前面说的错误码,可定义以下格式:


{   "code": number,   "msg": string,   "data": object}
复制代码


在 SpringMVC 中实现方式是自定义 ResponseBodyAdvice 和异常拦截,具体实现方式直接查看:源码


实现了以上步骤之后就可以在 SpringMVC 框架中愉快的使用了,会自动处理异常及封装成统一返回格式


    @GetMapping("/order")    public Order getOrder(Long orderId) {        return service.findById(orderId);    }
复制代码

总结

本文总结了设计错误码需要考虑的各种因素,并给出了参考示例,基本能满足一般中大型项目。规范有了最重要的还是落地,让团队成员遵守规范才能让项目健康的迭代。


源码地址:https://github.com/sofn/app-engine/tree/master/common-error


本文链接:错误码设计思考


作者简介:木小丰,美团 Java 技术专家,专注分享软件研发实践、架构思考。欢迎关注公共号:Java 研发


更多精彩文章:


Java线程池进阶


从MVC到DDD的架构演进


平台化建设思路浅谈


构建可回滚的应用及上线checklist实践


Maven依赖冲突问题排查经验

发布于: 刚刚阅读数: 2
用户头像

木小风

关注

美团技术专家。公共号:Java研发 2012.03.22 加入

还未添加个人简介

评论

发布
暂无评论
错误码设计思考_Java_木小风_InfoQ写作平台