云API错误码的设计规则
错误码分为两级。例如,InvalidParameter.ImageId,以点号分隔。其中, 第一级错误码统一由API平台提供 ,业务选择合适的错误场景。第二级错误码可选,业务可自定义。 luke的github仓库
如果有更好地设计,欢迎和我探讨。
举例
比如InvalidParameterValue.InvalidAppId 一级错误码为:InvalidParameterValue 定义为参数错误。 二级错误码为:InvalidAppId 业务自定义,表示非法的应用Id。
对于错误码的设计,我非常不喜欢其他项目中设计为数字的形式。类似100001,100002这样的错误码,每次这样都得进行一次大脑的映射。所以虽然不喜欢用云API,但是云API字符串的错误码,我双手支持。
处理云API的错误码类和异常类
项目中处理云API错误码的异常类过于简单,经常遇到老板和客户对业务抛出的错误码产生质疑。 其本质的原因有俩点。
- 大家抛出异常的代码很多写的很随意。基本上只有自己本人可以看懂。
- 开发项目的时候,说实话,大部分人其实不关心错误码。
- 部分同学是go转过来的,对错误码抛出的标准理解不太一样。
常见的错误处理方式
1. 依据返回值
经常写go的同学比较熟悉。
public class HandleErrorTest {
Integer shouldPrint(String input){
if(input == null) {
return -1;
}
System.out.println(input);
return 0;
}
@Test
public void handleErrorByReturnValueIs0(){
Assert.assertTrue(0 == shouldPrint("123"));
}
@Test
public void handleErrorByReturnValueIs1(){
Assert.assertTrue(-1 == shouldPrint(null));
}
}
什么时候需要使用这样的方式呢?
大多数业务中,我们不会采取这样的方式。 只有一种场景:就是我们发现这个函数运行错误的时候,我们需要根据返回值处理,并继续处理下面的逻辑。例如这样。
public void handleErrorDemo(String x){
Integer integer = shouldPrint(x);
if(integer == null){
handleNullCondition();
}else{
handleOtherCondition();
}
}
当然,我们也可以通过catch异常的方式来处理。但是不太推荐。不过这个具体按照团队风格,保持统一即可。
异常指的是程序没有按照预期的逻辑去执行。此时需要抛出异常。大多数的异常,我们都是不去catch的,而是直接让他抛出,好进入我们的exceptionHandler的处理逻辑,并通过错误码的方式纠正或者防止调用方的异常调用。
而返回错误值的形式更多用来处理已经预料到的情况,此时,则是直接通过返回的错误码继续进行程序的逻辑。
2. 根据异常处理
Integer shouldPrint2(String input){
if(input == null) {
throw new RuntimeException("input can't be null");
}
System.out.println(input);
return 0;
}
一般这样的写法,我在调用侧都不会做任何处理。直接让请求失败即可。
总结
其实大部分的异常情况,我的建议是直接抛出错误。不要吞掉异常!!!少部分带有逻辑的情况,可能需要按照返回错误值的方式来处理。
错误码类的设计
在做任何类设计的时候,我们都是有目标的,即要解决什么问题。这里先列出问题。
- 错误码需要区分一级错误码和二级错误码,可以很方便地帮我获取一级错误码。场景:对于上游业务来说,对于参数错误我会忽略,对于内部错误我会告警。
- 错误码可以返回更加定制化的信息。比如ResourceNotFound,最好告诉我ResourceId是什么。
- 当链路中存在2个以上服务时,我想知道更下游的错误信息。
按照上述目标我们进行设计。
public interface IBizErrorCode {
String getErrorMessage(Object... args);
String getErrorCode();
PrimaryErrorCode getPrimaryCode();
String detailErrorCode();
String getDownStreamErrorMessage(Object... args);
String getDownStreamErrorCode(Object... args);
String getDownStreamPrimaryCode(String downStreamErrorCode);
}
具体的实现例子如下
public enum TestErrorCode implements IBizErrorCode {
TestBaseError(PrimaryErrorCode.InternalError, "TestBaseError", "this is a message, reason = {0}", "{1}", "{2}"),
ResourceNotFound(PrimaryErrorCode.ResourceNotFound, "UserNotFound", "userId={0} not found", "{1}", "{2}")
;
private final PrimaryErrorCode primaryErrorCode;
private final String detailErrorCode;
private final String errorMessage;
private final String downStreamErrorCode;
private final String downStreamErrorMessage;
TestErrorCode(PrimaryErrorCode primaryErrorCode, String detailErrorCode, String errorMessage,
String downStreamErrorCode, String downStreamErrorMessage) {
this.primaryErrorCode = primaryErrorCode;
this.detailErrorCode = detailErrorCode;
this.errorMessage = errorMessage;
this.downStreamErrorCode = downStreamErrorCode;
this.downStreamErrorMessage = downStreamErrorMessage;
}
@Override
public String getErrorMessage(Object... args) {
return new MessageFormat(this.errorMessage).format(args);
}
@Override
public String getErrorCode() {
Objects.requireNonNull(getPrimaryCode(), "primaryCode can't be null");
String code = getPrimaryCode().getCode();
if (StringUtils.isBlank(detailErrorCode())) {
return code;
} else {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(code)
.append(".")
.append(detailErrorCode());
return stringBuffer.toString();
}
}
@Override
public PrimaryErrorCode getPrimaryCode() {
return this.primaryErrorCode;
}
@Override
public String detailErrorCode() {
return this.detailErrorCode;
}
@Override
public String getDownStreamErrorMessage(Object... args) {
return replacePlaceholderWithValue(this.downStreamErrorMessage, args);
}
@Override
public String getDownStreamErrorCode(Object... args) {
return replacePlaceholderWithValue(this.downStreamErrorCode, args);
}
@Override
public String getDownStreamPrimaryCode(String downStreamErrorCode) {
return ErrorCodeUtils.getFirstCode(downStreamErrorCode);
}
}
具体的使用场景如下
public void userNotFound(String userId){
if(StringUtils.isBlank(userId)){
throw new BizException(TestErrorCode.InvalidParameter, "userId can't be blank");
}
if(StringUtils.equals(userId, "secret user id")){
// do nothing
}else{
throw new BizException(TestErrorCode.UserNotFound, userId);
}
}
@Test
public void testUserNotFound() {
try{
userNotFound("luke");
}catch (BizException bizException){
bizException.printStackTrace();
}
}
一级错误码的设计
云API因为分为一级错误码和二级错误码的概念。 我们从规范可以看到,一级错误码的变化很少。直接设计为枚举类。
public enum PrimaryErrorCode {
AuthFailure("AuthFailure"),
FailedOperation("FailedOperation"),
InternalError("InternalError"),
ResourceInsufficient("ResourceInsufficient"),
ResourceNotFound("ResourceNotFound"),
/**
省略部分
**/
UnsupportedOperation("UnsupportedOperation"),
;
PrimaryErrorCode(String code){
this.code = code;
}
public String getCode(){
return this.code;
}
private String code;
}
二级错误码的设计
因为是公共包,下面业务的错误码均由业务自己定义,公共包只定义String基础类。