错误码处理类的设计
云API错误码的设计规则
腾讯云云API错误码分为两级。以点号分隔。
第一级错误码统一由API平台提供 ,业务选择合适的错误场景。第二级错误码可选,业务可自定义。
例如,InvalidParameter.InvalidUserName。其中, 第一级错误码为InvalidParameter
,表示这是一个参数错误。第二级错误码为InvalidUserName
,表示具体错误的原因是UserName非法。
第一级错误码被整个平台进行监控。并进行错误统计,以及告警。
第二级错误码则一般为业务自定义具体错误码。用来提供FAQ,或者给上游服务更加明确地指示。
对于错误码的设计,我非常不喜欢其他项目中设计为数字的形式。类似100001,100002这样的错误码,每次这样都得进行一次大脑的映射。
详细设计请看 luke的github仓库
如果有更好地设计,欢迎和我探讨。
举例
参数错误
比如InvalidParameterValue.InvalidAppId
一级错误码为:InvalidParameterValue 定义为参数错误。
二级错误码为:InvalidAppId 业务自定义,表示非法的应用Id。
服务端错误
比如InternalError.CantFindConsul
一级错误码为:InternalError 定义为系统错误,或者说服务端错误。
二级错误码为:CantFindConsul 具体定义,表示Consul找不到。
这样的错误码,即使我不去看任何文档,我也能第一时间大概明白发生了什么。
处理云API的错误码类和异常类
如何抛出异常以及如何处理异常是一门学问。
常见的错误处理方式
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 retCode = shouldPrint(x);
if(retCode == -1){
handleNullCondition();
}else{
handleOtherCondition();
}
}
返回错误值的本质是为了让我们根据错误值可以按照不同逻辑处理代码。
但是有个重大缺陷,就是在一些列函数调用中,必须依次返回这个错误。
public void func1(String x) int{
return func2(x);
}
public void func2(String x) int{
return shouldPrint(x);
}
在Go代码中,我们可以看到error。所以采用此种方式返回。
但是在Java代码中,强烈不建议这么做。
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基础类。