返回错误码
一、需求与背景
错误码这个就不多说需求了,说一下这两年的一个场景,作为后端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
,这个是返回前端对象的封装;
public class ResponseDTO<T> {
private Integer code; //返回码: 0 成功;不是0,不成功
private String level;// 分类:系统错误,未预期到的错误,用户错误; 如果正确,则为空
private String msg;//消息
private Boolean ok;//是否正确返回
private T data;//返回数据
}
常用方法:
//------- 成功方法 使用 --------------
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
实际使用举例:
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
// 因为菜单
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
code: 1, 0表示成功,不是0表示错误
level: 'user', 等级:对应上面的三类,system,unexpected,user
msg:"成功",
data:{}
上面中 多了一个level
字段,就是表明 这个错误的分类 ,很重要。
对于通用的几个错误码如下:
ErrorCode.java
public interface ErrorCode {
String LEVEL_SYSTEM = "system";//系统等级
String LEVEL_USER = "user";//用户等级
String LEVEL_UNEXPECTED = "unexpected";//未预期到的等级
//错误码
int getCode();
//错误消息
String getMsg();
//错误等级
String getLevel();
}
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;
}
}
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;
}
}
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;
}
}
4.2、维护错误码
希望有个地方来维护这些返回码,通常最简单的想法是放到一个常量类或者枚举enum类里面,想法挺好,但是这样又有些问题:
- 怎么避免返回码的重复?
- 业务多会导致这个类特别大,难维护
- 如何定义范围?
带着以上问题可以总结如下:
- 为避免类特别大,必须放到多个类里面
- 必须要有范围的定义和说明
- 必须要有全局的码值和避免范围重复的检测机制
所以 ErrorCodeRegister
和 ErrorCodeRangeContainer
类横空出世。
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 错误码 注册容器
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
@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);
}
}
联系我们
- 教育领域(高职院校数字化、就业创业大数据平台、继续教育平台;在线教育系统、视频直播、题库等,包含:医学、应急管理、成考、专升本等)
- 供应链领域(网络货运平台、大宗贸易进销存ERP、物流管理TMS、B2B电商、仓储WMS、AI提效等)
- 中医领域(诊所数字化管理、互联网医院、AI辅助诊疗、中医适宜技术、在线云问诊、空中药房等)
- AI+软件领域(软件定制外包、开源技术、数据大屏、国产化改造、技术升级换代、人员外包、技术顾问、技术培训等)
加微信: 卓大 拉你入群,一起学习 | 公众号 :六边形工程师 分享:赚钱、代码、生活 | 请 “1024创新实验室” 烩面里加肉 咖啡配胡辣汤,提神又饱腹 | 抖音 : 六边形工程师 直播:赚钱、代码、中医 |