返回错误码

11/1/2022

# 一、需求与背景

错误码这个我们就不多说需求了,说一下这两年的我们一个场景,作为后端Coder,经常会遇到这样的情况:

测试或者客服、客户 找来:“你的XX 出Bug了,xxxx没有反应?”
测试上:“我看一个接口,接口返回:xxxx”,
测试问后端:“是不是你的问题?”
后端此时憋大件,还差500神装,被迫一顿操作,发现不是他的问题,是用户的误操作问题等等,错失超神。
然后怼了一顿测试,“这是xxx的问题,找我做什么....”

以上是一个小情景,但是很真实。怎么解决呢?

# 二、架构与思想

# 2.1、错误码分类的意义

我们这里将错误码定义为了三类:

  • 第一类:系统错误,system (即后台报错了,抛异常了)
  • 第二类:未预期到的错误,unexpected(比如用户的钱对应不上了,异或 该有这个奖品,后来某个“信球”给删了,即后台发生了不该发生的事情,超乎寻常!)
  • 第三类:用户级别错误,user (比如表单验证错误,用户不满足抽奖条件)

根据以上三类,对于即将神装的我们,如果测试能够告诉我们是那一种类型的错误码就好了,如果是第三类,我肯定就超神 Penta Kill 异或 Rampage 了。

# 2.2、返回码维护的好处

考虑下分布式的场景,服务比较多,场景比较多,业务相对复杂点,当你调用其他服务的接口,需要在某个特定的场景下做一些事情的时候,需要怎么区分,或者可以看看其他开放平台接口。比如微信,支付宝等等,你会发现都会对不同的返回结果有个特殊的返回码。

所以如果对于一个长期维护的产品而言,把返回码维护好是非常重要的和必要的。

返回码维护的好处:

  • 便于长期维护
  • 避免在java代码中直接写字符串,不符合阿里规约
  • 便于将来的服务拆分和扩展
  • 便于与其他系统进行对接和开放接口
  • 便于前端做更加细致的操作
  • 暂时想到这么多

# 三、具体使用

# 3.1、三个错误码类

  • 系统错误: SystemErrorCode.java
  • 未预期到的错误: UnexpectedErrorCode.java
  • 用户错误: UserErrorCode.java

# 3.2、ResponseDTO

sa-common项目中有一个核心的javabean类,即ResponseDTO,这个是返回前端对象的封装;

public class ResponseDTO<T> {
    private Integer code; //返回码: 0 成功;不是0,不成功
    private String level;// 分类:系统错误,未预期到的错误,用户错误; 如果正确,则为空
    private String msg;//消息
    private Boolean ok;//是否正确返回
    private T data;//返回数据
1
2
3
4
5
6
7

常用方法:

//------- 成功方法 使用 --------------
ResponseDTO.ok();  //返回成功
ResponseDTO.ok(resultObject);  //返回成功,并且 data 为 resultObject
ResponseDTO.okMsg(msg);  //返回成功,并且 msg 为 msg

//------- 返回错误码 使用 --------------
ResponseDTO.error(UserErrorCode.LOGIN_STATE_INVALID);// 直接返回错误码
ResponseDTO.errorData(UserErrorCode.LOGIN_STATE_INVALID, errorObject);// 直接返回错误码,并附带 data信息
..还有其他方法..

//------- 最常用的 用户参数 错误码  --------------
ResponseDTO.userErrorParam(); //用户参数错误
ResponseDTO.userErrorParam(msg); //用户参数错误,并附带提示信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14

GoodsService.java 实际使用举例:

  private ResponseDTO<String> checkGoods(GoodsAddForm addForm, Long goodsId) {
        // 校验类目id
        Long categoryId = addForm.getCategoryId();
        Optional<CategoryEntity> optional = categoryQueryService.queryCategory(categoryId);
        if (!optional.isPresent() || !CategoryTypeEnum.GOODS.equalsValue(optional.get().getCategoryType())) {
            return ResponseDTO.error(UserErrorCode.DATA_NOT_EXIST, "商品类目不存在~");
        }
        return ResponseDTO.ok();
    }

    /**
     * 删除
     */
    @Transactional(rollbackFor = Exception.class)
    public ResponseDTO<String> delete(Long goodsId) {
        GoodsEntity goodsEntity = goodsDao.selectById(goodsId);
        if (goodsEntity == null) {
            return ResponseDTO.userErrorParam("商品不存在");
        }
        if (!goodsEntity.getGoodsStatus().equals(GoodsStatusEnum.SELL_OUT.getValue())) {
            return ResponseDTO.userErrorParam("只有售罄的商品才可以删除");
        }
        batchDelete(Arrays.asList(goodsId));
        dataTracerService.batchDelete(Arrays.asList(goodsId), DataTracerTypeEnum.GOODS);
        return ResponseDTO.ok();
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

因为菜单是系统能运行的核心功能,所以菜单业务返回的是“系统错误”,举例如下 MenuService.java

// 因为菜单
    public ResponseDTO<MenuVO> getMenuDetail(Long menuId) {
        //校验菜单是否存在
        MenuEntity selectMenu = menuDao.selectById(menuId);
        if (selectMenu == null) {
            return ResponseDTO.error(SystemErrorCode.SYSTEM_ERROR, "菜单不存在");
        }
        if (selectMenu.getDeletedFlag()) {
            return ResponseDTO.error(SystemErrorCode.SYSTEM_ERROR, "菜单已被删除");
        }
        MenuVO menuVO = SmartBeanUtil.copy(selectMenu, MenuVO.class);
        //处理接口权限
        String perms = menuVO.getApiPerms();
        if (!StringUtils.isBlank(perms)) {
            List<String> permsList = Lists.newArrayList(StringUtils.split(perms, ","));
            menuVO.setApiPermsList(permsList);
        }
        return ResponseDTO.ok(menuVO);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 四、实现原理

# 4.1、 返回错误分类

代码:net.lab1024.sa.common.common.domain.ResponseDTO

code: 1,               0表示成功,不是0表示错误
level: 'user',        等级:对应上面的三类,system,unexpected,user
msg:"成功",
data:{}
1
2
3
4

上面中 多了一个level 字段,就是表明 这个错误的分类 ,很重要。

对于通用的几个错误码如下:
ErrorCode.java

public interface ErrorCode {

    String LEVEL_SYSTEM = "system";//系统等级
    String LEVEL_USER = "user";//用户等级
    String LEVEL_UNEXPECTED = "unexpected";//未预期到的等级

    //错误码
    int getCode();

    //错误消息
    String getMsg();

    //错误等级
    String getLevel();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

SystemErrorCode

@Getter
@AllArgsConstructor
public enum SystemErrorCode implements ErrorCode {
    SYSTEM_ERROR(10001, "系统似乎出现了点小问题");

    private final int code;
    private final String msg;
    private final String level;

    SystemErrorCode(int code, String msg) {
        this.code = code;
        this.msg = msg;
        this.level = LEVEL_SYSTEM;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

SystemErrorCode

@Getter
@AllArgsConstructor
public enum UnexpectedErrorCode implements ErrorCode {
    BUSINESS_HANDING(20001, "呃~ 业务繁忙,请稍后重试"),
    PAY_ORDER_ID_ERROR(20002, "付款单id发生了异常,请联系技术人员排查");

    private final int code;
    private final String msg;
    private final String level;

    UnexpectedErrorCode(int code, String msg) {
        this.code = code;
        this.msg = msg;
        this.level = LEVEL_UNEXPECTED;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

UserErrorCode

@Getter
@AllArgsConstructor
public enum UserErrorCode implements ErrorCode {

    PARAM_ERROR(30001, "参数错误"),
    DATA_NOT_EXIST(30002, "左翻右翻,数据竟然找不到了~"),
    ALREADY_EXIST(30003, "数据已存在了呀~"),
    REPEAT_SUBMIT(30004, "亲~您操作的太快了,请稍等下再操作~"),
    NO_PERMISSION(30005, "对不起,您无法访问此资源哦~"),
    LOGIN_STATE_INVALID(30007, "您还未登录或登录失效,请重新登录!"),
    FORM_REPEAT_SUBMIT(30009, "请勿重复提交");

    private final int code;
    private final String msg;
    private final String level;

    UserErrorCode(int code, String msg) {
        this.code = code;
        this.msg = msg;
        this.level = LEVEL_USER;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 4.2、维护错误码

希望有个地方来维护这些返回码,通常最简单的想法是放到一个常量类或者枚举enum类里面,想法挺好,但是这样又有些问题:

  1. 怎么避免返回码的重复?
  2. 业务多会导致这个类特别大,难维护
  3. 如何定义范围?

带着以上问题可以总结如下:

  • 为避免类特别大,必须放到多个类里面
  • 必须要有范围的定义和说明
  • 必须要有全局的码值和避免范围重复的检测机制

所以 ErrorCodeRegisterErrorCodeRangeContainer 类横空出世。

import static net.lab1024.sa.common.common.code.ErrorCodeRangeContainer.register;
public class ErrorCodeRegister {
    static {
        // 系统 错误码
        register(SystemErrorCode.class, 10001, 20000);
        // 意外 错误码
        register(UnexpectedErrorCode.class, 20001, 30000);
        // 用户 通用错误码
        register(UserErrorCode.class, 30001, 40000);
    }
    public static int initialize() {
        return ErrorCodeRangeContainer.initialize();
    }
    public static void main(String[] args) {
        ErrorCodeRegister.initialize();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

ErrorCodeRangeContainer 错误码 注册容器

class ErrorCodeRangeContainer {

    static final int MIN_START_CODE = 10000;//所有的错误码均大于10000
    static int errorCounter = 0;//用于统计数量
    static final Map<Class<? extends ErrorCode>, ImmutablePair<Integer, Integer>> CODE_RANGE_MAP = new ConcurrentHashMap<>();

   // 注册状态码 校验是否重复 是否越界
    static void register(Class<? extends ErrorCode> clazz, int start, int end) {
        String simpleName = clazz.getSimpleName();
        if (!clazz.isEnum()) {
            throw new ExceptionInInitializerError(String.format("<<ErrorCodeRangeValidator>> error: %s not Enum class !", simpleName));
        }
        if (start > end) {
            throw new ExceptionInInitializerError(String.format("<<ErrorCodeRangeValidator>> error: %s start must be less than the end !", simpleName));
        }
        ...
        ...
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 4.3、解读

1)对于每个业务模块的XxxErrorCode,继承自三个基类(UserErrorCode/UnexpectedErrorCode/SystemErrorCode)中的一个,并将此类的code的起始值和末尾值注册进来。

2)在ErrorCodeRangeContainer类中有map用于接受注册的code,用于检测。

3)系统启动时检测:
因为都是static常量,且类结构相同,所以可以在项目启动的时候利用static静态加载和反射技术进行全项目的code值检测。

即 调用ErrorCodeRegister.initialize()方法

举例 :AdminStartupRunner.java

@Slf4j
@Component
public class AdminStartupRunner implements CommandLineRunner {

    @Autowired
    private ScheduleConfig scheduleConfig;

    @Override
    public void run(String... args) {
        // 初始化状态码
        int codeCount = ErrorCodeRegister.initialize();
        //TODO <卓大> :根据实际情况来决定是否开启定时任务
        String destroySchedules = "Spring 定时任务 @Schedule 已启动";
//      destroySchedules = scheduleConfig.destroy();
        log.info("\n ---------------【1024创新实验室 温馨提示:】 ErrorCode 共计完成初始化: {}个!---------------" +
                 "\n ---------------【1024创新实验室 温馨提示:】 {}---------------\n", codeCount, destroySchedules);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 联系我们

1024创新实验室-主任:卓大 (opens new window),混迹于各个技术圈,研究过计算机,熟悉点 java,略懂点前端。
1024创新实验室(河南·洛阳) (opens new window) 致力于成为中原领先、国内一流的技术团队,以技术创新为驱动,合作各类项目。

加 主任 “卓大” 微信
拉你入群,一起学习
关注 “小镇程序员”
分享代码与生活、技术与赚钱
请 “1024创新实验室” 喝咖啡
支持我们的开源与分享

告白气球 (钢琴版)
JESSE T