錯誤碼設計思考


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

本文中涉及的源碼: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依賴沖突問題排查經驗


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM