天天看點

Java異常的正确使用姿勢

最近在項目代碼中,遇見異常濫用的情形,會帶來什麼樣的後果呢?

1. 代碼可讀性變差,業務邏輯難以了解

異常流與業務狀态流混在一起,無法從接口協定層面了解業務代碼,隻能深入到方法(Method)内部才能準确了解傳回值的行為。

可看一下代碼:

public UserProfile findByID(long user_id) {
		Map<String, Object> cond = new HashMap<String, Object>();
		cond.put("id", user_id);
		UserProfile userInfo = null;
		try {
			userInfo = DBUtil.selecta(UserProfile.class, "user_info", cond);
		} catch (Throwable e) {
			log.error(e, "UserProfile findByID");
		}
		return userInfo;
	}
           

DAO層負責資料庫的基本操作,該方法傳回值為查詢結果使用者對象資料。代碼強行抓了所有的異常,并以null傳回,後來人無法确認null是代表該使用者不存在還是出現異常。

2. 代碼健壯性變差,異常資訊被随意捕捉,甚至被吃掉

同樣上述代碼,首先抓了Throwable這個所有異常,包括Error(後文會介紹異常體系)。代碼内部隐藏了問題,隻是列印了一行日志,并且讓程式可以正常繼續往後走,帶來的不确定性和風險都很大,這也極大的影響代碼的健壯。

3. 破壞架構的分層清晰,職責單一的原則,為系統擴充帶來很大阻礙

随着系統的發展,往往會沉澱出一些平台系統,比如調用監控。會負責統一采集系統的各類資訊,因為這樣的錯誤異常處理,将很難統一分離出異常資訊。

那我們在實際編寫代碼的如何正确考慮異常的使用呢?

首先了解下Java異常的設計初衷。

Exceptions are the customary way in Java to indicate to a calling method that an abnormal condition has occurred. 一個很重要的概念——不正常情形。Java異常旨在處理方法調用時不正常情形,但我們該如何了解“不正常情形”?

看下圖,JDK給我們定義了以下異常體系:

Java異常的正确使用姿勢

根節點是Throwable,代表Java内所有可以Catch的異常都繼承此,向下有兩類,Exception和Error,日常用到較多的都是Exception,Error一般留給JDK内部自己使用,比如記憶體溢出OutOfMemoryError,這類嚴重的問題,應用程序什麼都做不了,隻能終止。使用者抓住此類Error,一般無法處理,盡快終止往往是最安全的方式,既然什麼都幹不了就沒必要抓住了。Exception是應用代碼要重點關心的,其下又分為運作時異常RuntimeException和編譯時異常,各自區分就不在詳述了。

了解異常的結構後,接下來解決兩個重要問題,何時抛異常和抛什麼異常,何時抓異常和抓什麼異常 何時會有異常抛出,總結起來有以下三個典型的場景:

1. 調用方(Client)破壞了協定

說白了就是調用方法時沒有按照約定好的規範來傳參數,典型的比如參數是個非空集合卻傳入了空值。這種破壞協定的還可以細分兩類,一類是調用方從接口形式上不易覺察的規則但需要在出現時給調用方些強提示,帶些資訊上去,這時異常就是特别好的方式;另一類是調用方可以明确看到的規則,正常情況會正常處理協定,不會産生破壞,但可能因為bug導緻破壞協定。

public void method(String[] args) {
        int temperature = 0;
        if (args.length > 0) {
            try {
                temperature = Integer.parseInt(args[0]);
            }
            catch(NumberFormatException e) {
                throw new IllegalArgumentException(
                    "Must enter integer as first argument, args[0]="+args[0],e);
            }
        }
        // 其他代碼
}
           

要求傳入整數,但轉換數字時出錯,此時可抛出特定異常并附上提示資訊

2. (Method)知道有問題,但自己處理不了

這裡"有問題",可能是調用方破壞了協定,但是單獨提出來是要從被調用方出發考慮,比如Method内部有讀取檔案操作,但發現檔案并不存在

public static void main(String[] args) {
        if (args.length == 0) {
            System.out.println("Must give filename as first arg.");
            return;
        }
        FileInputStream in = null;
        try {
            in = new FileInputStream(args[0]);
        }
        catch (FileNotFoundException e) {
            System.out.println("Can't find file: " + args[0]);
            return;
        }
        // 其他代碼
}
           

FileInputStream在建立時抛出了FileNotFoundException,顯然出現該問題時FileInputStream是處理不了的。

3. 預料不到的情形

空指針異常、數組越界是這種典型的場景,一般是由于有代碼分支被忽略

了解了何時會出現異常,但是需要抛出異常時是選擇編譯時異常還是運作時異常呢? 很多人可能會說,很簡單啊,需要調用方catch的就編譯時,否則運作時。問題來了,什麼時候需要調用方catch?

分析編譯時和運作時對代碼編寫的影響,可以總結出來區分時考慮的點有:調用方能否處理、嚴重程度、出現的可能性。

調用方能處理->編譯時

調用方不能處理->運作時

嚴重程度高->運作時

出現可能性低->運作時

本人細化了這個分類的考慮過程如下:

Java異常的正确使用姿勢

首先從調用方開始考慮,如果是調用方破壞了協定,則抛出運作時異常,這類異常一般出現可能性較低,調用方已知,是以沒必要強制調用方抓此異常。

然後如果問題出現被調用方,無法正常執行完成工作,這時候考慮該問題調用方是否可以處理,如果能處理,比如檔案找不到、網絡逾時,則抛出編譯時異常,否則比如磁盤滿,抛運作時異常

解決了何時抛異常和抛什麼異常,接下來是調用這些有異常的代碼時,何時catch和catch什麼異常呢? 攻守不分離... 免不了俗,總結一下幾點供大家探讨:

  1. 不要輕易抓Throwable,圖省事可能會帶來巨大的隐患
try {
   someMethod();
} catch (Throwable e) {
   log.error("method has failed", e);
}
           

應該盡量隻去抓關注的異常,明确catch的都是什麼具體的異常

  1. 自己處理不了,不要抓

比如上文DB可能會有異常,在DAO層是處理不了這種問題的,交由上層處理。抓異常宜晚不宜早,抛異常宜早不宜遲。

  1. 切忌抓了,又把異常吞掉,不留下一絲痕迹

抓住異常,打行日志完事兒,不是一個好習慣。

  1. 切忌抓異常了将異常狀态流和業務狀态流混在一起,這樣你算是徹底抛棄了Java的異常機制

版權聲明:本文為CSDN部落客「weixin_33831196」的原創文章,遵循CC 4.0 BY-SA版權協定,轉載請附上原文出處連結及本聲明。

原文連結:https://blog.csdn.net/weixin_33831196/article/details/91924478