錯誤碼設計思考

木小豐發表於2022-03-24

在微服務化的今天,服務間的互動越來越複雜,統一異常處理規範作為框架的基礎,一旦上線後很難再更改,如果設計不好,會導致後期的維護成本越來越來大。 對於錯誤碼的設計,不同的開發團隊有不同的風格習慣。本文分享作者從實踐中總結的經驗及對應的思考,期望對讀者有所啟發。

本文中涉及的原始碼:https://github.com/sofn/app-engine/tree/master/common-error

什麼是錯誤碼

引自阿里巴巴《Java 開發手冊》- 異常日誌-錯誤碼

錯誤碼的制定原則:快速溯源、簡單易記、溝通標準化。

正例:錯誤碼回答的問題是誰的錯?錯在哪?
1)錯誤碼必須能夠快速知曉錯誤來源,可快速判斷是誰的問題。
2)錯誤碼易於記憶和比對(程式碼中容易 equals)。
3)錯誤碼能夠脫離文件和系統平臺達到線下輕量化地自由溝通的目的。

那麼用Java異常能表示出來嗎?答案顯然是否定的

  • 必須能夠快速知曉錯誤來源:異常類因為複用性不能很快的定位,異常類和程式碼行數也不是一個穩定的值
  • 必須易於記憶和對比:異常類不具有可比性,且不利於前後端互動
  • 能夠脫離程式碼溝通:異常類只能存在於Java程式碼中

錯誤碼設計

錯誤碼的設計是比較簡單的,一般只需要定義一個數字和描述資訊即可。不過想設計一套完善錯誤碼系統還有很多需要考慮的場景。

1、錯誤碼的分層

大部分專案錯誤碼設計分為3級能滿足業務場景,即專案、模組、錯誤編碼。比如錯誤碼是6位,前兩位是專案碼、中間兩位是模組碼,最後兩位是異常編號。以下是錯誤碼10203的對應說明:

2、錯誤的表示方法:列舉or 類

推薦使用列舉,因為列舉具有不可變性,且所有值都在一個檔案裡描述。

3、多模組錯誤碼定義及介面定義

最原始的錯誤定義方法是專案中所有的錯誤碼都定義在一個類裡,但是這樣會隨著業務的發展錯誤碼越來越多,最終導致難以維護,推薦的做法是按照專案+模組粒度定義成多個錯誤碼列舉類。有兩個問題需要考慮:

(1)專案編碼、模組編碼的維護:推薦另建一個列舉類統一維護

(2)異常類的統一引用:定義介面,列舉類實現介面

示例:

//異常介面定義
public interface ErrorCode {
}
//模組定義
public enum UserProjectCodes {
    LOGIN(1, 1, "登入模組"),
    USER(1, 2, "使用者模組")
}
//登入模組異常碼定義
public enum LoginErrorCodes implements ErrorCode {
    USER_NOT_EXIST(0, "使用者名稱不存在"), //錯誤碼: 10100
    PASSWORD_ERROR(1, "密碼錯誤");    //錯誤碼: 10101
    
    private final int nodeNum;
    private final String msg;

    UserLoginErrorCodes(int nodeNum, String msg) {
        this.nodeNum = nodeNum;
        this.msg = msg;
        ErrorManager.register(UserProjectCodes.LOGIN, this);
    }
}

4、防重設計

錯誤碼本質上就是一個數字,且每一個都需要由RD編碼定義,在錯誤碼多的專案很容易重複。最佳實踐是在列舉的構造方法裡呼叫Helper類,Helper類統一維護所有的異常碼,如有重複則列舉初始化失敗。

5、錯誤擴充套件資訊

只有錯誤碼是不夠的,還需要反饋給呼叫方詳細的錯誤資訊以方便修正。固定的錯誤資訊字串在某些場景寫也是不夠的,這裡推薦使用slf4j打日誌時使用的動態引數,這種方式相比於String.format格式的好處是不需要關心引數的型別以及記憶%s、%d等的區別,且列印日誌時經常使用,降低了團隊成員的學習成本。

示例:

//錯誤碼定義
PARAM_ERROR(17, "引數非法,期望得到:{},實際得到:{}")
//錯誤碼使用
ErrorCodes.PARAM_ERROR.format(arg1, arg2);

實現方式:

org.slf4j.helpers.MessageFormatter.arrayFormat(this.message, args).getMessage()  

錯誤碼和異常

在日常業務開發中,對於異常使用最多的還是丟擲Java異常(Exception),異常又分為受檢查異常(Exception)和不受檢查異常(RuntimeException):

  • 受檢查的異常:這種在編譯時被強制檢查的異常稱為"受檢查的異常"。即在方法的宣告中宣告的異常。
  • 不受檢查的異常:在方法的宣告中沒有宣告,但在方法的執行過程中發生的各種異常被稱為"不被檢查的異常"。這種異常是錯誤,會被自動捕獲。

1、異常繫結錯誤碼

定義兩個父類,分別用於首檢查異常和非受檢查異常。可支援傳入錯誤碼,同時需要支援原始的異常傳參,這種場景會賦予一個預設的錯誤碼,比如:500伺服器內部異常

//父類定義
public abstract class BaseException extends Exception {

    protected BaseException(String message) {...}

    protected BaseException(String message, Throwable cause) {...}

    protected BaseException(Throwable cause) {...}

    protected BaseException(ErrorInfo errorInfo) {...}

    protected BaseException(ErrorCode errorCode) {...}

    protected BaseException(ErrorCode errorCode, Object... args) {...}
}

2、部分異常

使用異常能適用於大部分場景,不過對於多條目的場景不是很適合,比如需要批量儲存10條記錄,某些成功、某些失敗,這種場景就不適合直接丟擲異常。

在Node.js和Go語言中異常處理採用多返回值方式處理,第一個值是異常,如果為null則表示無異常。在Java裡建議採用vavr庫中的Either來實現,通常使用左值表示異常,而右值表示正常呼叫後的返回結果,即: Either<ErrorCode, T>

注意不推薦Pair、Tuple來實現,因為Either只能設定一個左值或右值,而Pair、Tuple無此限制。

錯誤碼和統一返回值

在前後端的互動中,後端一般使用JSON方式返回結果,整合前面說的錯誤碼,可定義以下格式:

{
   "code": number,
   "msg": string,
   "data": object
}

在SpringMVC中實現方式是自定義ResponseBodyAdvice和異常攔截,具體實現方式直接檢視:原始碼

實現了以上步驟之後就可以在SpringMVC框架中愉快的使用了,會自動處理異常及封裝成統一返回格式

    @GetMapping("/order")
    public Order getOrder(Long orderId) {
        return service.findById(orderId);
    }

總結

本文總結了設計錯誤碼需要考慮的各種因素,並給出了參考示例,基本能滿足一般中大型專案。規範有了最重要的還是落地,讓團隊成員遵守規範才能讓專案健康的迭代。

原始碼地址:https://github.com/sofn/app-engine/tree/master/common-error

本文連結:錯誤碼設計思考

作者簡介:木小豐,美團Java技術專家,專注分享軟體研發實踐、架構思考。歡迎關注公共號:Java研發

更多精彩文章:

Java執行緒池進階

從MVC到DDD的架構演進

平臺化建設思路淺談

構建可回滾的應用及上線checklist實踐

Maven依賴衝突問題排查經驗

相關文章