1

错误码设计思考

 2 years ago
source link: https://lesofn.com/archives/errorcode-design
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

错误码设计思考

木小丰 2022年03月24日 71次浏览

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

本文中涉及的源码: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依赖冲突问题排查经验


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK