啪,還敢拋出異常

来源:https://www.cnblogs.com/xiezhr/p/18093599
-Advertisement-
Play Games

前言 去年又重新刷了路遙的《平凡的世界》,最近也在朋友推薦下,來看了路遙的另一部成名作《人生》。 故事中的主人公高加林,雖生在農村,面朝黃土背朝天,卻不甘心像父輩一樣或者,一心想著擺脫民語的束縛,追求他的理想生活。 然而命運多舛,在他所想象的理想生活中,一次次跌倒,最終不得不承認自己的平凡,生活總得 ...


前言

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

一、異常引入

因此呢就出現了異常處理,我們把可能出現異常的業務代碼放到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){...}

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

本文來自博客園,作者:xiezhr,轉載請註明原文鏈接:https://www.cnblogs.com/xiezhr/p/18093599


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...