1024创新实验室-公告

助力抖音1000个粉丝,开播写代码🎉🎉

打开【抖音APP】-点击【左上角侧边栏】-【点击扫一扫】-【进行关注】🎉🎉

和1024创新实验室一起,热爱代码,热爱生活,永远年轻,永远前行🎉🎉


Skip to content

返回错误码

一、需求与背景

错误码这个就不多说需求了,说一下这两年的一个场景,作为后端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-base项目中有一个核心的javabean类,即ResponseDTO,这个是返回前端对象的封装;

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

常用方法:

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

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

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

GoodsService.java 实际使用举例:

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();
    }

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

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);
    }

四、实现原理

4.1、 返回错误分类

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

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

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

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

java
public interface ErrorCode {

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

    //错误码
    int getCode();

    //错误消息
    String getMsg();

    //错误等级
    String getLevel();
}

SystemErrorCode

java
@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;
    }
}

SystemErrorCode

java
@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;
    }
}

UserErrorCode

java
@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;
    }
}

4.2、维护错误码

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

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

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

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

所以 ErrorCodeRegisterErrorCodeRangeContainer 类横空出世。

java
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();
    }
}

ErrorCodeRangeContainer 错误码 注册容器

java
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));
        }
        ...
        ...
    }
}

4.3、解读

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

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

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

即 调用ErrorCodeRegister.initialize()方法

举例 :AdminStartupRunner.java

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);
    }
}

联系我们

1024创新实验室-主任:卓大,混迹于各个技术圈,研究过计算机,熟悉点 java,略懂点前端。
1024创新实验室 致力于成为中原领先、国内一流的技术团队, 以AI+数字化为驱动,用技术为产业互联网提供无限可能, 业务如下:
  • 教育领域(高职院校数字化、就业创业大数据平台、继续教育平台;在线教育系统、视频直播、题库等,包含:医学、应急管理、成考、专升本等)
  • 供应链领域(网络货运平台、大宗贸易进销存ERP、物流管理TMS、B2B电商、仓储WMS、AI提效等)
  • 中医领域(诊所数字化管理、互联网医院、AI辅助诊疗、中医适宜技术、在线云问诊、空中药房等)
  • AI+软件领域(软件定制外包、开源技术、数据大屏、国产化改造、技术升级换代、人员外包、技术顾问、技术培训等)
加微信: 卓大
拉你入群,一起学习
公众号 :六边形工程师
分享:赚钱、代码、生活
请 “1024创新实验室”
烩面里加肉
咖啡配胡辣汤,提神又饱腹
抖音 : 六边形工程师
直播:赚钱、代码、中医