程序式陰影:爲什麼不報錯?
一、簡介
在程序開發的過程中,異常處理從來都是一個複雜的維度,無論是新手還是經驗老到的選手,在編碼時都會面對各種異常情況;
程序中的異常可以反映系統的缺陷和待優化的點,並且是無法完全避免的,如何處理異常和降低異常出現的頻率,是系統質量的基礎保障;
隨着分佈式架構的流行,各種複雜的請求鏈路給異常處理帶來了巨大的麻煩,需要全面的監控來定位原因,才能快速的優化和解決;
二、異常體系
不論是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 響應方式
在微服務項目中,通常採用RestControllerAdvice
和ExceptionHandler
註解,實現全局異常的捕獲和處理;
@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