程式式陰影:為什么不報錯?
一、簡介
在程式開發的程序中,例外處理從來都是一個復雜的維度,無論是新手還是經驗老到的選手,在編碼時都會面對各種例外情況;
程式中的例外可以反映系統的缺陷和待優化的點,并且是無法完全避免的,如何處理例外和降低例外出現的頻率,是系統質量的基礎保障;
隨著分布式架構的流行,各種復雜的請求鏈路給例外處理帶來了巨大的麻煩,需要全面的監控來定位原因,才能快速的優化和解決;
二、例外體系
不論是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 = https://www.cnblogs.com/cicada-smile/p/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
Gitee主頁: https://gitee.com/cicadasmile/butte-java-note
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/552901.html
標籤:Java
上一篇:Linux網路編程:socket & pthread_create()多執行緒 實作clients/server通信
下一篇:返回列表
