Java基礎學習之異常處理

1. Java異常體系

Java異常體系設計的目的在於通過使用少量代碼,實現大型、健壯、可靠程序。

1.1. 異常處理

異常處理是Java中唯一正式的錯誤報告機制。異常處理機制最大的好處就是降低錯誤代碼處理的複雜程度。

如果不使用異常,那麼就必須在調用點檢查特定的錯誤,並在程序的很多地方去處理它;如果使用異常,那麼就不必在方法調用處進行檢查,因爲異常機制將保證能夠捕獲這個錯誤。因此只需要在一個地方處理錯誤,這種方式不僅節省代碼,而且把“描述正確執行過程做什麼事”和“出了問題怎麼辦”相分離。

異常處理的一個重要原則就是“只有在你知道如何處理的情況下才捕獲異常”,實際上異常處理的一個重要目標就是把錯誤處理的代碼與錯誤發生地點相分離。這使你能在一段代碼中專注於要做的事,至於異常處理,則放在另一端代碼中。這樣,主幹代碼就不會與錯誤處理邏輯混在一起,更容易理解和維護。通過允許一個異常處理程序處理多個異常點,使得異常處理代碼集中於一處。

“異常情形”,指阻止當前方法或作用域繼續執行的問題。異常最重要的方面之一就是如果發生問題,他將不允許程序沿着正常路徑繼續執行,而是將控制權轉交給異常處理程序,強制處理出現的問題,並恢復穩定狀態。

與程序正常處理不同,當拋出異常時,有幾件事會隨之發生:

  • 首先,使用new關鍵字在堆上創建一個異常對象;
  • 然後,當前執行路徑被終止,從當前環境中throw出異常對象引用;
  • 此時,異常處理機制接管程序,並尋找一個合適的異常處理程序來繼續執行程序;
  • 如果,找到合適的異常處理程序,則使用該處理程序對異常進行恢復;如果未找到合適的異常處理程序,則將異常向調用鏈上級拋出;
  • 如果,到最頂層仍舊未找到合適的異常處理程序,則當前線程異常退出。

Java異常處理:

1
2
3
4
5
6
7
8
9
10
11
try {
    // 正常處理流程,正確執行過程做什麼事
    Path path = Paths.get("var", "error");
    List<String> lines = Files.readAllLines(path, Charset.defaultCharset());
    System.out.println(lines);
} catch (IOException e) {
    // 異常處理流程,出了問題怎麼辦
    e.printStackTrace();
}finally {
    // 清理資源
}

 

1.1.1. Exception

異常與其他對象一樣,使用new關鍵字在堆上創建異常對象,也伴隨着存儲空間的分配和構造函數的調用。

標準異常會有幾個構造函數:

  • 無參默認構造函數
  • 接受一個字符串的構造函數
  • 接收一個字符串和 Throwable 的構造函數
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
    public class Exception extends Throwable {
        static final long serialVersionUID = -3387516993124229948L;
        public Exception() {
            super();
        }
        public Exception(String message) {
            super(message);
        }
        public Exception(String message, Throwable cause) {
            super(message, cause);
        }
        public Exception(Throwable cause) {
            super(cause);
        }
    }
    

1.1.2. throw

將異常對象的引用傳遞給throw,從效果上看,它就像從方法中“返回”一樣,可以將異常處理當做一種不同的返回機制。不同的是return返回到方法調用點,throw返回到異常處理程序。

1
2
3
4
private String throwException(){
    //return "Test";
    throw new RuntimeException("Test Exception");
}

1.1.3. try

“監控區域”是一段可能產生異常的代碼,並且後面跟着處理這些異常的代碼。可以簡單的理解爲try塊就是監控區域。

如果在方法內部拋出異常(或調用其他方法出現異常),這個方法將在拋出異常的點結束,如果不希望方法結束,那麼需要在方法內設置一個特殊的塊來捕獲異常。

1
2
3
4
5
6
7
8
9
private String tryException(){
    try {
        // 監控區域
        return throwException();
    }catch (Exception e){
        // 異常處理區域
    }
    return "";
}

1.1.4. catch

拋出的異常必須在某處得到處理,這個點就是異常處理程序。針對每個要捕獲的異常,準備相應的處理程序。異常處理程序緊跟着try塊,以關鍵字catch表示。

每個catch子句,看起來就像是接收一個且只接收一個特殊異常類型的方法。當異常發生後,異常處理機制會搜尋參數與異常類型匹配的第一個異常處理器,然後進入catch子句執行,此時認爲異常得到了處理。一旦catch子句結束,則處理程序的查找過程結束。

在查找異常處理器時,並不要求拋出的異常與異常處理器所聲明的異常完全匹配。派生類的對象也可以匹配基類的處理器。

1
2
3
4
5
6
7
8
9
10
11
12
private String tryException(){
    try {
        // 監控區域
        return throwException();
    }catch (RuntimeException e){
        // 處理 RuntimeException 情況
    }
    catch (Exception e){
        // 處理 Exception 情況
    }
    return "";
}

順序,異常處理機制會搜索第一個匹配的異常處理器,因此catch語句的順序至關重要,通常將具體類型前置,通用類型後置。

1.1.5. finally

對於一些代碼,可能會希望無論try塊中是否拋出異常,他們都會執行。爲了達到效果,可以在異常處理後面加上finally子句。

對於沒有垃圾回收和析構函數自動調用機制的語言來說,finally非常重要。它是程序員能夠保證在任何情況下,內存總能得到釋放。但在Java中存在垃圾回收機制,內存釋放不再是個問題。當要把除內存外的資源恢復到他們的初始化狀態時,就需要使用finally子句。常見的資源包括:網絡鏈接、文件句柄、顯示鎖等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private String tryException(){
    try {
        // 監控區域
        return throwException();
    }catch (RuntimeException e){
        // 處理 RuntimeException 情況
    }
    catch (Exception e){
        // 處理 Exception 情況
    }finally {
        // 對 網絡鏈接、文件句柄、鎖等資源進行處理
    }
    return "";
}

1.2 方法異常說明

Java鼓勵將方法可能會拋出的異常告知使用該方法的客戶端。這種做法,使得調用者能知道在代碼中可以獲取所有的異常。

異常說明在方法聲明中使用附加的關鍵字throws,後面接一個所有潛在異常類型列表,所以方法簽名變成:

1
2
3
4
5
// 方法異常說明
private List<String> readFromFile(String filePath) throws IOException {
    Path path = Paths.get(filePath);
    return Files.readAllLines(path, Charset.defaultCharset());
}

代碼必須和異常說明保存一致。如果方法裏面的代碼產生了異常卻沒有被處理,編譯器會報錯,要麼處理這個異常,要麼在異常說明列表中添加這個異常類型。

當然,可以在方法簽名中聲明異常,實際上並不拋出。這樣可以爲異常佔個位置,以後可以拋出該異常而不用修改調用代碼。

被檢查異常,這種在編譯階段被強制檢查的異常成爲被檢查異常。

備註: 被檢查異常,可以通過反射機制獲取異常列表。

1.3 異常的限制

當覆蓋方法時,只能拋出在基類方法的異常列表中列出的異常,這意味着當基類使用的代碼應用到其派生類對象的時候,程序一樣能正常工作。

1.3.1. 方法重寫

儘管在繼承過程中,編譯器會對異常說明做強制要求,但異常說明並不是方法類型的一部分,方法類型由方法名和參數類型組成。因此不能基於異常說明來重載方法。

對於方法重寫時子類方法中的異常列表,要求要寬鬆得多。

  • 子類方法異常列表與父類完全一致
  • 子類方法異常列表是父類方法異常列表的子集
  • 子類方法沒有拋出異常
  • 子類方法拋出父類方法異常的子異常

具體代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 父類接口
public interface FileReader {
    List<String> readFromFile(String filePath) throws IOException;
}

class FileReader1 implements FileReader{

    // 子類方法異常列表與父類完全一致
    @Override
    public List<String> readFromFile(String filePath) throws IOException {
        return null;
    }
}

class FileReader2 implements FileReader{
    // 子類方法拋出父類方法異常的子異常
    @Override
    public List<String> readFromFile(String filePath) throws FileNotFoundException {
        return null;
    }
}
class FileReader3 implements FileReader{

    // 子類方法沒有拋出異常
    @Override
    public List<String> readFromFile(String filePath){
        return null;
    }
}

 

1.3.2. 方法重載

Java 的方法重載,只涉及方法名和參數列表。方法返回值和異常列表都作爲方法重載的依據。

1
2
3
4
5
6
7
8
9
10
public List<String> readFromFile(String path) throws IOException{
    return null;
}

/**
  編譯不過
public List<String> readFromFile(String path) throws FileNotFoundException{
    return null;
}
 */

1.3.3. 構造函數

異常限制對構造函數不起作用,子類構造函數能夠拋出任意異常,而不必理會基類構造函數所拋出的異常。但,因爲基類構造函數必須以某種形式被調用,派生類構造函數的異常說明必定包含基類構造函數的異常說明。

構造器會把對象設置爲安全的初始化狀態,如果有別的工作,比如打開一個文件,這樣的動作只有在對象使用完畢並且用戶調用了清理方法才能清理。如果在構造函數中拋出異常,這些清理動作就不能正常工作,因此在編寫構造器時要格外注意。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Parent{
    Parent() throws IOException{

    }
}
class Child extends Parent{

    Child() throws IOException {
        super(); //此次拋出異常
    }
    /**
    Child() throws IOException {
        // super 必須是第一個語句,無法對異常進行捕獲
        try {
            super(); //此次拋出異常
        }catch (Exception e){

        }

    }
     */
}

1.4 受檢查異常

在編譯時被強制檢查的異常稱爲”受檢查的異常”。即在方法的聲明中聲明的異常。

受檢查異常要求方法調用者必須對異常進行處理。從某種角度來說,受檢查異常違反了 Java 異常處理的初衷。

1
2
3
4
private List<String> readFromFile(String filePath) throws IOException {
    Path path = Paths.get(filePath);
    return Files.readAllLines(path, Charset.defaultCharset());
}

在調用 readFromFile 方法時,無法忽略對 IOException 的處理。

一般情況下,面對受檢查異常,我們通常這樣處理:

  1. 修改自己的方法簽名,添加新的異常聲明;
  2. 使用 try catch 包裹異常調用(大多時候,我們不知道如何進行恢復);
  3. 將受檢查異常轉化爲運行時異常;
1
2
3
4
5
6
7
8
9
private void printFile2(String filePath){
    try {
        List<String> lines = readFromFile(filePath);
        lines.forEach(System.out::println);
    }catch (IOException e){
        // 使用異常鏈,將受檢查異常轉化爲運行時異常
        throw new RuntimeException(e);
    }
}

1.4.1 Spring DAO Support

JDBC 接口中存在大量的受檢查異常,在操作數據庫時,會出現大量的try catch 樣板代碼,使核心邏輯埋葬在代碼海中。

爲此,Spring 對其進行優化,具體優化措施主要有:

  1. 在運行時異常基礎上,建立了一整套異常體系(DataAccessException以及子類);
  2. 將 jdbc 中的受檢查異常轉化爲運行時異常;
  3. 使用模板方法降低冗餘代碼。

jdbcTempalte 代碼片段如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public <T> T execute(StatementCallback<T> action) throws DataAccessException {
	Assert.notNull(action, "Callback object must not be null");

	Connection con = DataSourceUtils.getConnection(obtainDataSource());
	Statement stmt = null;
	try {
		stmt = con.createStatement();
		applyStatementSettings(stmt);
		T result = action.doInStatement(stmt);
		handleWarnings(stmt);
		return result;
	}
	catch (SQLException ex) {
		// Release Connection early, to avoid potential connection pool deadlock
		// in the case when the exception translator hasn't been initialized yet.
		String sql = getSql(action);
		JdbcUtils.closeStatement(stmt);
		stmt = null;
		DataSourceUtils.releaseConnection(con, getDataSource());
		con = null;
		// 完成受檢查異常到運行時異常的轉化
		throw translateException("StatementCallback", sql, ex);
	}
	finally {
		JdbcUtils.closeStatement(stmt);
		DataSourceUtils.releaseConnection(con, getDataSource());
	}
}

 

2. 自定義異常

不必侷限於Java提供的異常類型。我們可以自定義異常類來表示程序中可能會遇到的特定問題。

要自定義異常類,必須從已有異常類繼承,最好的方式是選擇意思相近的異常類繼承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 業務異常
class BizException extends RuntimeException{
    public BizException() {
        super();
    }

    public BizException(String message) {
        super(message);
    }

    public BizException(String message, Throwable cause) {
        super(message, cause);
    }

    public BizException(Throwable cause) {
        super(cause);
    }
}

異常一般是用名稱代表發生的問題,並且異常的名稱應該可以望文知意。

2.1. 異常繼承體系

異常本身也是類,存在一個完整的繼承體系。

2.2. Throwable

Throwable被用來表示任何可以作爲異常被拋出的類。

Throwable對象可以分爲倆種類型(從Throwable繼承而來):

  • Error 用於表示編譯時和系統錯誤,出特殊情況外,開發人員不必關係
  • Exception 表示可以被拋出的基礎類型。在Java類庫、用戶方法以及運行時故障都可能拋出Exception異常。這是開發人員最關係的異常。

throwable主要是對異常棧進行維護,核心方法如下:
方法 | 含義
—|—
printStackTrace | 打印調用棧信息,輸出到標準錯誤輸出(System.error)
printStackTrace(PrintStream) | 指定Stream打印調用棧信息
printStackTrace(PrintWriter) | 指定Print打印調用棧信息
getStackTrace() | 獲取調用棧序列信息
fillInStackTrace() | 記錄棧幀的當前狀態

異常棧記錄了”把你帶到異常拋出點”的方法調用序列,是問題排查的主要信息之一。

1
2
3
4
5
6
7
8
9
10
11
public static void main(String... arg){
    try {
        // 正常處理流程,正確執行過程做什麼事
        Path path = Paths.get("var", "error");
        List<String> lines = Files.readAllLines(path, Charset.defaultCharset());
        System.out.println(lines);
    } catch (IOException e) {
        // 異常處理流程,出了問題怎麼辦
        e.printStackTrace();
    }
}

運行程序,獲得結果,異常棧如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java.nio.file.NoSuchFileException: var/error
	at sun.nio.fs.UnixException.translateToIOException(UnixException.java:86)
	at sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:102)
	at sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:107)
	at sun.nio.fs.UnixFileSystemProvider.newByteChannel(UnixFileSystemProvider.java:214)
	at java.nio.file.Files.newByteChannel(Files.java:361)
	at java.nio.file.Files.newByteChannel(Files.java:407)
	at java.nio.file.spi.FileSystemProvider.newInputStream(FileSystemProvider.java:384)
	at java.nio.file.Files.newInputStream(Files.java:152)
	at java.nio.file.Files.newBufferedReader(Files.java:2781)
	at java.nio.file.Files.readAllLines(Files.java:3199)
	at com.geekhalo.exception.Demo.main(Demo.java:15)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:483)
	at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)

 

2.3. Exception

Exception是與編程有關的所有異常類的基類。

方法 含義
getMessage 獲取詳細信息
getLocaliedMessage 獲取本地語言表示的詳細信息

2.4. RuntimeException

從RuntimeException派生出來的異常成爲“不受檢查異常”。這種異常會自動被Java虛擬機拋出,所以不必在方法的異常說明中列出來。

1
2
3
private void throwRuntimeException(){
    throw new RuntimeException();
}

RuntimeException 及其子類 無需在方法中進行聲明。

3. 常見異常處理策略

完成自定義異常後,下一個關鍵點便是如何處理異常。

3.1. 異常恢復

異常處理程序的目的就是處理所發生的異常。因此,第一個異常處理策略便是,處理異常,進行異常恢復。

1
2
3
4
5
6
7
8
9
10
11
12
13
private void recoveryException(String filePath){
    try {
        List<String> lines = readFromFile(filePath);
        lines.forEach(System.out::println);
    }catch (IOException e){
        // 打印日誌,從異常中恢復程序
        LOGGER.error("failed to read from file {}", filePath, e);
    }
}
private List<String> readFromFile(String filePath) throws IOException {
    Path path = Paths.get(filePath);
    return Files.readAllLines(path, Charset.defaultCharset());
}

3.2. 重新拋出異常

當你無法得到足夠信息,從而對異常進行恢復時。可以把剛剛捕獲的異常重新拋出。在catch子句中已經獲得了對當前異常對象的引用,可以直接將其拋出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void printFile(String filePath) throws IOException{
    try {
        List<String> lines = readFromFile(filePath);
        lines.forEach(System.out::println);
    }catch (IOException e){
        // 重新拋出異常
        throw e;
    }
}

// 方法異常說明
private List<String> readFromFile(String filePath) throws IOException {
    Path path = Paths.get(filePath);
    return Files.readAllLines(path, Charset.defaultCharset());
}

重拋異常會把異常拋給上一級調用,同一try後的catch子句被忽略。如果只是把當前異常拋出,那麼printStackTrace顯示的是原來異常拋出點的調用鏈信息,而非重新拋出點的信息。如果想要更新調用信息,可以調用fillInStackTrace方法,返回另一個Throwable對象,它將當前調用棧信息填入原來的異常對象。

3.3. 異常鏈

如果想要在捕獲一個異常後拋出另一個新異常,並希望把原始異常信息保留下來,這成爲異常連。

Throwable的子類在構造器中都可以接受一個cause對象,用於表示原始異常,這樣把原始異常傳遞給新異常,使得當前位置創建並拋出的新異常,通過異常鏈追蹤到異常最初發生的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void printFile2(String filePath){
    try {
        List<String> lines = readFromFile(filePath);
        lines.forEach(System.out::println);
    }catch (IOException e){
        // 異常鏈
        throw new BizException(e);
    }
}


// 方法異常說明
private List<String> readFromFile(String filePath) throws IOException {
    Path path = Paths.get(filePath);
    return Files.readAllLines(path, Charset.defaultCharset());
}

Throwable子類中,只有Error、Exception、RuntimeException在構造函數中提供了cause參數,如果要把其他異常鏈起來,可以使用initCause方法。

4. 異常實戰

異常是框架設計不可遺漏的點。

框架中的異常處理,同樣遵循固定的操作流程:

  1. 根據需求自定義異常;
  2. 提供異常處理器,統一對異常進行處理;

4.1. Spring MVC 統一異常處理

Spring MVC 是最常見的 Web 框架,上手簡單,開發迅速。

遵循正常流程與異常處理分離策略。研發人員只需關心正常邏輯,由框架對異常流程進行統一處理。那應該怎麼操作呢?

4.1.1. 定義業務異常

首先,需要定義自己的業務異常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public abstract class BusinessException extends RuntimeException{
    /**
     * 異常處理碼
     */
    private final int code;

    /**
     * 異常消息
     */
    private final String msg;

    private final String timestamp = String.valueOf(System.currentTimeMillis());

    protected BusinessException(int code, String msg){
        this.code = code;
        this.msg = msg;
    }

    protected BusinessException(int code, String msg, Exception e) {
        super(e);
        this.code = code;
        this.msg = msg;
    }
}

4.1.2. 異常處理

可以使用 HandlerExceptionResolver 擴展,對異常進行定製。

RestHandlerExceptionResolver 對 Rest 請求的服務異常進行處理。將異常統一轉化爲 JSON 返回給用戶。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Component
public class RestHandlerExceptionResolver implements HandlerExceptionResolver {
    private static final Logger LOGGER = LoggerFactory.getLogger(RestHandlerExceptionResolver.class);

    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        // 是 Rest 請求 並且能夠處理
        if(isRestRequest(handler) && isAcceptException(ex)){
            // 將異常傳化爲 RestResVo 對象
            RestResVo<Void> restResVo = RestResVo.error((BusinessException)ex);
            try {
                // 以 Json 格式進行寫回
                response.getWriter().println(JSON.toJSONString(restResVo));
            }catch (Exception e){
                LOGGER.error("failed to write json {}", restResVo, e);
            }
            // empty ModelAndView說明已經處理
            return new ModelAndView();
        }
        return null;
    }

    private boolean isRestRequest(Object handler) {
        if (handler instanceof HandlerMethod){
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            return AnnotationUtils.findAnnotation(handlerMethod.getMethod(), ResponseBody.class) !=null ||
                    AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), ResponseBody.class) != null;
        }
        return false;
    }

    private boolean isAcceptException(Exception ex) {
        return ex instanceof BusinessException;
    }
}

PageHandlerExceptionResolver 對頁面請求的異常進行處理。將異常統一轉發到 error 視圖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Component
public class PageHandlerExceptionResolver implements HandlerExceptionResolver {
    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        // 是頁面請求並且能夠處理當前異常
        if(isPageRequest(handler) && isAcceptException(ex)){
            // 返回 error 視圖
            ModelAndView mv =  new ModelAndView("error");
            mv.addObject("error", ex);
            return mv;
        }
        return null;
    }


    private boolean isPageRequest(Object handler) {
        if (handler instanceof HandlerMethod){
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            return AnnotationUtils.findAnnotation(handlerMethod.getMethod(), ResponseBody.class) == null
                    && AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), ResponseBody.class) == null;
        }
        return true;
    }

    private boolean isAcceptException(Exception ex) {
        return ex instanceof BusinessException;
    }
}

 

4.2. Spring Cloud 異常穿透

在使用 Spring Cloud 進行微服務時,如果 Server 端發生異常,客戶端會收到一個 5xx 錯誤,從而中斷當前正常請求邏輯。但,異常中所含有的業務信息也一併丟失了,如何最大限度的保持異常信息呢?

4.2.1. 定義業務異常

首先,仍舊是定義自己的業務異常類。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Data
public class CodeBasedException extends RuntimeException {
    private Integer code;
    private String msg;
    private Object data;

    public CodeBasedException(){
        super();
    }
    public CodeBasedException(String msg) {
        super(msg);
        this.msg = msg;
    }

    public CodeBasedException(Integer code, String msg, Object data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    public CodeBasedException(String message, Integer code, String msg, Object data) {
        super(message);
        this.code = code;
        this.msg = msg;
        this.data = data;
    }


}

4.2.2. Server 端處理

在 Server 端,捕獲業務異常,並將信息通過 Header 進行寫回。

HandlerInterceptorBasedExceptionBinder 在業務處理完成後,捕獲 CodeBasedException 異常,並將異常信息通過 Response 對象回寫到 Header 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class HandlerInterceptorBasedExceptionBinder implements HandlerInterceptor {
    private static final Logger LOGGER = LoggerFactory.getLogger(HandlerInterceptorBasedExceptionBinder.class);

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        if (ex == null){
            return;
        }
        if (ex instanceof CodeBasedException){
            CodeBasedException codeBasedException = (CodeBasedException) ex;
            response.addHeader(SoaConstants.HEADER_ERROR_CODE, String.valueOf(codeBasedException.getCode()));
            response.addHeader(SoaConstants.HEADER_ERROR_MSG, encode(codeBasedException.getMsg()));

            response.addHeader(SoaConstants.HEADER_ERROR_EXCEPTION_MSG, encode(codeBasedException.getMessage()));
            return;

        }

        response.setHeader(SoaConstants.HEADER_ERROR_CODE, "500");
        response.setHeader(SoaConstants.HEADER_ERROR_MSG, encode(ex.getMessage()));

        response.setHeader(SoaConstants.HEADER_ERROR_EXCEPTION_MSG, encode(String.valueOf(ex.getStackTrace())));
        LOGGER.error("failed to handle request.", ex);
    }
}

如果是 Spring Boot 項目,我們需要完成 HandlerInterceptorBasedExceptionBinder 的註冊。

1
2
3
4
5
6
7
@Configuration
public class SoaWebMvcConfigurer implements WebMvcConfigurer{
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new HandlerInterceptorBasedExceptionBinder()).addPathPatterns("/**");
    }
}

4.2.3. Client 端處理

客戶端在獲取請求結果後,從 Header 中提取異常信息,並重新組裝並拋出異常。

FeignErrorDecoderBasedExceptionConverter 從 Header 中提取異常信息,並重新組裝並拋出 SoaRemoteCallException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class FeignErrorDecoderBasedExceptionConverter implements ErrorDecoder {
    private static final Logger LOGGER = LoggerFactory.getLogger(FeignErrorDecoderBasedExceptionConverter.class);

    public FeignErrorDecoderBasedExceptionConverter() {
    }

    @Override
    public Exception decode(String methodKey, Response response) {
        Map<String, Collection<String>> headers = response.headers();
        report(methodKey, response);
        return checkException(headers);
    }

    private void report(String methodKey, Response response) {
        String message = format("status %s reading %s", response.status(), methodKey);
        try {
            if (response.body() != null) {
                String body = Util.toString(response.body().asReader());
                message += "; content:\n" + body;
            }
        } catch (IOException ignored) { // NOPMD
        }
        LOGGER.error("status {}, message {}", response.status(), message);
    }

    private Exception checkException(Map<String, Collection<String>> headers) {
        String code = getValue(headers, SoaConstants.HEADER_ERROR_CODE);

        String msg = HeaderValueUtils.decode(getValue(headers, SoaConstants.HEADER_ERROR_MSG));
        String exceptionMsg = HeaderValueUtils.decode(getValue(headers, SoaConstants.HEADER_ERROR_EXCEPTION_MSG));


        Integer errorCode = NumberUtils.isNumber(code) ? Integer.valueOf(code) : -1;
        return new SoaRemoteCallException(exceptionMsg, errorCode, msg, "");
    }

    private String getValue(Map<String, Collection<String>> headers, String key) {
        Collection<String> values = headers.get(key);
        if (values != null && values.size() == 1){
            return values.iterator().next();
        }
        LOGGER.debug("failed to find value of {} in header {}", key, headers);
        return null;
    }
}

最後,需要完成 FeignErrorDecoderBasedExceptionConverter 的註冊。

1
2
3
4
@Bean
public FeignErrorDecoderBasedExceptionConverter exceptionCheckFeignDecoder(){
    return new FeignErrorDecoderBasedExceptionConverter();
}

 

5. 小節

  1. Java 異常的本質就是,將正常處理邏輯和異常處理邏輯進行分離;
  2. 異常使用方面,通過需要兩步操作:
    • 自定義異常
    • 自定義異常處理器
  3. 不管是日常開發,還是框架擴展,Java 異常機制都能出色的完成分離任務。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章