雲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基礎類。