論一個優秀的工程師應該如何做好異常處理和日誌記錄

攻城獅Chova發表於2021-06-30

異常處理

  • Java類庫中定義的可以通過預檢查方式規避的RuntimeException異常不應該通過catch方式來處理:
    • NullPointerException
    • IndexOutofBoundsException
    • 無法通過預檢查的異常除外: 在解析字串形式數字時,不得不通過catch NumberFormatException來實現
if (obj != null) {}
  • 異常不要用來做流程控制,條件控制:
    • 異常設計的初衷是解決程式執行中的各種意外情況,且異常的處理效率比條件判斷方式要低很多
  • 使用catch時要區分穩定程式碼和非穩定程式碼:
    • 穩定程式碼: 無論如何不會出錯的程式碼
    • 非穩定程式碼: 非穩定程式碼的catch儘可能區分異常型別,再做對應處理
    • 對於大段程式碼進行try - catch,會使得程式無法根據不同的異常做出正確的應激反應,也不利於定位問題
      • 在使用者註冊場景中,如果使用者輸入非法字元,或者使用者名稱稱已存在,或者使用者密碼過於簡單,在程式上作出分門別類的判斷,並提示給使用者
  • 捕獲異常是為了處理,不要捕獲了什麼都不處理.如果不需要處理,應該將異常拋給呼叫者
    • 最外層的業務使用者,必須處理異常,將其轉化為使用者可以理解的內容
  • 如果有try塊放到了事務程式碼中 ,catch異常後,如果需要回滾事務,一定要注意手動回滾事務
  • finally塊必須對資源物件,流物件進行關閉,有異常也要做try - catch
    • JDK 7以後,可以使用try - with - resources 方式
  • 不要在finally塊中使用return:
    • finally塊中的return返回後方法結束執行,不會再執行try塊中的return語句
  • 捕獲異常與丟擲異常必須完全匹配,或者是拋異常的父類
  • 方法的返回值可以為null,不強制返回空集合或者空物件等,必須新增註釋充分說明什麼情況下會返回null值
    • 即使呼叫方法返回空集合或者空物件,對於呼叫者來說,必須考慮到遠端呼叫失敗,序列化失敗,執行時異常等返回null的場景
  • 一定要防止出現NPE異常,注意NPE產生的場景:
    • 返回型別為基本資料型別,return包裝資料型別的物件時, 自動拆箱有可能產生NPE
    • 資料庫的查詢結果可能為null
    • 集合裡的元素即使isNotEmpty, 取出的資料元素也可能為null
    • 遠端呼叫返回物件時,一律要進行空指標判斷,防止NPE
    • 對於Session中獲取的資料,建議進行NPE檢查,避免空指標
    • 級聯呼叫obj.getA().getB.getC(), 一連串的呼叫,容易產生NPE
    • JDK 8使用Optional類來防止NPE問題
  • 定義時區分uncheckedchecked異常,避免直接丟擲new RuntimeException(), 不允許丟擲Exception或者Throwable, 應該使用有業務含義的自定義異常
    • 推薦使用業務界已定義過的異常:
      • DAOException
      • ServiceException
  • 對於公司外的http或者api開放介面必須使用 "錯誤碼"; 應用內部推薦異常丟擲; 跨應用間的RPC呼叫優先考慮使用Result方式,封裝isSuccess()方法,錯誤碼,錯誤簡簡訊息
    • RPC方法使用Result方式的原因:
      • 使用拋異常返回方式,呼叫方如果沒有捕獲到就會產生執行時錯誤
      • 如果不加棧資訊,只是new自定義異常,加入自己理解的error message, 對於呼叫端解決問題的幫助不會太多.如果加了棧資訊,在頻繁呼叫出錯的情況下,資料序列化和傳輸的效能損耗也是問題
  • 避免出現重複的程式碼,即DRY(Don't Repeat Yourself)原則:
    • 重複的程式碼在以後的修改時,需要修改所有的副本,容易遺漏
    • 抽取共性方法,或者抽象公共類,或者元件化
      • 一個類中有多個public方法,都需要進行數行相同的引數校驗工作,這個時候就要進行抽取:
private boolean checkParam(DTO dto) {...}

日誌規約

  • 應用中不可直接使用日誌系統(log4j,logback)中的API,應該使用日誌框架slf4j中的API, 使用門面模式的日誌框架,有利於維護和各個類的日誌處理方式統一
  • 日誌檔案至少儲存15天,因為有些異常具備以 "周" 為頻次發生的特點
  • 應用中的擴充套件日誌(打點,臨時監控,訪問日誌等)命名方式:
    • appName_logType_logName.log
      • logType: 日誌型別,如 stats,monitor,access
      • logName: 日誌描述
      • 這樣通過檔名就可以知道日誌檔案屬於什麼應用,什麼型別,什麼目的,也方便歸類查詢
        • mppserver應用中單獨監控時區轉換異常: mppserver_monitor_timeZoneConvert.log
      • 對日誌進行分類,比如將錯誤日誌和業務日誌分開存放,便於開發人員檢視,也便於對日誌系統進行及時監控
  • trace,debug,info級別的日誌輸出,必須使用條件輸出形式或者使用佔位符方式
    logger.debug("Processing trade with id: " + id + " and symbol: " + symbol);
    
    • 如果日誌級別是warn: 上述日誌不會列印,但是或執行字串拼接操作
    • 如果symbol是物件,會執行toString() 方法,浪費了系統資源,執行上述操作,最終日誌卻沒有列印
    • 使用條件輸出形式:
    if (logger.isDebugEnabled()) {
    	logger.debug("Processing trade with id: " + id + " and symbol: " + symbol);
    }
    
    • 使用佔位符輸出形式:
    logger.debug("Processing trade with id: {} and symbol: {}, id, symbol);
    
  • 避免重複列印日誌,浪費磁碟空間,必須在log4j.xml中設定additivity=false
<logger name="com.oxford.dubbo.config" additivity="false">
  • 異常資訊包括:
    • 案發現場資訊
    • 異常堆疊資訊
    • 如果不處理,應該通過異常關鍵字throws向上丟擲
logger.error(各類引數或者物件toString() + "_" + e.getMessage(), e);
  • 謹慎的記錄日誌:
    • 生產環境禁止輸出debug日誌
    • 有選擇地輸出info日誌
    • 如果使用warn來記錄剛上線時的業務行為資訊,一定要注意日誌輸出量問題,避免伺服器內容過多,並及時刪除這些觀察日誌
      • 大量地輸出無效日誌,不利於系統效能的提升,也不利於快速定位錯誤點
      • 記錄日誌時需要思考:
        • 這些日誌真的有人看嗎?
        • 看到這條日誌能夠做什麼?
        • 能不能給排查問題帶來好處?
  • 可以使用warn日誌級別來記錄使用者輸入引數錯誤的情況
  • 注意日誌的輸出級別:
    • error級別只記錄系統邏輯出錯,異常或者重要的錯誤資訊
  • 使用全英文來註釋和描述日誌錯誤資訊

相關文章