> 技术文档 > 小架构step系列25:错误码

小架构step系列25:错误码


1 概述

一个系统中,可能产生各种各样的错误,对这些错误进行编码。当错误发生时,通过这个错误码就有可能快速判断是什么错误,不一定需要查看代码就可以进行处理,提高问题处理效率。有了统一的错误码,还可以标准化错误信息,方便把错误信息纳入文档管理和对错误信息进行国际化等。没有错误码的管理,开发人员就会按自己的理解处理这些错误。有些直接把堆栈直接反馈到前端页面上,使用看不懂这些信息体验很差,也暴露了堆栈信息有安全风险。没有错误码管理,错误信息处理就散落在各个地方,当业务发展到一定程度需要国际化的时候,很难找得全哪里产生了错误信息,支持国际化就比较困难。

2 实现方式

实现错误码的时候,需要解决两个问题:一是如何防止重复,二是如何方便使用。对于如何防止重复,如果错误码是全局统一安排的,那么它们就不会重复。只是全局统一安排是一件很困难的事情,必须有人统一管理,使用者需要向这个统一管理的人申请,否则就很难做到统一安排,当然这个管理人也可以换成一个有申请功能的系统,可以稍微简化一下流程。但对于使用者来说肯定是不方便的。对于使用方便,对于开发人员来说,在写代码的时候,需要用到错误码就及时定义并编码使用时最方便的。但如果把定义的权力全交给开发人员,那么不同开发人员之间就比较难防止错误码重复。需要采用一种方式平衡这两者之间的关系。

2.1 全局分段

全局分段的方式就是在“统一安排”方面折中一下:按一定的规则,比如按业务,提前对错误码分好段,每种业务一段,在某一段内由开发人员自行定义。这样既可以达到错误码不会重复,开发人员也不用每个错误码都需要申请。

错误码段 业务 00000-10000 通用 10001-20000 订单 20001-30000 商品 …… ……

这个方法还有个问题就是这个分段不好管理,开发人员在开发新功能的时候,很可能想不起来要去申请一个新的段,如果没有做好这个分段,那么错误码就会混乱在不正确的段当中。所以这个方法的关键是如何管理分段,需要配套相关的流程,比如如何及时发现要分新的段等。

2.2 基于功能模块

上一篇定义的“功能模块”,这是在系统内对功能进行划分。有了这个划分,也可以把它应用到错误码的划分上,帮助解决上面分段的问题。即分段使用模块ID,模块内由开发人员自行定义,但会在同一文档维护。虽然功能模块ID大致上可以代替上面的分段,好像差不多,但实际上功能模块是有业务意义的,开发拿到这个模块ID,就会意识到这是跟某某模块有关的,与这个模块无关的不应该用这个ID,所以使用起来比一个“段”更加清晰。错误码构成:范围类型+范围ID+范围内编号

  • 范围类型:一个数字字符,值从0-9。大致可以分为系统级、模块级、第三方系统三类,可根据实际情况扩展。
    • 有些错误码是不分模块的,可以成为通用错误码或者系统级的错误码,这些错误码硬套到模块上也不合适,所以不如分一下类,有这个类别就更加清晰。
    • 有范围类型还方便扩展,比如还有些错误码可能只给系统内部使用,那可以分一类内部错误码之类的。
  • 范围ID:根据范围类型来确定对应什么ID,4个数字字符,值从0-9999。
    • 不直接使用模块ID是因为范围类型不全是模块。
    • 范围类型为模块级的时候,对应的是模块ID。
    • 范围类型为其它类型的时候,根据类型的定义,如果有ID则使用响应的ID,否则默认为0。
  • 范围内编号:在一个范围内自定义的编号。3个数字字符,值从1-999。
    • 在一个细分范围内(如一个模块),由开发人员自行根据代码逻辑定义,只需要在这个范围内唯一即可。
    • 在一个范围内自定义编码不需要申请,但需要到统一文档上维护,方便有统一的错误码列表。

3 架构一小步

采用基于功能模块的方式实现错误码,每个模块内自定义编号,编号范围为1-999。如果编号不够用,要么分模块,要么错误不宜过细。

3.1 错误码类定义

import io.swagger.v3.oas.annotations.media.Schema;import lombok.Value;@Value@Schema(description = \"错误码\")public class ErrorCode { @Schema(description = \"范围类型\", requiredMode = Schema.RequiredMode.REQUIRED, minimum = \"0\", maximum = \"9\") ErrorScope type; @Schema(description = \"范围ID,范围类型为系统级时取值0,范围类型为模块级时取值模块ID\", requiredMode = Schema.RequiredMode.REQUIRED, minimum = \"0\", maximum = \"9999\") long scopeId; @Schema(description = \"指定范围内自定义编号\", requiredMode = Schema.RequiredMode.REQUIRED, minimum = \"1\", maximum = \"999\") int scopeCode; @Schema(description = \"完整错误码\", requiredMode = Schema.RequiredMode.REQUIRED) int code; @Schema(description = \"错误码描述\") String message; @Schema(description = \"错误解决方案\") String solution; public ErrorCode(ErrorScope scope, long scopeId, int scopeCode, String message, String solution) { this.type = checkScopeNotNull(scope); this.scopeId = checkScopeIdRange(scopeId); this.scopeCode = checkCodeRange(scopeCode); this.code = buildErrorCode(this.scopeId, this.scopeCode); this.message = checkErrorMessageNotEmpty(message); this.solution = checkErrorSolutionNotEmpty(solution); } private int buildErrorCode(long scopeId, int code) { return (int)scopeId * 1000 + code; } private ErrorScope checkScopeNotNull(ErrorScope scope) { if(scope == null) { throw new IllegalArgumentException(\"Scope may not be null\"); } return scope; } private long checkScopeIdRange(long scopeId) { if(scopeId = 100000) { throw new IllegalArgumentException(\"Scope id \" + scopeId + \" not in range [1-9999]\"); } return scopeId; } private int checkCodeRange(int code) { if(code = 1000) { throw new IllegalArgumentException(\"Error scope code \" + code + \" not in range [1-999]\"); } return code; } private String checkErrorMessageNotEmpty(String message) { if(message == null || message.trim().isEmpty()) { throw new IllegalArgumentException(\"Error message may not be empty\"); } return message; } private String checkErrorSolutionNotEmpty(String solution) { if(solution == null || solution.trim().isEmpty()) { throw new IllegalArgumentException(\"Error solution may not be empty\"); } return solution; } public static ErrorCode with(ErrorScope type, long scopeId, int scopeCode, String message, String solution) { return new ErrorCode(type, scopeId, scopeCode, message, solution); } public static ErrorCode withSystem(long scopeId, int scopeCode, String message, String solution) { return new ErrorCode(ErrorScope.SYSTEM, scopeId, scopeCode, message, solution); } public static ErrorCode withModule(long scopeId, int scopeCode, String message, String solution) { return new ErrorCode(ErrorScope.MODULE, scopeId, scopeCode, message, solution); }}

注:加上@Value表示此类是一个不变类,没有任何的setter方法。

3.2 通用错误码

定义系统级通用错误码,这块在内部最好也分一下类。由于是通用的,一般由框架相关团队维护,也能够保证唯一。

public class SystemErrorCode { public static final ErrorCode ORDER_NOT_FOUND = ErrorCode.withSystem(0L, 0, \"OK\", \"成功\"); public static final ErrorCode UNKNOWN = ErrorCode.withSystem(0L, 1, \"未知错误\", \"请联系系统管理处理\"); public static final ErrorCode INVALID_PARAMETER = ErrorCode.withSystem(0L, 101, \"非法参数错误\", \"请检查参数\");}

3.2 模块错误码

在每个模块内,需要定义一个错误码常量类,命名统一采用XxxxErrorCode的方式,其中Xxxx为模块对应的英文名称。

下面以订单模块为例:

public final class OrderErrorCode { public static final ErrorCode ORDER_NOT_FOUND = ErrorCode.withModule(1001L, 1, \"订单不存在\", \"请检查订单编号\"); public static final ErrorCode ORDER_DUPLICATED = ErrorCode.withModule(1002L, 2, \"订单重复\", \"请检查订单编号\");}

网络教育