小架构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,
            "订单重复", "请检查订单编号");
}
相关推荐
Amagi.14 分钟前
Java设计模式-代理模式
java·代理模式
Joker—H19 分钟前
【Java】Reflection反射(代理模式)
java·开发语言·经验分享·代理模式·idea
阿里巴巴淘系技术团队官网博客1 小时前
面向互联网2C业务的分布式类Manus Java框架
java·开发语言·分布式
躲在云朵里`1 小时前
Java面试题(中等)
java
懂得节能嘛.1 小时前
【SpringAI实战】实现仿DeepSeek页面对话机器人(支持多模态上传)
java·spring
张乔242 小时前
mybatisX的自定义模板生成
java·ide·intellij-idea
笨蛋不要掉眼泪2 小时前
Java测试题(上)
java·开发语言
ahauedu2 小时前
用Java 代码实现一个简单的负载均衡逻辑
java·python·负载均衡
Java初学者小白2 小时前
秋招Day18 - MyBatis - 基础
java·数据库·mybatis
大白玉米2 小时前
TVBOXOS6.0双端APP二开源码完整版全开源源码重构版
java·重构·php