異常體系與項目實踐

程序式陰影:爲什麼不報錯?

一、簡介

在程序開發的過程中,異常處理從來都是一個複雜的維度,無論是新手還是經驗老到的選手,在編碼時都會面對各種異常情況;

程序中的異常可以反映系統的缺陷和待優化的點,並且是無法完全避免的,如何處理異常和降低異常出現的頻率,是系統質量的基礎保障;

隨着分佈式架構的流行,各種複雜的請求鏈路給異常處理帶來了巨大的麻煩,需要全面的監控來定位原因,才能快速的優化和解決;

二、異常體系

不論是JDK基礎,還是各類組件,在源碼中都涉及大量的異常封裝,從而精確的反映出描述信息,先來看看Java中的異常體系基礎;

Throwable:是所有錯誤「Error」和異常「Exception」的超類,

Error:通常是底層的不可恢復的類,此類錯誤一般都比較嚴重,JVM將終止其運行的線程;

Exception:程序自身可以捕獲並且可以預處理的異常,例如捕獲處理或者拋出;

針對「編譯器」來說,異常又分爲「檢查」異常和「非檢查」異常;

檢查異常:即編譯時異常,在編譯時期就會被編譯器查驗到的異常,這類異常要麼捕獲處理要麼拋出,否則就會報編譯錯誤;

非檢查異常:即運行時異常,在編譯時期不會被編譯器查驗到的異常,這類異常只有在程序運行的時候,纔會有可能被拋出;

三、異常用法

1、使用細節

Java異常處理關鍵字,分別是:「try」可能拋異常的代碼塊,「catch」捕獲異常、「finally」必須執行的代碼塊、「throw」方法內拋指定異常、「throws」方法聲明拋多個異常;

public class UseExe01 {
    public static void main(String[] args) {
        try {
            strStm ();
            ioStm();
        } catch (NullPointerException e) {
            System.out.println("空指針異常:"+e.getMessage());
            e.printStackTrace();
        } catch (IOException e) {
            System.out.println("IO流異常:"+e.getMessage());
            e.printStackTrace();
        } catch (Exception e) {
            System.out.println("異常:"+e.getMessage());
            e.printStackTrace();
        } finally {
            System.out.println("execute...finally");
        }
    }
    public static void ioStm () throws FileNotFoundException {
        new FileInputStream(new File("file_path"));
    }
    public static String strStm () throws NullPointerException {
        Object object = new Object() ;
        return object.getClass().getName() ;
    }
}

案例分析

細節分析

  • 如果「try」代碼塊中沒有拋出異常,執行完會跳到「finally」代碼塊;
  • 如果「try」代碼塊中拋出異常,則執行「catch」代碼塊,無論是否捕獲異常,最終都要執行「finally」代碼塊;
  • 可以存在多個「catch」代碼塊,但是最多隻匹配一個異常;
  • 捕獲異常與拋出異常的類型可以匹配,或者捕獲異常的類型是拋出異常的父類;
  • 在異常捕獲時,同一個繼承體系內,先捕獲子類異常,再捕獲父類異常;

2、返回值問題

在異常處理邏輯中,有一個非常經典的問題,就是「return」返回值,如果在「try.catch.finally」代碼塊中都存在「return」關鍵字,則要分情況討論;

2.1 值類型

public class UseExe02 {
    // 返回【2】
    public static int getInt1 () {
        try {
            int i = 1 / 0;
        } catch (ArithmeticException e){
            e.printStackTrace();
            return 1;
        } finally {
            System.out.println("execute...finally");
            return 2;
        }
    }
    // 返回【1】
    public static int getInt2 () {
        int a = 1;
        try{
            int i = 1/0;
            return a;
        }catch (ArithmeticException e){
            e.printStackTrace();
            return a;
        }finally {
            ++a;
            System.out.println("execute...finally");
        }
    }
    // 返回【3】
    public static int getInt3 () {
        int a = 1;
        try{
            int i = 1/0;
            a++;
            return a ;
        }catch (ArithmeticException e){
            a++;
            e.printStackTrace();
        }finally {
            a++;
            System.out.println("execute...finally");
        }
        return a ;
    }
}

邏輯分析

2.2 引用類型

public class UseExe03 {
    // 返回【張三】
    public static String getStr1 () {
        String var ;
        try {
            var = new String("張三");
            return var ;
        } catch (ArithmeticException e){
            e.printStackTrace();
        } finally {
            var = new String("李四");
            System.out.println("execute...finally:"+var);
        }
        return var ;
    }
    // 返回【李四】
    public static String getStr2 () {
        String var ;
        try{
            int i = 1/0;
            var = new String("張三");
            return var;
        }catch (ArithmeticException e){
            e.printStackTrace();
            var = new String("李四");
            return var;
        }finally {
            var = new String("王五");
            System.out.println("execute...finally:"+var);
        }
    }
    // 返回【王五】
    public static String getStr3 () {
        String var ;
        try{
            int i = 1/0;
            var = new String("張三");
            return var ;
        }catch (ArithmeticException e){
            var = new String("李四");
            e.printStackTrace();
        }finally {
            var = new String("王五");
            System.out.println("execute...finally:"+var);
        }
        return var ;
    }
}

邏輯分析

2.3 結論說明

  • 如果只有「try」代碼塊中有「return」關鍵字,邏輯執行正常則得到「try」處的返回值;
  • 如果只有「try.catch」代碼塊中有「return」關鍵字,「try」代碼塊異常,「catch」代碼塊執行正常,則得到「catch」處的返回值;
  • 如果「finally」代碼塊中有「return」關鍵字,當該代碼塊執行正常時會得到此處的返回值;

值得說明的一點是,從異常的設計原理來來說,並不推薦在「finally」代碼塊中使用「return」關鍵字,可能會導致程序提前結束,這也是常見的開發規範;

四、項目實踐

1、異常定義

對於複雜的分佈式工程來說,系統發生問題時,十分依賴異常信息的捕獲,從而快速定位原因和解決;

項目在處理異常時,需要考慮兩個核心維度:「1」捕獲和解決異常信息,「2」傳遞異常信息到應用端,從而引導用戶的動作;

在系統中,通常依賴很多自定義的異常,比如常見:系統異常,業務異常,第三方異常;基本都是「運行時」異常;

系統異常:比如超時請求或者服務級別異常,導致流程無法執行,需要研發人員介入處理;

業務異常:基於響應的提示信息,用戶可以自行解決的問題,比如常見的參數校驗,授權問題等;

第三方異常:可以是內部不同系統的交互,也可以是第三方的交互,可能會涉及到各種響應狀態,通過內部的封裝進行統一管理,並且要保留第三方的響應;

2、異常封裝

基於運行時異常「RuntimeException」類,分別定義「系統」、「業務」、「第三方」三類異常;

自定義異常基礎類,注意此處省略很多構造方法,作爲「RuntimeException」的子類,具體參考其源碼的構造方法即可;

public class BaseExe extends RuntimeException {
    private String code ;
    public BaseExe (String code,String msg) {
        super(msg);
        this.code = code ;
    }
    public BaseExe(String message, Throwable cause) {
        super(message, cause);
    }
    // 省略其他構造方法
}

系統異常類,並提供常用的系統異常信息枚舉類;

public enum SysExeCode {
    SYSTEM_EXE("S00000", "系統異常");
}
public class SysException extends BaseExe {
    public SysException(String code, String msg) {
        super(code, msg);
    }
    public SysException(SysExeCode sysExeCode) {
        super(sysExeCode.getCode(), sysExeCode.getMsg());
    }
}

業務異常類,並提供常用的業務異常信息枚舉類;

public enum BizExeCode {
    BIZ_EXE("B00000", "業務異常");
}
public class BizException extends BaseExe {
    public BizException(String code, String msg) {
        super(code, msg);
    }
    public BizException(BizExeCode bizExeCode) {
        super(bizExeCode.getCode(), bizExeCode.getMsg());
    }
}

第三方異常類,並提供常用的第三方異常信息枚舉類;

public enum ThirdExeCode {
    THIRD_EXE("T00000", "第三方異常");
}
public class ThirdException extends BaseExe {
    // 第三方交互異常響應信息
    private String thirdCode ;
    private String thirdMsg ;
    public ThirdException(String code, String msg) {
        super(code, msg);
    }
    public ThirdException(String code, String msg,String thirdCode,String thirdMsg) {
        super(code, msg);
        this.thirdCode = thirdCode ;
        this.thirdMsg = thirdMsg ;
    }
    public ThirdException(ThirdExeCode thirdExeCode,String thirdCode,String thirdMsg) {
        super(thirdExeCode.getCode(), thirdExeCode.getMsg());
        this.thirdCode = thirdCode ;
        this.thirdMsg = thirdMsg ;
    }
}

從開發規範來說,不允許在代碼中隨意添加異常描述信息,必須都維護在相應的枚舉類中,不同的異常類型,要在合適的場景下拋出,儘量由最上層統一捕獲並處理,再轉換爲統一的響應結果;

3、異常處理

3.1 響應方式

在微服務項目中,通常採用RestControllerAdviceExceptionHandler註解,實現全局異常的捕獲和處理;

@RestControllerAdvice
public class ExeHandler {
    /**
     * 默認異常
     */
    @ExceptionHandler(value = Exception.class)
    public void defaultException(Exception e) {
        // 統一返回
    }
    /**
     * 系統異常
     */
    @ExceptionHandler(value = SysException.class)
    public void sysException(SysException e) {
        // 統一返回
    }
    /**
     * 業務異常
     */
    @ExceptionHandler(value = BizException.class)
    public void bizException(BizException e) {
        // 統一返回
    }
    /**
     * 第三方異常
     */
    @ExceptionHandler(value = ThirdException.class)
    public void thirdException(ThirdException e) {
        // 統一返回
    }
}

3.2 記錄方式

通常在一些核心的業務流程中,會通過註解的方式記錄日誌,於研發而言,最關心的還是異常日誌,以此爲邏輯優化的關鍵依據;

比較常用的技術手段是自定義註解+切面編程來實現,細節參考開源倉庫中《集成日誌,複雜業務下的自定義實現》篇幅內容;

@Component
@Aspect
public class LogAop {
    /**
     * 日誌切入點
     */
    @Pointcut("@annotation(com.defined.log.annotation.DefinedLog)")
    public void logPointCut() {
    }
    /**
     * 環繞切入
     */
    @Around("logPointCut()")
    public Object around (ProceedingJoinPoint proceedingJoinPoint) {
        try{
            // 執行方法
            result = proceedingJoinPoint.proceed();
        } catch (SysException e){
            // 系統異常
        } catch (BizException e){
            // 業務異常
        } catch (ThirdException e){
            // 第三方異常
        } catch (Exception e){
            // 默認異常
        } finally {
            // 信息處理
        }
        return result ;
    }
}

4、異常通知

拋開業務異常不說,對於「系統」和「第三方」異常,通常都會第一時間觸達到研發,從而快速定位原因和處理;

一般會根據異常的級別,將進行不同維度的消息觸達,比如某微,某釘,郵件,短信等;

從技術的實現上來看,常規也是採用切面編程的方式,細節參考開源倉庫中《基於AOP切面,實現系統告警功能》篇幅內容;關於消息中心的搭建設計,同樣可以參考開源倉庫中《聊聊消息中心的設計與實現邏輯》篇幅內容;

5、系統故障

從系統架構的層面來分析,大部分組件都提供了必要的監控能力,而這種監控手段的核心價值在於快速發現故障,並且提供一定的分析能力;

比如分佈式系統中,複雜的請求的鏈路,對於故障的定位和排查難度都是極大的,需要將各種組件的監控信息進行統籌分析;

系統層面監控

請求鏈路分析

日誌記錄能力

可以從關鍵的日誌記錄作爲問題切入點,再基於系統層面的監控能力縮小問題範圍,分析請求鏈路的異常原因,最後通過完整的日誌分析細節,從而提升問題解決的效率;

關於這些技術的應用,在開源倉庫中都有詳細案例,此處不再贅述;

五、參考源碼

編程文檔:
https://gitee.com/cicadasmile/butte-java-note

應用倉庫:
https://gitee.com/cicadasmile/butte-flyer-parent
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章