小架构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 错误码类定义

java 复制代码
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 <= 0 || scopeId >= 100000) {
            throw new IllegalArgumentException("Scope id " + scopeId + " not in range [1-9999]");
        }

        return scopeId;
    }

    private int checkCodeRange(int code) {
        if(code <=0 || 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 通用错误码

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

java 复制代码
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为模块对应的英文名称。

下面以订单模块为例:

java 复制代码
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,
            "订单重复", "请检查订单编号");
}
相关推荐
小信丶15 小时前
Spring Cloud Stream EnableBinding注解详解:定义、应用场景与示例代码
java·spring boot·后端·spring
全栈开发圈15 小时前
新书速览|从零开始学Spring Cloud微服务架构
spring cloud·微服务·架构
无限进步_15 小时前
【C++】验证回文字符串:高效算法详解与优化
java·开发语言·c++·git·算法·github·visual studio
亚历克斯神15 小时前
Spring Cloud 2026 架构演进
java·spring·微服务
七夜zippoe15 小时前
Spring Cloud与Dubbo架构哲学对决
java·spring cloud·架构·dubbo·配置中心
海派程序猿15 小时前
Spring Cloud Config拉取配置过慢导致服务启动延迟的优化技巧
java
阿维的博客日记15 小时前
为什么不逃逸代表不需要锁,JIT会直接删掉锁
java
William Dawson15 小时前
CAS的底层实现
java
九英里路15 小时前
cpp容器——string模拟实现
java·前端·数据结构·c++·算法·容器·字符串
Gavin_ZYX15 小时前
Skill 管理过于繁琐,不如写个自动同步的工具
人工智能·架构·github