啪,還敢丟擲異常

xiezhr發表於2024-03-25

前言

去年又重新刷了路遙的《平凡的世界》,最近也在朋友推薦下,來看了路遙的另一部成名作《人生》。
故事中的主人公高加林,雖生在農村,面朝黃土背朝天,卻不甘心像父輩一樣或者,一心想著擺脫民語的束縛,追求他的理想生活。
然而命運多舛,在他所想象的理想生活中,一次次跌倒,最終不得不承認自己的平凡,生活總得繼續。
現實世界如此,程式碼世界裡,我們也希望一切都是理想的。
我們希望使用者輸入的資料格式永遠是正確的,開啟的資源也一定存在,使用者的硬體是正常的,使用者的作業系統四穩定的,使用者的網路也一定是暢通的等等
還敢拋異常
然而事與願違,願望是好的,現實是殘酷的。
引入異常機制的目的就在於,當“人生”中出現異常時,能夠將其捕獲並處理,保證我們能更好的走下去,不至於出現一點插曲,就停滯不前。

一、異常引入

因此呢就出現了異常處理,我們把可能出現異常的業務程式碼放到try塊中定義,把異常處理放到catch塊中進行處理,保證程式遇到異常依然能繼續執行,保證程式的健壯性。

① 我們來看看高加林的一生中各種異常的處理

try{
	//業務邏輯,高加林的一生
	System.out.println("1、高中畢業,雖然沒考上大學。卻不斷學習,在報紙上發表詩歌和散文,參加考試,謀得一份臨時教師職位");
	System.out.println("2、高加林的叔叔從外地回到家鄉工作,在縣城擔任勞動局局長,副局長為了討好新上任的局長;");
	System.out.println("便私下給高加林走了後門,就這樣高加林成為了公職人員");
	System.out.println("3、與高中同學黃亞萍相遇,再次相遇的兩個人,志趣相投,相聊甚歡。高加林以為攀上黃亞萍這個高枝,能到大城市一展宏圖");
}catch(ExceptionClass1 e1){
	System.out.println("第一個異常發生,教師職位被他人頂替");
	System.out.println("沒有了工作,重新回到農村,成為最不想當的農民");
	System.out.println("很快加入村裡的勞動隊伍裡,白天努力勞作,晚上看書學習,等待著東山再起的機會");
}catch(ExceptionClass2 e2){
	System.out.println("第二個異常發生,遭人舉報走後門");
	System.out.println("再次丟了工作,一直想擺脫農名身份的他,再次成為了農民");
}catch(ExceptionClass3 e3){
	System.out.println("第三個異常發生,紙包不住火,黃亞萍及其家人知道了高加林遭遇舉報");
	System.out.println("被打回原型,和黃亞萍斷絕了關係");
	System.out.println("黃亞萍不想高加林回農村,希望高加林去找叔父幫忙,看是否可以繼續留下來");
}catch(Exception e){
	System.out.println("再次跌倒的他,沒再去找叔父");
	System.out.println("有了清醒的認知,自己的路還是得靠自己走下去");
}finally{
	System.out.println("接受現實,更好的走下去");
}

② 程式碼中

//未處理異常,程式遇到異常沒法繼續執行
public class ExceptionTest {
    public static void main(String[] args) {
        int num1 =7;
        int num2 =0;
        int res = num1/num2;
        System.out.println("程式繼續執行......");
    }
}
//輸出
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at ExceptionTest.main(ExceptionTest.java:5)

加入異常處理

//加入異常處理,程式遇到異常後繼續執行
public class ExceptionTest {
    public static void main(String[] args) {
        int num1 =7;
        int num2 =0;
        try {
            int res = num1/num2;
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
        System.out.println("程式繼續執行......");
    }
}
//輸出
/ by zero
程式繼續執行......

小結: 如果執行try塊裡的業務邏輯程式碼出現異常,系統會自動生成一個異常物件,該異常物件被體驕傲給Java執行時環境,這個過程成為丟擲異常
Java執行時環境收到異常物件時,會尋找能處理異常物件的catch塊,如果找到合適的catch塊,則把該異常物件交給該catch塊處理,這個過程被成為異常捕獲;
如果Java執行環境找不到捕獲異常的catch塊,則執行時環境終止,Java程式已將退出

二、基本概念

程式執行中發生的不正常情況(語法錯誤邏輯錯誤不是異常)稱為異常,所有的異常都繼承與java.lang.Throwable Throwable 有兩個重要子類

  • Error(錯誤):Java虛擬機器無法解決的嚴重問題,我們沒辦法透過 catch 來進行捕獲 。如:記憶體溢位(OutOfMemoryError)、Java 虛擬機器執行錯誤(Virtual MachineError)、類定義錯誤(NoClassDefFoundError)等。error 是嚴重錯誤,Java虛擬機器會選擇執行緒終止
  • Exception(異常):程式設計錯誤或偶爾的外在因素導致的一般問題,程式本身可以處理的異常,可以透過 catch 來進行捕獲。Exception 又可以分 執行時異常(程式執行時發生的異常)和編譯時異常(程式編譯期間產生的異常,必須要處理的異常,否則程式碼沒法編譯透過)。

在這裡插入圖片描述

三、異常繼承體系

:虛線為實現,實線為繼承
在這裡插入圖片描述

四、常見執行異常

4.1 NullPointerException 空指標異常

在這裡插入圖片描述

public class ExceptionTest {
    public static void main(String[] args) {
       String str = null;
        System.out.println(str.length());
    }
}
//輸出
Exception in thread "main" java.lang.NullPointerException
	at ExceptionTest.main(ExceptionTest.java:4)

4.2 ArithmeticException 數學運算異常

在這裡插入圖片描述

public class ExceptionTest {
    public static void main(String[] args) {
      int num1=7;
      int num2=0;
      int res = num1/num2;
    }
}
//輸出
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at ExceptionTest.main(ExceptionTest.java:5)

4.3 ArrayIndexOutOfBoundsException 陣列下標越界異常

在這裡插入圖片描述

public class ExceptionTest {
    public static void main(String[] args) {
      String strarr[] ={"個人部落格","www.xiezhrspace.cn","公眾號","XiezhrSpace"};
        //注意陣列下標時從0開始的
        for (int i = 0; i <= strarr.length; i++) {
            System.out.println(strarr[i]);
        }
    }
}
//輸出
個人部落格
www.xiezhrspace.cn
公眾號
XiezhrSpace
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 4
	at ExceptionTest.main(ExceptionTest.java:5)

4.4 ClassCastException 型別轉換異常

在這裡插入圖片描述

public class ExceptionTest {
    public static void main(String[] args) {
        Class1 class1 = new Class2();  //向上轉型
        Class2 class2 = (Class2)class1; //向下轉型
        Class3 class3= (Class3)class1;  //兩個類沒有關係,轉型失敗
    }
}

class Class1{}

class Class2 extends Class1{};
class Class3 extends Class1{};

//輸出
Exception in thread "main" java.lang.ClassCastException: Class2 cannot be cast to Class3
	at ExceptionTest.main(ExceptionTest.java:5)

4.5 NumberFormatException 數字格式不正確異常

在這裡插入圖片描述

public class ExceptionTest {
    public static void main(String[] args) {
      String str1 ="123";
      String str2 = "abc";
        System.out.println(Integer.valueOf(str1));   //可以轉成功
        System.out.println(Integer.valueOf(str2));
    }
}
//輸出
123
Exception in thread "main" java.lang.NumberFormatException: For input string: "abc"
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Integer.parseInt(Integer.java:580)
	at java.lang.Integer.valueOf(Integer.java:766)
	at ExceptionTest.main(ExceptionTest.java:6)

五、常見編譯時異常

編譯期間就必須處理的異常,否則程式碼不能透過編譯
在這裡插入圖片描述

5.1 FileNotFoundException 操作一個不存在的檔案時候發生異常

在這裡插入圖片描述

5.2 ClassNotFoundException 載入類,類不存在時異常

在這裡插入圖片描述

5.3 SQLException 運算元據庫,查詢表時發生異常

在這裡插入圖片描述

5.4 IllegalArgumentException 引數不匹配時發生異常

在這裡插入圖片描述

六、異常處理

Java 的異常處理透過 5 個關鍵字來實現:trycatchthrowthrowsfinally
try catch 語句用於捕獲並自行處理異常
finally 語句用於在任何情況下(除特殊情況外)都必須執行的程式碼
throw 手動生成異常物件
throws 將發生的異常丟擲,交給呼叫者處理,最頂級的處理者是JVM

6.1 異常處理方式

  • try-catch-finally :在程式碼中捕獲異常自行處理
  • throws :將發生的異常丟擲,交給呼叫者處理,最頂級處理者是JVM

注: try-catch-finally 和throws 任選一種即可

6.2 異常處理

6.2.1 try-catch-finally

①原理圖
在這裡插入圖片描述
②語法結構

try {
    邏輯程式塊  //可能有異常的程式碼
} catch(Exception e) {
	/*
	①發生異常時,系統將異常封裝成Exception物件e,並傳遞給catch
	②得到異常Exception e 後,程式設計師自行處理
	注:只有發生異常時候,catch程式碼塊才執行
    */
    捕獲異常
    throw(e);  
} finally {
    釋放資原始碼塊
}

③例項
小提示:選中程式碼,按下快捷鍵ctrl+alt+t 可以撥出程式碼提示

public class TestException {
    public static void main(String[] args) {
        int num1 =10;
        int num2 =0;

        try {
            int num3=num1/num2;
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            System.out.println("finally程式碼塊被執行了");
        }

        System.out.println("程式繼續執行...");
    }
}
//輸出
java.lang.ArithmeticException: / by zero
	at com.xiezhr.TestException.main(TestException.java:9)
finally程式碼塊被執行了
程式繼續執行...

上述程式碼中,把可能出現異常的程式碼num1/num2; 放到了try語句塊中,當程式碼發生異常時,在catch中捕獲異常,並列印異常。不管有沒有發生異常,finally 語句塊裡的程式碼都會執行。發生異常後程式並沒有終止,最後輸出 “程式繼續執行...”

6.2.2 try-with-resources

Java中,對於檔案操作IO流、資料庫連線等開銷非常昂貴的資源,用完之後必須及時透過close方法將>其關閉,否則資源會一直處於開啟狀態,可能會導致記憶體洩露等問題。
關閉資源的常用方式就是在finally塊裡呼叫close方法將資源關閉

所以對於流的操作我們經常回用到如下程式碼

//讀取文字檔案的內容
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
public class TestException {
    public static void main(String[] args) {
        Scanner scanner = null;
        try {
            scanner = new Scanner(new File("D://xiezhr.txt"));
            while (scanner.hasNext()) {
                System.out.println(scanner.nextLine());
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            if (scanner != null) {
                scanner.close();
            }
        }
    }

}
//輸出
個人部落格:www.xiezhrspace.cn
個人公眾號:XiezhrSpace
歡迎你關注

分析: 上述程式碼中我們透過io流讀取D盤xiezhr.txt檔案中的內容,並將其內容按行列印出來。最後在finally程式碼塊中將scanner關閉

Java 7 之後,新的語法糖 try-with-resources 可以簡寫上述程式碼

import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;

public class TestException {
    public static void main(String[] args) {
        try(Scanner scanner = new Scanner(new File("D://xiezhr.txt"))){
            while (scanner.hasNext()) {
                System.out.println(scanner.nextLine());
            }
        }catch (FileNotFoundException e){
            e.printStackTrace();
        }
    }
}
//輸出
個人部落格:www.xiezhrspace.cn
個人公眾號:XiezhrSpace
歡迎你關注

兩段程式碼實現的功能是一樣的,但明顯try-with-resources 寫法簡單了好多。我們也更提倡優先使用 try-with-resources 而不是try-finally

6.2.3 丟擲異常

當一個方法產生某種異常,但是不確定如何處理這種異常,那麼就需要在該方法的頭部顯示地申明丟擲異常,表明該方法將不對這些異常進行處理,而用該方法呼叫者負責處理

6.2.3.1 thorows

①語法格式
returnType method_name(paramList) throws Exception 1,Exception2,…{…}

  • returnType 表示返回值型別
  • method_name 表示方法名
  • paramList 表示引數列表;
  • Exception 1,Exception2,… 表示異常類
    如果有多個異常類,它們之間用逗號分隔。這些異常類可以是方法中呼叫了可能丟擲異常的方法而產生的異常,也可以是方法體中生成並丟擲的異常

②使用場景
當前方法不知道如何處理這種型別的異常,該異常應該由向上一級的呼叫者處理;
如果 main 方法也不知道如何處理這種型別的異常,也可以使用 throws 宣告丟擲異常,該異常將交給 JVM 處理。
JVM 對異常的處理方法是,列印異常的跟蹤棧資訊,並中止程式執行,這就是前面程式在遇到異常後自動結束的原因

③實踐操作

import java.io.File;
import java.io.IOException;
import java.util.Scanner;

public class TestException {
    //定義方法時宣告丟擲異常,方法中出現的異常自己不處理,交由呼叫者處理
    public void readfile() throws IOException {
        // 讀取檔案
        Scanner scanner = new Scanner(new File("D://xiezhr.txt"));
        while (scanner.hasNext()) {
            System.out.println(scanner.nextLine());
        }
        scanner.close();
    }
    public static void main(String[] args)  {
        TestException tt = new TestException();
        try {
            //呼叫readfile方法
            tt.readfile();
        } catch (IOException e) {
            //列印異常
            e.printStackTrace();
        }
    }
}
//輸出
個人部落格:www.xiezhrspace.cn
個人公眾號:XiezhrSpace
歡迎你關注

分析: 以上程式碼,首先在定義 readFile() 方法時用 throws 關鍵字宣告在該方法中可能產生的異常,然後在 main() 方法中呼叫readFile() 方法,並使用 catch 語句捕獲產生的異常

④ 異常處理流程圖
在這裡插入圖片描述

注意:子類方法宣告丟擲的異常型別應該是父類方法宣告丟擲的異常型別的子類或相同,子類方法宣告丟擲的異常不允許比父類方法宣告丟擲的異常多。

//下面程式編譯就報錯,原因時子類丟擲比父類還大的異常

 public class OverrideThrows {
        public void test() throws IOException {
            FileInputStream fis = new FileInputStream("a.txt");
        }
    }
  class Sub extends OverrideThrows {
      // 子類方法宣告丟擲了比父類方法更大的異常
      public void test() throws Exception {
      }
  }
6.2.3.1 throw

throw用來直接丟擲一個異常

①語法
throw ExceptionObject;

  • ExceptionObject 必須是 Throwable 類或其子類的物件
  • 如果是自定義異常類,也必須是 Throwable 的直接或間接子類

②執行原理
throw 語句執行時,它後面的語句將不執行;
此時程式轉向呼叫者程式,尋找與之相匹配的 catch 語句,執行相應的異常處理程式。
如果沒有找到相匹配的 catch 語句,則再轉向上一層的呼叫程式。這樣逐層向上,直到最外層的異常處理程式終止程式並列印出呼叫棧情況

③實踐操作

import java.util.Scanner;
public class TestException {
    public boolean validateUserName(String username) {
        boolean con = false;
        if (username.length() >4) {
            // 判斷使用者名稱長度是否大於8位
            if ("admin".equals(username)) {
                con = true;
            }else{
                throw new IllegalArgumentException("你輸入的使用者名稱不對");
            }
        } else {
            throw new IllegalArgumentException("使用者名稱長度必須大於 4 位!");
        }
        return con;
    }
    public static void main(String[] args) {
        TestException te = new TestException();
        Scanner input = new Scanner(System.in);
        System.out.println("請輸入使用者名稱:");
        String username = input.next();
        try {
            boolean con = te.validateUserName(username);
            if (con) {
                System.out.println("使用者名稱輸入正確!");
            }
        } catch (IllegalArgumentException e) {
            System.out.println(e);
        }
    }
}
//輸出
①
請輸入使用者名稱:
abc
java.lang.IllegalArgumentException: 使用者名稱長度必須大於 4 位!
②
請輸入使用者名稱:
abcdef
java.lang.IllegalArgumentException: 你輸入的使用者名稱不對
③
請輸入使用者名稱:
admin
使用者名稱輸入正確!
6.2.4 自定義異常

當Java提供的內建異常型別不能滿足我們的需求時,我們可以設計自己的異常型別。

①語法格式
class XXXException extends Exception|RuntimeException

  • 一般將自定義異常類的類名命名為 XXXException,其中 XXX 用來代表該異常的作用
  • 自定義異常類需要繼承 Exception 類或其子類,如果自定義執行時異常類需繼承 RuntimeException 類或其子類
  • 自定義異常類一般包含兩個構造方法:一個是無參的預設構造方法,另一個構造方法以字串的形式接收一個定製的異常訊息,並將該訊息傳遞給超類的構造方法。

②實踐操作

import java.util.Scanner;
public class TestException {

    public static void main(String[] args) {
       int age;
       Scanner scanner = new Scanner(System.in);
        System.out.println("請輸入你的年齡");
        age=scanner.nextInt();
        try {
            if(age < 0) {
                throw new AgeException("您輸入的年齡為負數!輸入有誤!");
            } else if(age > 100) {
                throw new AgeException("您輸入的年齡大於100!輸入有誤!");
            } else {
                System.out.println("您的年齡為:"+age);
            }
        } catch (AgeException e) {
            e.printStackTrace();
        }
    }
}
//輸出
①
請輸入你的年齡
120
com.xiezhr.AgeException: 您輸入的年齡大於100!輸入有誤!
	at com.xiezhr.TestException.main(TestException.java:15)
②
請輸入你的年齡
-34
com.xiezhr.AgeException: 您輸入的年齡為負數!輸入有誤!
	at com.xiezhr.TestException.main(TestException.java:13)
③
請輸入你的年齡
30
您的年齡為:30

6.2.5 多異常捕獲

Java7以後,catch 語句可以有多個,用來匹配多個異常

①語法格式

try{
    // 可能會發生異常的語句
} catch (IOException | ParseException e) {
    // 異常處理
}
  • 多種異常型別之間用豎線|隔開
  • 異常變數有隱式的 final 修飾,因此程式不能對異常變數重新賦值

② 異常書寫方式變化

try{
    // 可能會發生異常的語句
} catch (FileNotFoundException e) {
    // 呼叫方法methodA處理
} catch (IOException e) {
    // 呼叫方法methodA處理
} catch (ParseException e) {
    // 呼叫方法methodA處理
}

變成

try{
    // 可能會發生異常的語句
} catch (FileNotFoundException | IOException | ParseException e) {
    // 呼叫方法處理
} 

③實踐操作

import java.util.Scanner;
public class TestException {

    public static void main(String[] args) {
       int num1;
       int num2;
        try {
            Scanner scanner = new Scanner(System.in);
            System.out.println("請輸入num1的值");
            num1=scanner.nextInt();
            System.out.println("請輸入num2的值");
            num2=scanner.nextInt();
            int num= num1/num2;
            System.out.println("你輸入的兩個數相除,結果是" + num);
        } catch (IndexOutOfBoundsException | NumberFormatException | ArithmeticException e){
            System.out.println("程式發生了陣列越界、數字格式異常、算術異常之一");
            e.printStackTrace();
        }
        catch (Exception e) {
            System.out.println("未知異常");
            e.printStackTrace();
        }
    }
}
//輸出
①
請輸入num1的值
12
請輸入num2的值
0
程式發生了陣列越界、數字格式異常、算術異常之一
java.lang.ArithmeticException: / by zero
	at com.xiezhr.TestException.main(TestException.java:15)
②
請輸入num1的值
6888888888
未知異常
java.util.InputMismatchException: For input string: "6888888888"
	at java.util.Scanner.nextInt(Scanner.java:2123)
	at java.util.Scanner.nextInt(Scanner.java:2076)
	at com.xiezhr.TestException.main(TestException.java:12)
③
請輸入num1的值
12
請輸入num2的值
4
你輸入的兩個數相除,結果是3

分析:
上面程式中IndexOutOfBoundsException|NumberFormatException|ArithmeticException來定義異常型別,這就表明該 catch 塊可以同時捕獲這 3 種型別的異常

七、Throwable 類常用方法

  • String getMessage(): 返回異常發生時的簡要描述
  • String toString(): 返回異常發生時的詳細資訊
  • String getLocalizedMessage(): 返回異常物件的本地化資訊。使用 Throwable 的子類覆蓋這個方法,可以生成本地化資訊。如果子類沒有覆蓋該方法,則該方法返回的資訊與 getMessage()返回的結果相同
  • void printStackTrace(): 在控制檯上列印 Throwable 物件封裝的異常資訊

八、易混概念

8.1 Error和Exception的異同

  • Error Exception 都有共同的祖先Throwable,即ErrorException 都是Throwable的子類
  • Exception :程式本身可以處理的異常,可以透過 try-catch 來進行捕獲。Exception又可以分為 Checked Exception (受檢查異常,必須處理) 和 Unchecked Exception(不受檢查異常,可以不處理)。
  • ErrorError 屬於程式無法處理的錯誤 ,我們沒辦法透過 try-catch 來進行捕獲 。例如 Java 虛擬機器執行錯誤(Virtual MachineError)、虛擬機器記憶體不夠錯誤(OutOfMemoryError)、類定義錯誤(NoClassDefFoundError)等 。這些異常發生時,Java 虛擬機器(JVM)一般會選擇執行緒終止

8.2 throw和throws的區別

  • throws 用來宣告一個方法可能丟擲的所有異常資訊,表示出現異常的一種可能性,但並不一定會發生這些異常;throw 則是指丟擲的一個具體的異常型別,執行 throw 則一定丟擲了某種異常物件。
  • 通常在一個方法(類)的宣告處透過 throws 宣告方法(類)可能丟擲的異常資訊,而在方法(類)內部透過 throw宣告一個具體的異常資訊。
  • throws 通常不用顯示地捕獲異常,可由系統自動將所有捕獲的異常資訊拋給上級方法; throw 則需要使用者自己捕獲相關的異常,而後再對其進行相關包裝,最後將包裝後的異常資訊丟擲

8.3 Checked Exception 和 Unchecked Exception

  • Checked Exception 受檢查異常 Java 程式碼在編譯過程中,如果受檢查異常沒有被 catch或者throws 關鍵字處理的話,就沒辦法透過編譯
  • 常見的受檢查異常有: IO 相關的異常、ClassNotFoundExceptionSQLException
  • 例如,下面就是
    -在這裡插入圖片描述
  • Unchecked Exception 即 不受檢查異常 ,Java 程式碼在編譯過程中 ,我們即使不處理不受檢查異常也可以正常透過編譯。
  • 我們經常看到的有以下幾種
    NullPointerException :空指標錯誤
    IllegalArgumentException :引數錯誤比如方法入參型別錯誤
    NumberFormatException:字串轉換為數字格式錯誤
    ArrayIndexOutOfBoundsException:陣列越界錯誤
    ClassCastException:型別轉換錯誤
    ArithmeticException:算術錯誤
    SecurityException :安全錯誤比如許可權不夠
    UnsupportedOperationException:不支援的操作錯誤比如重複建立同一使用者

8.4 try-with-resources 與 try-catch-finally

  • try-with-resources 是Java 1.7增加的新語法糖,在try 程式碼塊結束之前會自動關閉資源。
  • try-with-resources 適用於任何實現 java.lang.AutoCloseable或者 java.io.Closeable 的物件, 位元組輸入流(InputStream),位元組輸出流(OutputStream),字元輸入流(Reader),字元輸出流(Writer)均實現了這介面
  • try-catch-finally 沒有限制條件,finally不僅可以關閉資源,還可以用於執行其他程式碼塊;
  • try-with-resources 程式碼更加簡潔,有限制條件,資源會立即被關閉
  • finally關閉資源不會立即關閉,取決與網路和系統,可能會很快,也可能會等一兩天,所以,最好不要使用finally作為業務流程的控制,在《Effective java》一書 的第9條:try-with-resources優先於try-finally 中有相關詳細的介紹,其中提到了許多由於finally延遲導致的網路事件

九、SpringBoot 中優雅的處理統一異常返回

日常開發中,我們處理異常一般都會用到try-catchthrowthrows 的方式丟擲異常。
這種方式不經程式設計師處理麻煩,對使用者來說也不太友好
我們都希望不用寫過多的重複程式碼處理異常,又能提升使用者體驗。這時候全域性異常處理就顯得很便捷很重要了

9.1 全域性異常捕獲與處理

Springboot對提供了一個 @ControllerAdvice註解以及 @ExceptionHandler註解,分別用於開啟全域性的異常捕獲和說明捕獲哪些異常,對那些異常進行處理。

@ControllerAdvice
public class MyExceptionHandler {

    @ExceptionHandler(value =Exception.class)
	public String exceptionHandler(Exception e){
		System.out.println("出現了一個異常"+e);
       	return e.getMessage();
    }
}

分析 上面這段程式碼就是說,只要是程式碼執行過程中有異常就會進行捕獲,並輸出出這個異常。然後我們隨便編寫一個會發生異常的程式碼,測試出來的異常是這樣的。

在這裡插入圖片描述
這對於前後端分離來說這樣的報錯對使用者並不好,前後端分離之後唯一的互動就是json了,我們也希望將後端的異常變成json返回給前端處理。

9.2 用列舉型別記錄已知錯誤資訊與成功資訊

ErrorEnum列舉類中定義了常見的錯誤碼以及錯誤的提示資訊。
SuccEnum 列舉類中定義了成功碼及成功提示資訊

至於這裡為什麼用列舉就不具體說了,網上文章說說的也比較多了
具體可以參照:Java 列舉(enum) 詳解7種常見的用法

① 已知錯誤資訊

public enum ErrorEnum {
    // 資料操作錯誤定義
    NO_PERMISSION(403,"沒有許可權訪問"),
    NO_AUTH(401,"請先登入系統"),
    NOT_FOUND(404, "未找到該資源!"),
    USER_NOT_FIND(402, "未找到使用者資訊"),
    INTERNAL_SERVER_ERROR(500, "伺服器出問題了"),
    UNKNOW_ERR(-1,"未知錯誤")
    ;

    /** 錯誤碼 */
    private Integer errorCode;

    /** 錯誤資訊 */
    private String errorMsg;

    ErrorEnum(Integer errorCode, String errorMsg) {
        this.errorCode = errorCode;
        this.errorMsg = errorMsg;
    }

    public Integer getErrorCode() {
        return errorCode;
    }

    public String getErrorMsg() {
        return errorMsg;
    }
}

② 成功資訊

public enum SuccEnum  {
    SUCCESS(200, "success");

    /** 成功碼 **/
    private Integer succCode;

    /* 成功資訊*/
    private String succMsg;

    SuccEnum(Integer succCode, String succMsg) {
        this.succCode = succCode;
        this.succMsg = succMsg;
    }

    public Integer getSuccCode() {
        return succCode;
    }

    public String getSuccMsg() {
        return succMsg;
    }
}

9.3 定義統一結果返回與異常返回

  • success:用boolean 型別標識,標識是否成功
  • code: 狀態碼,區分各種報錯資訊與成功返回
  • msg : 成功或錯誤提示資訊
  • data : 返回的資料
@Data
public class Result<T> {
    //是否成功
    private Boolean success;
    //狀態碼
    private Integer code;
    //提示資訊
    private String msg;
    //資料
    private T data;
    public Result() {

    }
    //自定義返回結果的構造方法
    public  Result(Boolean success,Integer code, String msg,T data) {
        this.success = success;
        this.code = code;
        this.msg = msg;
        this.data = data;
    }


}

9.4 封裝工具類返回結果

這裡我們定義好了統一的結果返回,其中裡面的靜態方法是用來當程式異常的時候轉換成異常返回規定的格式。

public class ResultUtil {

    //成功,並返回具體資料
    public static Result success(SuccEnum succEnum,Object obj){
        Result result = new Result();
        result.setSuccess(true);
        result.setMsg(succEnum.getSuccMsg());
        result.setCode(succEnum.getSuccCode());
        result.setData(obj);
        return result;
    }

    //成功,無資料返回
    public static Result succes(SuccEnum succEnum){
        Result result = new Result();
        result.setSuccess(true);
        result.setMsg(succEnum.getSuccMsg());
        result.setCode(succEnum.getSuccCode());
        result.setData(null);
        return result;
    }

    //自定義異常返回的結果
    public static Result defineError(DefinitionException de){
        Result result = new Result();
        result.setSuccess(false);
        result.setCode(de.getErrorCode());
        result.setMsg(de.getErrorMsg());
        result.setData(null);
        return result;
    }
    //其他異常處理方法返回的結果
    public static Result otherError(ErrorEnum errorEnum){
        Result result = new Result();
        result.setSuccess(false);
        result.setMsg(errorEnum.getErrorMsg());
        result.setCode(errorEnum.getErrorCode());
        result.setData(null);
        return result;
    }
}

9.5 自定義異常

內建異常不能滿足我們業務需求的時候,我們就需要自定義異常

public class DefinitionException extends RuntimeException {
    protected Integer errorCode;
    protected String errorMsg;

    public DefinitionException(){

    }
    public DefinitionException(Integer errorCode, String errorMsg) {
        this.errorCode = errorCode;
        this.errorMsg = errorMsg;
    }

    public Integer getErrorCode() {
        return errorCode;
    }

    public void setErrorCode(Integer errorCode) {
        this.errorCode = errorCode;
    }

    public String getErrorMsg() {
        return errorMsg;
    }

    public void setErrorMsg(String errorMsg) {
        this.errorMsg = errorMsg;
    }
}

9.6 定義全域性異常處理類

我們自定義一個全域性異常處理類,來處理各種異常,包括自己定義的異常內部異常。這樣可以簡化不少程式碼,不用自己對每個異常都使用try,catch的方式來實現

@ControllerAdvice
public class GlobalExceptionHandler {
    /**
     * 處理自定義異常
     *
     */
    @ExceptionHandler(value = DefinitionException.class)
    @ResponseBody
    public Result bizExceptionHandler(DefinitionException e) {
        return ResultUtil.defineError(e);
    }

    /**
     * 處理其他異常
     *
     */
    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public Result exceptionHandler( Exception e) {
        return ResultUtil.otherError(ErrorEnum.UNKNOW_ERR);
    }
}

說明: 方法上面加上一個 @ResponseBody的註解,用於將物件解析成json,方便前後端的互動,也可以使用 @ResponseBody放在異常類上面

9.7 程式碼測試

9.7.1 定義User實體類
@Data
public class User {
    //唯一標識id
    private Integer id;
    //姓名
    private String name;
    //性別
    private String sex;
    //年齡
    private Integer age;
}

9.7.2 定義controller類
@RestController
@RequestMapping("/result")
public class ExceptionController {

    @Autowired
    private GlobalExceptionHandler globalExceptionHandler;

    @GetMapping("/getUser")
    public Result getStudent(){
        User user = new User();
        user.setId(100);
        user.setName("xiezhr");
        user.setAge(21);
        user.setSex("男");

        Result result = ResultUtil.success(SuccEnum.SUCCESS, user);
        return result;
    }

    @GetMapping("/getDefException")
    public Result DeException(){
        throw new DefinitionException(400,"我出錯了");
    }

    @GetMapping("/getException")
    public Result Exception(@RequestParam("name") String name, @RequestParam("pwd") String pwd){
        Result result = ResultUtil.success(SuccEnum.SUCCESS);
        try {
            if ("admin".equals(name)){
                User user = new User();
                user.setId(101);
                user.setName("xiezhr");
                user.setAge(18);
                user.setSex("男");
                result =  ResultUtil.success(SuccEnum.SUCCESS,user);
            }else if (name.equals("xiezhr")){
                result =  ResultUtil.otherError(ErrorEnum.USER_NOT_FIND);
            }else{
                int i = 1/0;
            }
        }catch (Exception e){
            result =  globalExceptionHandler.exceptionHandler(e);
        }

        return result;
    }
}

9.8 介面測試

9.8.1 獲取沒有異常的資料返回

http://localhost:8090/result/getUser
在這裡插入圖片描述

9.8.2 自定義異常返回

http://localhost:8090/result/getDefException
在這裡插入圖片描述
http://localhost:8090/result/getException?name=xiezhr&pwd=123
在這裡插入圖片描述

9.8.3 其他的異常 返回

http://localhost:8090/result/getException?name=ff&pwd=abc
在這裡插入圖片描述

十、異常處理及規約

異常的處理⽅式有兩種。 1、 ⾃⼰處理。 2、 向上拋, 交給調⽤者處理。

異常, 千萬不能捕獲了之後什麼也不做。 或者只是使⽤e.printStacktrace。

具體的處理⽅式的選擇其實原則⽐較簡明: ⾃⼰明確的知道如何處理的, 就要處理掉。 不知道如何處理的, 就交給調⽤者處理。

下面時阿里巴巴Java開發手冊關於異常處理規則

①【強制】 Java類庫中定義的可以透過預檢查方式規避的RuntimeException不應該透過catch的方式處理,如NullPointerExceptionIndexOutOfBoundsException

說明:無法透過預檢查的異常不在此列,比如當解析字串形式的數字時,可能存在數字格式錯誤,透過catch NumberFormatException實現

正例:

if(obj!=null){....}

反例:

try{
	obj.method();
}catch(NullPointerException e){
	...
}

②【強制】 異常捕獲後不要用來做流程控制和條件控制

說明:異常設計的初衷是解決程式執行中各種意外,且異常的處理效率比條件判斷方式要第很多。

③【強制】 catch 時請分清穩定程式碼和非穩定程式碼。穩定程式碼一般指本機執行且執行結果確定性高的程式碼。對於非穩定程式碼的catch 儘可能在進行異常型別的分割槽後,再做對應的異常處理

說明:對大段程式碼進行try-catch,將使程式無法根據不同的異常做出正確的“應激”反應,也不利於定位問題,這是一種不負責的表現

正例:在使用者註冊場景中,如果使用者輸入非法字串,或使用者名稱稱已存在,或使用者輸入的密碼過於簡單,那麼程式會作出分門別類的判斷並提示使用者。

④【強制】 捕獲異常使為了處理異常,不要捕獲了卻說明都不處理而拋棄之,如果不想處理它,請將異常拋給它的呼叫者。最外層的業務使用者必須處理異常,將其轉換為使用者可以理解的內容。

⑤【強制】 在事務場景中,丟擲異常被catch 後,如果需要回滾,那麼一定要注意手動回滾事務。

⑥【強制】 finally 塊必須對資源物件、流物件進行關閉操作,如果有一次要做try-catch操作。

說明:對於JDK7即以上版本,可以使用try-catch-resource 方式

⑦【強制】 不要在finally塊中使用return

說明try 塊中return 語句執行成功後,並不馬上返回,而是繼續執行finally 塊中的語句,如果此處存在return語句,則在此直接返回,無情地丟棄try塊中的返回點。

正例:

private int x =0;
public int checkReturn(){
	try{
		//x=1,此處不返回
		return ++x;
	}finally{
		//返回的結果是2
		return ++x;
	}
}

⑧【強制】 捕獲異常與丟擲異常必須完全匹配,或者捕獲異常時丟擲異常的父類。

說明:如果以及對方丟擲的時繡球,實際接收到的時鉛球,就會產生意外

⑨【強制】 在呼叫RPC、二方包或動態生成類的相關方法時,捕獲異常必須使用Throwable攔截。

說明:透過反射機制呼叫方法,如果找不到方法,則丟擲NoSuchMethodException
在說明情況下丟擲NoSuchMethodException呢?二方包在類衝突時,仲裁機制可能導致引入非預期的版本使類的方法簽名不匹配,或者在位元組碼修改框架(比如:ASM)動態建立或修改類時,修改了相應的方法簽名。對於這些情況,即使在程式碼編譯期是正確的,在程式碼執行期也會丟擲NoSuchMethodException

⑩【推薦】 方法的返回值可以為null,不強制返回空集合或者空物件等,必須新增註釋充分說明在說明情況下會返回null值。此時資料庫id不支援存入負數二丟擲異常。

說明:本手冊明確,防止產生NPE是呼叫者的責任。即使被呼叫方法返回空集合或者空物件,對呼叫者來說,也並非高枕無憂,必須考慮遠端呼叫失敗、序列化失敗、執行時異常等場景返回null值的情況

⑪【推薦】 防止產生NPE時程式設計師的基本修養,注意NPE產生的場景。
1)當返回型別為基本資料型別,return 包裝資料型別的物件時,自動拆箱有可能產生NPE
反例:

public int f(){
	//如果為null,則自動拆箱,拋NPE。
	return Integer 物件;
}

2)資料庫的查詢結果可能為null

  1. 集合裡的元素即使isNotEmpty , 取出的資料元素也有可能為null
  2. 當遠端呼叫返回物件時,一律要求進行空指標判斷,以防止產生NPE。
    5)對於Session 中獲取的資料,建議進行NPE檢查,以避免空指標
    6)級聯呼叫obj.getA().getB().getC();的一連串呼叫容易產生NPE。

⑫【推薦】 定義時區分 unchecked/checked 異常,避免直接丟擲new RuntimeException(),更不允許丟擲Exception 或者Throwable,應該使用業務含義的自定義異常。推薦業界已定義過的自定義異常,如:DAOException / ServiceException

⑬ 【參考】 對於公司外的HTTP/API 開放介面,必須使用“errorCode”:應用內部推薦異常丟擲;
跨應用間RPC呼叫優先考慮使用Result 方式,封裝isSuccess() 方法、errorCodeerrorMessage

說明:關於RPC方法返回方式使用Result方式的理由
1)使用丟擲異常返回方式,呼叫方式如果沒有捕獲到,就會產生執行時錯誤
2)如果不加棧資訊,知識new自定義異常,加入自己理解的errorMesage,對於呼叫解決問題的幫助不會太多。如果加了棧資訊,在頻繁呼叫出錯的情況下,資料序列化和傳輸的效能損耗也是問題。

⑭【參考】 避免出現重複的程式碼(Don't Repeat Yourself),即DRY原則

說明: 隨意複製和貼上程式碼,必然導致程式碼的重複,當以後需要修改時,需要修改所有的副本,容易遺漏。必要時抽取共性方法或公共類,甚至將程式碼元件化。

正例: 一個類中由多個public 方法,都需要進行數行相同的引數校驗操作,這個時候請抽取:

private boolean checkParam(DTO dto){...}

本期內容到這就結束了,各位小夥伴們,我們下期見 (●'◡'●)

相關文章