精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

項目中處理異常的四種常見錯誤!你知道幾個?

開發(fā) 項目管理
掌握了這些異常處理的最佳實踐,相信你能寫出更加健壯和優(yōu)雅的代碼。記住,異常處理不是可有可無的"裝飾",而是保證系統(tǒng)穩(wěn)定性和可維護(hù)性的重要基石。?

應(yīng)用程序難免會出現(xiàn)異常,而捕獲和處理異常是考驗編程功力的精細(xì)活。在一些業(yè)務(wù)項目中,我曾見過這樣的場景:開發(fā)同學(xué)在編寫業(yè)務(wù)邏輯時完全不考慮異常處理,等項目接近完成時再采用"流水線"的方式進(jìn)行異常處理——統(tǒng)一為所有方法添加 try...catch... 來捕獲所有異常并記錄日志,技巧熟練的同學(xué)甚至?xí)褂?AOP 來實現(xiàn)類似的"統(tǒng)一異常處理"。

然而,這種處理異常的方式是極其不可取的。今天,我就和你深入探討這種做法不可取的原因,以及異常處理中的常見陷阱和最佳實踐。

異常處理的常見誤區(qū)

誤區(qū)一:框架層面的粗暴"統(tǒng)一處理"

"統(tǒng)一異常處理"正是我要說的第一個誤區(qū):不在業(yè)務(wù)代碼層面考慮異常處理,僅在框架層面粗暴地捕獲和處理異常。

要理解這種做法錯在何處,我們先來回顧一下大多數(shù)業(yè)務(wù)應(yīng)用采用的三層架構(gòu):

  • Controller 層:負(fù)責(zé)信息收集、參數(shù)校驗、數(shù)據(jù)轉(zhuǎn)換適配前端,承載輕量級業(yè)務(wù)邏輯
  • Service 層:負(fù)責(zé)核心業(yè)務(wù)邏輯,包括各種外部服務(wù)調(diào)用、數(shù)據(jù)庫訪問、緩存處理、消息處理等
  • Repository 層:負(fù)責(zé)數(shù)據(jù)訪問實現(xiàn),通常不包含業(yè)務(wù)邏輯

圖片圖片

由于每層架構(gòu)的工作性質(zhì)不同,加上從業(yè)務(wù)性質(zhì)角度異常可分為業(yè)務(wù)異常和系統(tǒng)異常兩大類,這就決定了很難進(jìn)行統(tǒng)一的異常處理。讓我們從底向上分析三層架構(gòu):

Repository 層的異常處理策略

  • 異常可能需要忽略、降級處理,或者轉(zhuǎn)化為更友好的異常
  • 如果一律捕獲異常僅記錄日志,很可能業(yè)務(wù)邏輯已經(jīng)出錯,而用戶和程序本身完全感知不到

Service 層的異常處理策略

  • 往往涉及數(shù)據(jù)庫事務(wù),出現(xiàn)異常不應(yīng)該被捕獲,否則事務(wù)無法自動回滾
  • 涉及復(fù)雜業(yè)務(wù)邏輯,某些業(yè)務(wù)異常發(fā)生后可能需要轉(zhuǎn)入分支業(yè)務(wù)流程
  • 如果業(yè)務(wù)異常都被框架捕獲,業(yè)務(wù)功能就會異常

Controller 層的異常處理策略

  • 如果下層異常上升到這里仍無法處理,通常需要給用戶友好提示
  • 或者根據(jù)每個 API 的異常表返回指定的異常類型
  • 同樣無法對所有異常一視同仁

因此,我強(qiáng)烈不建議在框架層面進(jìn)行異常的自動、統(tǒng)一處理,尤其不要隨意捕獲異常。不過,框架可以承擔(dān)兜底工作。如果異常上升到最上層邏輯仍無法處理,可以通過統(tǒng)一的方式進(jìn)行異常轉(zhuǎn)換,比如使用 @RestControllerAdvice + @ExceptionHandler 來捕獲這些"未處理"異常:

  • 自定義業(yè)務(wù)異常:以 Warn 級別記錄異常及當(dāng)前 URL、執(zhí)行方法等信息,提取異常中的錯誤碼和消息,轉(zhuǎn)換為合適的 API 響應(yīng)體返回給調(diào)用方
  • 無法處理的系統(tǒng)異常:以 Error 級別記錄異常和上下文信息(如 URL、參數(shù)、用戶 ID),轉(zhuǎn)換為通用的"服務(wù)器忙,請稍后再試"異常信息,同樣以 API 響應(yīng)體返回給調(diào)用方

示例代碼如下:

@RestControllerAdvice
@Slf4j
publicclass RestControllerExceptionHandler {

    privatestaticint GENERIC_SERVER_ERROR_CODE = 2000;
    privatestatic String GENERIC_SERVER_ERROR_MESSAGE = "服務(wù)器忙,請稍后再試";

    @ExceptionHandler
    public APIResponse handle(HttpServletRequest req, HandlerMethod method, Exception ex) {
        if (ex instanceof BusinessException) {
            BusinessException exception = (BusinessException) ex;
            log.warn(String.format("訪問 %s -> %s 出現(xiàn)業(yè)務(wù)異常!", req.getRequestURI(), method.toString()), ex);
            returnnew APIResponse(false, null, exception.getCode(), exception.getMessage());
        } else {
            log.error(String.format("訪問 %s -> %s 出現(xiàn)系統(tǒng)異常!", req.getRequestURI(), method.toString()), ex);
            returnnew APIResponse(false, null, GENERIC_SERVER_ERROR_CODE, GENERIC_SERVER_ERROR_MESSAGE);
        }
    }
}

運行時系統(tǒng)異常發(fā)生后,異常處理程序會直接將異常轉(zhuǎn)換為 JSON 返回給調(diào)用方:

圖片圖片

?? 提升建議:可以將相關(guān)的請求參數(shù)、響應(yīng)數(shù)據(jù)、用戶信息在脫敏后記錄到日志中,方便出現(xiàn)問題時根據(jù)上下文進(jìn)一步排查。

誤區(qū)二:捕獲異常后"生吞"

在任何時候,我們捕獲異常后都不應(yīng)該"生吞",也就是直接丟棄異常而不記錄、不拋出。這種處理方式還不如不捕獲異常,因為被生吞的異常一旦導(dǎo)致 Bug,就很難在程序中找到蛛絲馬跡,使得 Bug 排查難上加難。

通常情況下,生吞異常的原因可能是:

  • 不希望自己的方法拋出受檢異常,只是為了把異常"處理掉"而捕獲并生吞
  • 想當(dāng)然地認(rèn)為異常并不重要或不可能產(chǎn)生

但無論什么原因,無論你認(rèn)為異常多么不重要,都不應(yīng)該生吞,哪怕記錄一個日志也好。

誤區(qū)三:丟棄異常的原始信息

讓我們看兩個不太合適的異常處理方式,雖然沒有完全生吞異常,但也丟失了寶貴的異常信息。

假設(shè)有這樣一個會拋出受檢異常的方法:

private void readFile() throws IOException {
    Files.readAllLines(Paths.get("a_file"));
}

錯誤處理方式一:完全不記錄原始異常

@GetMapping("wrong1")
public void wrong1(){
    try {
        readFile();
    } catch (IOException e) {
        // 原始異常信息丟失  
        throw new RuntimeException("系統(tǒng)忙請稍后再試");
    }
}

這樣處理后,出現(xiàn)問題時我們根本不知道 IOException 具體是哪里引起的。

錯誤處理方式二:只記錄異常消息

catch (IOException e) {
    // 只保留了異常消息,棧信息沒有記錄
    log.error("文件讀取錯誤, {}", e.getMessage());
    throw new RuntimeException("系統(tǒng)忙請稍后再試");
}

留下的日志如下,看完一臉茫然,只知道文件讀取錯誤和文件名,至于為什么讀取錯誤、是不存在還是沒權(quán)限,完全不知道:

[12:57:19.746] [http-nio-45678-exec-1] [ERROR] [.g.t.c.e.d.HandleExceptionController:35  ] - 文件讀取錯誤, a_file

正確的處理方式

catch (IOException e) {
    log.error("文件讀取錯誤", e);
    throw new RuntimeException("系統(tǒng)忙請稍后再試");
}

或者將原始異常作為轉(zhuǎn)換后新異常的 cause,這樣原始異常信息同樣不會丟失:

catch (IOException e) {
    throw new RuntimeException("系統(tǒng)忙請稍后再試", e);
}

JDK 內(nèi)部的類似問題

有趣的是,JDK 內(nèi)部也會犯類似的錯誤。我曾遇到一個使用 JDK10 的應(yīng)用偶發(fā)啟動失敗的案例,日志中出現(xiàn)這樣的錯誤信息:

Caused by: java.lang.SecurityException: Couldn't parse jurisdiction policy files in: unlimited
  at java.base/javax.crypto.JceSecurity.setupJurisdictionPolicies(JceSecurity.java:355)
  at java.base/javax.crypto.JceSecurity.access$000(JceSecurity.java:73)
  at java.base/javax.crypto.JceSecurity$1.run(JceSecurity.java:109)
  at java.base/javax.crypto.JceSecurity$1.run(JceSecurity.java:106)
  at java.base/java.security.AccessController.doPrivileged(Native Method)
  at java.base/javax.crypto.JceSecurity.<clinit>(JceSecurity.java:105)
  ... 20 more

查看 JDK JceSecurity 類 setupJurisdictionPolicies 方法源碼,發(fā)現(xiàn)異常 e 既沒有記錄,也沒有作為新拋出異常的 cause。當(dāng)時讀取文件具體出現(xiàn)什么異常(權(quán)限問題還是 IO 問題)可能永遠(yuǎn)無法知道,這給問題定位造成了很大困擾。

圖片圖片

誤區(qū)四:拋出無消息異常

我見過一些代碼中的偷懶做法,直接拋出沒有 message 的異常:

throw new RuntimeException();

寫這種代碼的同學(xué)可能覺得永遠(yuǎn)不會走到這個邏輯,永遠(yuǎn)不會出現(xiàn)這樣的異常。但當(dāng)這樣的異常真的出現(xiàn),被 ExceptionHandler 攔截后,會輸出如下日志信息:

[13:25:18.031] [http-nio-45678-exec-3] [ERROR] [c.e.d.RestControllerExceptionHandler:24  ] - 訪問 /handleexception/wrong3 -> org.geekbang.time.commonmistakes.exception.demo1.HandleExceptionController#wrong3(String) 出現(xiàn)系統(tǒng)異常!
java.lang.RuntimeException: null
...

這里的 null 非常容易引起誤解,讓人以為是空指針問題,實際上是異常的 message 為空。

異常處理的三種模式

總結(jié)一下,如果你捕獲了異常打算處理,除了通過日志正確記錄異常原始信息外,通常還有三種處理模式:

  1. 轉(zhuǎn)換:轉(zhuǎn)換為新的異常拋出。新異常最好具有特定的分類和明確的異常消息,而不是隨便拋一個無關(guān)或沒有任何信息的異常,并最好通過 cause 關(guān)聯(lián)原異常
  2. 重試:重試之前的操作。比如遠(yuǎn)程調(diào)用服務(wù)端過載超時的情況,需要考慮當(dāng)前情況是否適合重試,盲目重試會讓問題更嚴(yán)重
  3. 恢復(fù):嘗試進(jìn)行降級處理,或使用默認(rèn)值替代原始數(shù)據(jù)

以上就是通過 catch 捕獲處理異常的一些最佳實踐。

finally 中的異常陷阱

有時候,我們希望無論是否遇到異常,邏輯完成后都要釋放資源,這時可以使用 finally 代碼塊。但要格外小心 finally 代碼塊中的異常,因為資源釋放處理等收尾操作同樣可能出現(xiàn)異常。

異常覆蓋問題

看下面這段代碼,我們在 finally 中拋出一個異常:

@GetMapping("wrong")
public void wrong() {
    try {
        log.info("try");
        // 異常丟失
        throw new RuntimeException("try");
    } finally {
        log.info("finally");
        throw new RuntimeException("finally");
    }
}

最后在日志中只能看到 finally 中的異常,雖然 try 中的邏輯出現(xiàn)了異常,但卻被 finally 中的異常覆蓋了。這非常危險,特別是 finally 中出現(xiàn)的異常是偶發(fā)的,就會在部分時候覆蓋 try 中的異常,讓問題更不明顯:

[13:34:42.247] [http-nio-45678-exec-1] [ERROR] [.a.c.c.C.[.[.[/].[dispatcherServlet]:175 ] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: finally] with root cause
java.lang.RuntimeException: finally

異常被覆蓋的原因很簡單:一個方法無法同時出現(xiàn)兩個異常。

解決方案

方案一:finally 代碼塊自己處理異常

@GetMapping("right")
public void right() {
    try {
        log.info("try");
        thrownew RuntimeException("try");
    } finally {
        log.info("finally");
        try {
            thrownew RuntimeException("finally");
        } catch (Exception ex) {
            log.error("finally", ex);
        }
    }
}

方案二:使用 addSuppressed 方法

將 try 中的異常作為主異常拋出,使用 addSuppressed 方法把 finally 中的異常附加到主異常上:

@GetMapping("right2")
public void right2() throws Exception {
    Exception e = null;
    try {
        log.info("try");
        thrownew RuntimeException("try");
    } catch (Exception ex) {
        e = ex;
    } finally {
        log.info("finally");
        try {
            thrownew RuntimeException("finally");
        } catch (Exception ex) {
            if (e != null) {
                e.addSuppressed(ex);
            } else {
                e = ex;
            }
        }
    }
    throw e;
}

運行這個方法可以得到如下異常信息,其中同時包含了主異常和被屏蔽的異常:

java.lang.RuntimeException: try
  at org.geekbang.time.commonmistakes.exception.finallyissue.FinallyIssueController.right2(FinallyIssueController.java:69)
  at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  ...
  Suppressed: java.lang.RuntimeException: finally
    at org.geekbang.time.commonmistakes.exception.finallyissue.FinallyIssueController.right2(FinallyIssueController.java:75)
    ... 54 common frames omitted

try-with-resources 的優(yōu)勢

這正是 try-with-resources 語句的做法。對于實現(xiàn)了 AutoCloseable 接口的資源,建議使用 try-with-resources 來釋放資源,否則也可能產(chǎn)生剛才提到的釋放資源時的異常覆蓋主異常的問題。

比如定義一個測試資源,其 read 和 close 方法都會拋出異常:

public class TestResource implements AutoCloseable {

    public void read() throws Exception{
        throw new Exception("read error");
    }

    @Override
    public void close() throws Exception {
        throw new Exception("close error");
    }
}

使用傳統(tǒng) try-finally 語句

@GetMapping("useresourcewrong")
public void useresourcewrong() throws Exception {
    TestResource testResource = new TestResource();
    try {
        testResource.read();
    } finally {
        testResource.close();
    }
}

可以看到,同樣出現(xiàn)了 finally 中的異常覆蓋 try 中異常的問題:

java.lang.Exception: close error
  at org.geekbang.time.commonmistakes.exception.finallyissue.TestResource.close(TestResource.java:10)
  at org.geekbang.time.commonmistakes.exception.finallyissue.FinallyIssueController.useresourcewrong(FinallyIssueController.java:27)

改為 try-with-resources 模式

@GetMapping("useresourceright")
public void useresourceright() throws Exception {
    try (TestResource testResource = new TestResource()){
        testResource.read();
    }
}

try 和 finally 中的異常信息都可以得到保留:

java.lang.Exception: read error
  at org.geekbang.time.commonmistakes.exception.finallyissue.TestResource.read(TestResource.java:6)
  ...
  Suppressed: java.lang.Exception: close error
    at org.geekbang.time.commonmistakes.exception.finallyissue.TestResource.close(TestResource.java:10)
    at org.geekbang.time.commonmistakes.exception.finallyissue.FinallyIssueController.useresourceright(FinallyIssueController.java:35)
    ... 54 common frames omitted

不要將異常定義為靜態(tài)變量

既然我們通常會自定義業(yè)務(wù)異常類型來包含更多異常信息(如異常錯誤碼、友好的錯誤提示等),那就需要在業(yè)務(wù)邏輯各處手動拋出各種業(yè)務(wù)異常來返回指定的錯誤碼描述(比如對于下單操作,用戶不存在返回 2001,商品缺貨返回 2002 等)。

對于這些異常的錯誤代碼和消息,我們期望能夠統(tǒng)一管理,而不是散落在程序各處定義。這個想法很好,但稍有不慎就可能掉入將異常定義為靜態(tài)變量的陷阱。

問題復(fù)現(xiàn)

我在排查某項目生產(chǎn)問題時,遇到了一件非常詭異的事情:異常堆棧信息顯示的方法調(diào)用路徑,在當(dāng)前入?yún)⒌那闆r下根本不可能產(chǎn)生。項目的業(yè)務(wù)邏輯又很復(fù)雜,我始終沒往異常信息是錯的這方面想,總覺得是因為某個分支流程導(dǎo)致業(yè)務(wù)沒有按照期望的流程進(jìn)行。

經(jīng)過艱難的排查,最終定位到原因是將異常定義為了靜態(tài)變量,導(dǎo)致異常棧信息錯亂。類似于定義一個 Exceptions 類來匯總所有的異常,把異常存放在靜態(tài)字段中:

public class Exceptions {
    public static BusinessException ORDEREXISTS = new BusinessException("訂單已經(jīng)存在", 3001);
    // ...
}

根本問題:將異常定義為靜態(tài)變量會導(dǎo)致異常信息固化,這與異常的棧信息需要根據(jù)當(dāng)前調(diào)用動態(tài)獲取的特性相矛盾。

問題演示

定義兩個方法 createOrderWrong 和 cancelOrderWrong,它們內(nèi)部都會通過 Exceptions 類獲得一個訂單已存在的異常,然后先后調(diào)用兩個方法并拋出:

@GetMapping("wrong")
public void wrong() {
    try {
        createOrderWrong();
    } catch (Exception ex) {
        log.error("createOrder got error", ex);
    }

    try {
        cancelOrderWrong();
    } catch (Exception ex) {
        log.error("cancelOrder got error", ex);
    }
}

private void createOrderWrong() {
    // 這里有問題
    throw Exceptions.ORDEREXISTS;
}

private void cancelOrderWrong() {
    // 這里有問題
    throw Exceptions.ORDEREXISTS;
}

運行程序后看到如下日志,cancelOrder got error 的提示對應(yīng)了 createOrderWrong 方法。顯然,cancelOrderWrong 方法出錯后拋出的異常,其實是 createOrderWrong 方法出錯時的異常:

[14:05:25.782] [http-nio-45678-exec-1] [ERROR] [.c.e.d.PredefinedExceptionController:25  ] - cancelOrder got error
org.geekbang.time.commonmistakes.exception.demo2.BusinessException: 訂單已經(jīng)存在
  at org.geekbang.time.commonmistakes.exception.demo2.Exceptions.<clinit>(Exceptions.java:5)
  at org.geekbang.time.commonmistakes.exception.demo2.PredefinedExceptionController.createOrderWrong(PredefinedExceptionController.java:50)
  at org.geekbang.time.commonmistakes.exception.demo2.PredefinedExceptionController.wrong(PredefinedExceptionController.java:18)

正確的解決方案

修復(fù)方式很簡單,改一下 Exceptions 類的實現(xiàn),通過不同的方法將每一種異常都 new 出來拋出即可:

public class Exceptions {
    public static BusinessException orderExists(){
        return new BusinessException("訂單已經(jīng)存在", 3001);
    }
}

線程池任務(wù)異常處理

在介紹線程池時我提到,線程池常用于異步處理或并行處理。那么,將任務(wù)提交到線程池處理時,任務(wù)本身出現(xiàn)異常會怎樣呢?

execute 方法提交任務(wù)的異常處理

我們來看一個例子:提交 10 個任務(wù)到線程池異步處理,第 5 個任務(wù)拋出 RuntimeException,每個任務(wù)完成后都會輸出一行日志:

@GetMapping("execute")
public void execute() throws InterruptedException {
    String prefix = "test";
    ExecutorService threadPool = Executors.newFixedThreadPool(1, 
        new ThreadFactoryBuilder().setNameFormat(prefix+"%d").get());

    // 提交10個任務(wù)到線程池處理,第5個任務(wù)會拋出運行時異常
    IntStream.rangeClosed(1, 10).forEach(i -> threadPool.execute(() -> {
        if (i == 5) thrownew RuntimeException("error");
        log.info("I'm done : {}", i);
    }));

    threadPool.shutdown();
    threadPool.awaitTermination(1, TimeUnit.HOURS);
}

觀察日志可以發(fā)現(xiàn)兩個現(xiàn)象:

...
[14:33:55.990] [test0] [INFO ] [e.d.ThreadPoolAndExceptionController:26  ] - I'm done : 4
Exception in thread "test0" java.lang.RuntimeException: error
  at org.geekbang.time.commonmistakes.exception.demo3.ThreadPoolAndExceptionController.lambda$null$0(ThreadPoolAndExceptionController.java:25)
  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
  at java.lang.Thread.run(Thread.java:748)
[14:33:55.990] [test1] [INFO ] [e.d.ThreadPoolAndExceptionController:26  ] - I'm done : 6
...

現(xiàn)象分析:

  1. 任務(wù) 1 到 4 所在的線程是 test0,任務(wù) 6 開始運行在線程 test1
  2. 由于線程池通過線程工廠為線程使用統(tǒng)一的前綴 test 加上計數(shù)器進(jìn)行命名,從線程名的改變可以知道因為異常的拋出,舊線程退出了,線程池只能重新創(chuàng)建一個線程
  3. 如果每個異步任務(wù)都以異常結(jié)束,那么線程池可能完全起不到線程重用的作用

因為沒有手動捕獲異常進(jìn)行處理,ThreadGroup 幫我們進(jìn)行了未捕獲異常的默認(rèn)處理,向標(biāo)準(zhǔn)錯誤輸出打印了出現(xiàn)異常的線程名稱和異常信息。顯然,這種沒有統(tǒng)一錯誤日志格式的打印形式對生產(chǎn)級代碼是不合適的。ThreadGroup 的相關(guān)源碼如下:

public void uncaughtException(Thread t, Throwable e) {
    if (parent != null) {
        parent.uncaughtException(t, e);
    } else {
        Thread.UncaughtExceptionHandler ueh =
            Thread.getDefaultUncaughtExceptionHandler();
        if (ueh != null) {
            ueh.uncaughtException(t, e);
        } elseif (!(e instanceof ThreadDeath)) {
            System.err.print("Exception in thread \""
                             + t.getName() + "\" ");
            e.printStackTrace(System.err);
        }
    }
}

修復(fù)方式有兩步:

  1. 以 execute 方法提交到線程池的異步任務(wù),最好在任務(wù)內(nèi)部做好異常處理
  2. 設(shè)置自定義的異常處理程序作為保底,比如在聲明線程池時自定義線程池的未捕獲異常處理程序:
new ThreadFactoryBuilder()
  .setNameFormat(prefix+"%d")
  .setUncaughtExceptionHandler((thread, throwable)-> 
    log.error("ThreadPool {} got exception", thread, throwable))
  .get()

或者設(shè)置全局的默認(rèn)未捕獲異常處理程序:

static {
    Thread.setDefaultUncaughtExceptionHandler((thread, throwable)-> 
        log.error("Thread {} got exception", thread, throwable));
}

submit 方法提交任務(wù)的異常處理

通過線程池 ExecutorService 的 execute 方法提交任務(wù)到線程池處理,如果出現(xiàn)異常會導(dǎo)致線程退出,控制臺輸出中可以看到異常信息。那么,把 execute 方法改為 submit,線程還會退出嗎?異常還能被處理程序捕獲到嗎?

修改代碼后重新執(zhí)行程序可以看到如下日志,說明線程沒退出,異常也被生吞了:

[15:44:33.769] [test0] [INFO ] [e.d.ThreadPoolAndExceptionController:47  ] - I'm done : 1
[15:44:33.770] [test0] [INFO ] [e.d.ThreadPoolAndExceptionController:47  ] - I'm done : 2
[15:44:33.770] [test0] [INFO ] [e.d.ThreadPoolAndExceptionController:47  ] - I'm done : 3
[15:44:33.770] [test0] [INFO ] [e.d.ThreadPoolAndExceptionController:47  ] - I'm done : 4
[15:44:33.770] [test0] [INFO ] [e.d.ThreadPoolAndExceptionController:47  ] - I'm done : 6
[15:44:33.770] [test0] [INFO ] [e.d.ThreadPoolAndExceptionController:47  ] - I'm done : 7
[15:44:33.770] [test0] [INFO ] [e.d.ThreadPoolAndExceptionController:47  ] - I'm done : 8
[15:44:33.771] [test0] [INFO ] [e.d.ThreadPoolAndExceptionController:47  ] - I'm done : 9
[15:44:33.771] [test0] [INFO ] [e.d.ThreadPoolAndExceptionController:47  ] - I'm done : 10

為什么會這樣?

查看 FutureTask 源碼可以發(fā)現(xiàn),在執(zhí)行任務(wù)出現(xiàn)異常之后,異常存儲到了一個 outcome 字段中,只有在調(diào)用 get 方法獲取 FutureTask 結(jié)果的時候,才會以 ExecutionException 的形式重新拋出異常:

public void run() {
// ...
    try {
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
                result = c.call();
                ran = true;
            } catch (Throwable ex) {
                result = null;
                ran = false;
                setException(ex);
            }
// ...
}

protected void setException(Throwable t) {
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        outcome = t;
        UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
        finishCompletion();
    }
}

public V get() throws InterruptedException, ExecutionException {
    int s = state;
    if (s <= COMPLETING)
        s = awaitDone(false, 0L);
    return report(s);
}

private V report(int s) throws ExecutionException {
    Object x = outcome;
    if (s == NORMAL)
        return (V)x;
    if (s >= CANCELLED)
        thrownew CancellationException();
    thrownew ExecutionException((Throwable)x);
}

正確的處理方式:

既然是以 submit 方式提交任務(wù),那么我們應(yīng)該關(guān)心任務(wù)的執(zhí)行結(jié)果,否則應(yīng)該以 execute 來提交任務(wù)。修改后的代碼如下,我們把 submit 返回的 Future 放到 List 中,隨后遍歷 List 來捕獲所有任務(wù)的異常:

List<Future> tasks = IntStream.rangeClosed(1, 10).mapToObj(i -> threadPool.submit(() -> {
    if (i == 5) throw new RuntimeException("error");
    log.info("I'm done : {}", i);
})).collect(Collectors.toList());

tasks.forEach(task-> {
    try {
        task.get();
    } catch (Exception e) {
        log.error("Got exception", e);
    }
});

執(zhí)行這段程序可以看到如下的日志輸出:

[15:44:13.543] [http-nio-45678-exec-1] [ERROR] [e.d.ThreadPoolAndExceptionController:69  ] - Got exception
java.util.concurrent.ExecutionException: java.lang.RuntimeException: error

總結(jié)與最佳實踐

通過今天的文章,我介紹了處理異常容易犯的幾個錯誤和最佳實踐:

核心原則

第一,精細(xì)化異常處理策略

  • 不應(yīng)該用 AOP 對所有方法進(jìn)行統(tǒng)一異常處理
  • 異常要么不捕獲不處理,要么根據(jù)不同的業(yè)務(wù)邏輯、不同的異常類型進(jìn)行精細(xì)化、針對性處理
  • 處理異常應(yīng)該杜絕生吞,并確保異常棧信息得到保留
  • 如果需要重新拋出異常,請使用具有意義的異常類型和異常消息

第二,小心 finally 代碼塊中的資源處理

  • 確保 finally 代碼塊不出現(xiàn)異常,內(nèi)部把異常處理完畢,避免 finally 中的異常覆蓋 try 中的異常
  • 或者考慮使用 addSuppressed 方法把 finally 中的異常附加到 try 中的異常上,確保主異常信息不丟失
  • 使用實現(xiàn)了 AutoCloseable 接口的資源,務(wù)必使用 try-with-resources 模式,確保資源正確釋放和異常正確處理

第三,正確定義業(yè)務(wù)異常

  • 雖然在統(tǒng)一的地方定義收口所有的業(yè)務(wù)異常是個不錯的實踐,但務(wù)必確保異常是每次 new 出來的
  • 不能使用預(yù)先定義的 static 字段存放異常,否則可能引起棧信息錯亂

第四,正確處理線程池中任務(wù)的異常

  • 如果任務(wù)通過 execute 提交,出現(xiàn)異常會導(dǎo)致線程退出,大量異常會導(dǎo)致線程重復(fù)創(chuàng)建引起性能問題
  • 應(yīng)該盡可能確保任務(wù)不出異常,同時設(shè)置默認(rèn)的未捕獲異常處理程序來兜底
  • 如果任務(wù)通過 submit 提交,意味著我們關(guān)心任務(wù)的執(zhí)行結(jié)果,應(yīng)該通過拿到的 Future 調(diào)用其 get 方法來獲得任務(wù)運行結(jié)果和可能出現(xiàn)的異常,否則異常可能就被生吞了

實用建議

  • 記錄完整的上下文信息:異常日志中應(yīng)包含請求參數(shù)、用戶信息等上下文,方便問題排查
  • 使用合適的日志級別:業(yè)務(wù)異常使用 Warn 級別,系統(tǒng)異常使用 Error 級別
  • 提供友好的錯誤消息:面向用戶的異常信息應(yīng)該友好且有意義
  • 建立異常處理規(guī)范:團(tuán)隊?wèi)?yīng)該建立統(tǒng)一的異常處理規(guī)范和最佳實踐

掌握了這些異常處理的最佳實踐,相信你能寫出更加健壯和優(yōu)雅的代碼。記住,異常處理不是可有可無的"裝飾",而是保證系統(tǒng)穩(wěn)定性和可維護(hù)性的重要基石。

責(zé)任編輯:武曉燕 來源: JAVA日知錄
相關(guān)推薦

2025-09-05 01:23:00

PyTorchPython分支

2021-08-12 11:37:23

數(shù)據(jù)分析錯誤

2019-10-28 09:53:42

Java開發(fā)結(jié)構(gòu)

2011-11-24 16:34:39

Java

2024-08-20 11:40:24

2025-01-06 08:33:10

2024-11-04 09:39:08

Java?接口Thread?類

2023-10-30 11:40:36

OOM線程池單線程

2021-06-04 10:45:31

軟件架構(gòu)分布式

2025-01-15 12:43:23

2019-08-29 09:15:30

負(fù)載均衡算法備份

2022-03-17 08:34:47

TypeScript項目類型

2022-09-02 14:29:01

JavaScrip數(shù)組屬性

2022-07-20 09:06:27

Hook封裝工具庫

2019-11-14 09:19:47

Python程序員系統(tǒng)

2020-12-09 11:21:48

大數(shù)據(jù)數(shù)據(jù)分析

2020-12-09 10:56:15

業(yè)務(wù)分析數(shù)據(jù)分析大數(shù)據(jù)

2017-04-07 12:30:38

2024-10-24 08:04:00

2017-07-14 16:28:21

點贊
收藏

51CTO技術(shù)棧公眾號

一区二区三区**美女毛片| 欧美破处大片在线视频| 在线亚洲人成电影网站色www| 香蕉久久夜色| 亚洲av无码一区二区三区性色| 99精品久久| 色先锋资源久久综合5566| 99国产精品免费视频| 免费成人直播| 有码一区二区三区| 免费看污久久久| www.日日夜夜| 日本va欧美va欧美va精品| 精品视频9999| 亚洲一二三四五六区| 久久激情av| 91精品国产色综合久久ai换脸| 男人的天堂狠狠干| 黄色网在线免费观看| 99re这里只有精品首页| 成人午夜黄色影院| 久久久久久无码午夜精品直播| 综合激情网站| 日韩中文在线观看| 9.1成人看片免费版| 欧美另类中文字幕| 欧美日韩亚州综合| 91视频 -- 69xx| 羞羞的网站在线观看| 国产精品三级av| 欧美一区二区三区四区夜夜大片| 成人av免费播放| 精品在线播放免费| 国产日韩欧美在线| 天天操天天干视频| 亚洲精品1区2区| 欧美成人在线免费| a在线视频播放观看免费观看| 国内精品久久久久久久久电影网 | avtt久久| 精品婷婷伊人一区三区三| 欧美牲交a欧美牲交aⅴ免费真| 欧美女同一区| 亚洲美女免费在线| 福利网在线观看| 天堂中文а√在线| 中文字幕在线观看一区二区| 性欧美精品一区二区三区在线播放 | 国产精品加勒比| 亚洲av无码一区二区乱子伦| 国产一区二区三区精品视频| 国产日韩欧美视频在线| 伊人久久成人网| 六月丁香婷婷久久| 成人精品久久av网站| 97精品人妻一区二区三区| 人人爽香蕉精品| 国产精品久久久久久久久免费| 一级黄色av片| 青青草91视频| 国产伊人精品在线| 国产精品久久免费| 国产主播一区二区三区| 成人亲热视频网站| 亚洲春色一区二区三区| 成人在线一区二区三区| 国产二区不卡| 涩爱av在线播放一区二区| 久久网站最新地址| 亚洲精品一区二区三区樱花| 日本电影在线观看网站| 亚洲日本乱码在线观看| www.国产在线视频| 色偷偷偷在线视频播放| 日本道在线观看一区二区| 麻豆一区二区三区视频| 四虎影视国产精品| 日韩精品专区在线影院重磅| 无码人妻丰满熟妇区毛片蜜桃精品| 精品淫伦v久久水蜜桃| 亚洲欧美国产另类| av女人的天堂| 国产精品久久天天影视| 欧美高清性猛交| 久久久久久久久久久久久av| 奇米一区二区三区av| 成人午夜激情网| 熟妇人妻系列aⅴ无码专区友真希 熟妇人妻av无码一区二区三区 | 国产91成人在在线播放| 在线观看免费中文字幕| 国产成人久久精品77777最新版本| 精品国产免费人成电影在线观...| 久久精品色图| 一级特黄大欧美久久久| 国产一区亚洲二区三区| 国产精品视频一区视频二区| 日韩av中文字幕在线播放| 欧洲性xxxx| 亚洲国产一区二区三区a毛片| 国产精欧美一区二区三区| 国产精品-色哟哟| 2020国产精品久久精品美国| 久久久久久久久久久久久国产| 黄视频网站在线观看| 3d动漫精品啪啪一区二区竹菊| 国产精品无码专区| 亚洲最大av| 国产精品久久二区| 日韩在线观看视频网站| 国产精品乱人伦一区二区| 国产综合中文字幕| 我要色综合中文字幕| 在线观看视频99| 欧美一区二区激情视频| 国产成人在线看| 亚洲免费视频一区| 亚洲女同志freevdieo| 日韩三级av在线播放| 精品伦精品一区二区三区视频密桃 | 97久久精品人人爽人人爽蜜臀| 亚洲一区在线直播| 日韩福利一区| 亚洲精品久久久久中文字幕欢迎你 | 国自产拍在线网站网址视频| 亚洲午夜久久久久久久久电影网| 日本人69视频| 日韩专区精品| 国产精品成人免费电影| 全色精品综合影院| 精品欧美国产一区二区三区| 欧美xxxx黑人| 一本一道久久综合狠狠老| 国产精品一区av| 国产专区在线| 色噜噜狠狠一区二区三区果冻| 国产毛片毛片毛片毛片毛片毛片| 欧美日韩综合| 国产精品国产三级国产专区53| 性欧美高清come| 91精品麻豆日日躁夜夜躁| 国产又粗又猛又爽又黄的视频四季 | 国产精国产精品| 毛片在线播放网址| 色88888久久久久久影院野外| 国产三级国产精品| 国产精品毛片一区二区三区| 久久久99爱| 日韩伦理在线| 亚洲精品一区av在线播放| 亚洲黄色小说图片| 久久久久久久久岛国免费| 777久久久精品一区二区三区| 婷婷综合成人| 国产精品pans私拍| 国产香蕉在线| 欧美日韩视频在线一区二区| 99精品中文字幕| 国产乱码精品一区二区三区av | 特级西西人体高清大胆| 蜜臀久久99精品久久久久宅男| 一区二区不卡在线| 国产精品**亚洲精品| 九九热精品在线| 黄色片一区二区三区| 精品国产91久久久| 国产精品毛片一区二区| 青青国产91久久久久久| 艳母动漫在线观看| 国产精品流白浆在线观看| 91sao在线观看国产| 国产午夜视频在线观看| 91精品福利在线一区二区三区| 国产97免费视频| 99久久精品免费| 日本特黄a级片| 欧美成人一区二免费视频软件| 懂色中文一区二区三区在线视频| 在线观看涩涩| 日韩中文字幕免费| 丰满熟妇乱又伦| 色婷婷综合久久久久中文 | 久久99爱视频| 国产农村妇女aaaaa视频| 久久久久久久久久看片| 毛片毛片毛片毛| 国产一区激情| 欧美一区二区三区四区在线观看地址| 麻豆久久久久| 国语对白做受69| 国产粉嫩一区二区三区在线观看| 91精选在线观看| 成人精品免费在线观看| 成人欧美一区二区三区1314| 国产日韩视频一区| 蜜桃av噜噜一区| 欧美精品卡一卡二| 日韩黄色大片| 好吊色欧美一区二区三区四区 | 国产精品美女呻吟| 色噜噜狠狠狠综合欧洲色8| 亚洲欧洲在线免费| 精品人妻无码一区二区| 在线观看区一区二| 国产极品在线播放| 国产精品久久久久久久久快鸭| 无码精品一区二区三区在线播放 | 日本中文字幕在线不卡| 国产伦理一区| 免费看日b视频| 色小子综合网| 欧美日韩视频在线一区二区观看视频| 国产美女亚洲精品7777| 国产精品美女主播| 精品众筹模特私拍视频| 日韩一区二区久久久| 日本不卡免费播放| 亚洲精品一区二区三区福利| 91久久精品国产91性色69| 色网综合在线观看| 国产视频91在线| 亚洲最大成人综合| 国产一区二区精彩视频| 国产精品麻豆欧美日韩ww| 国产精品无码久久久久一区二区| 国产99精品国产| 91亚洲一区二区| 久草在线在线精品观看| 天天操天天摸天天爽| 麻豆精品网站| 亚洲熟女乱色一区二区三区| 亚洲高清二区| 国产精品12345| 在线电影一区| 97干在线视频| 亚洲二区精品| 国产精品专区在线| 亚洲精品少妇| 久久亚洲中文字幕无码| 亚洲精品黄色| www.com毛片| 久久不射网站| 116极品美女午夜一级| 亚洲欧美日韩视频二区| 日本一区二区黄色| 视频一区中文字幕| www.欧美日本| 蜜桃av一区二区在线观看 | 懂色av一区二区三区免费观看| 久久婷婷中文字幕| 国产在线视频精品一区| 日本黄色www| 成人美女视频在线看| 性欧美丰满熟妇xxxx性久久久| 91免费国产在线| 一道本在线观看| 国产精品乱人伦中文| 国产日韩欧美在线观看视频| 亚洲精品菠萝久久久久久久| 免费毛片在线播放免费 | aa视频在线播放| 亚洲国产美女| 50路60路老熟妇啪啪| 免费看欧美女人艹b| 婷婷激情5月天| 成人免费高清在线| 亚洲第一香蕉网| 中文字幕一区二区不卡| 九九在线观看视频| 欧美性生交大片免费| 真实的国产乱xxxx在线91| 欧美人伦禁忌dvd放荡欲情| 国产精品永久久久久久久久久| 日韩一区二区免费在线观看| 欧性猛交ⅹxxx乱大交| 亚洲摸下面视频| 男人天堂久久久| 久久久久久伊人| 欧美大片高清| 91久久精品国产91久久性色| 2020国产精品极品色在线观看| 国产一级精品aaaaa看| 精品国产一区二区三区四区| 加勒比海盗1在线观看免费国语版| 国产欧美成人| 久久久久久久久久久久91| 国产成人免费在线视频| 欧美另类z0zx974| 亚洲麻豆国产自偷在线| 黄色片网站在线免费观看| 欧美女孩性生活视频| 欧美一区二区三区黄片| 综合国产在线观看| 3344国产永久在线观看视频| 国产欧美一区二区白浆黑人| 狠狠一区二区三区| 亚洲精品国产一区| 国产日韩综合| 亚洲制服在线观看| 国产精品污污网站在线观看| 日本三级网站在线观看| 欧美日韩国产a| 日本在线丨区| 欧美精品videosex极品1| 黑人一区二区三区| 欧美午夜欧美| 亚洲免费大片| 青青草精品在线| 中文字幕av资源一区| 久久99国产综合精品免费| 精品国产污污免费网站入口 | 国产91精品久久久久久| 欧美二区观看| 在线不卡视频一区二区| 久久黄色网页| 久久偷拍免费视频| 亚洲午夜三级在线| 国产av精国产传媒| 色噜噜久久综合伊人一本| 性欧美videohd高精| 国产一区二区免费电影| 欧美日韩hd| 999久久久精品视频| 亚洲国产成人在线| 波多野结衣视频观看| 国产丝袜高跟一区| 成年女人在线看片| 国产精品毛片va一区二区三区| 91成人国产| 999久久久精品视频| 国产精品福利一区二区三区| 久久久久久亚洲av无码专区| 精品夜色国产国偷在线| 91精品论坛| 欧美极品一区| 久久综合伊人| 亚洲精品成人无码| 色狠狠色噜噜噜综合网| 色综合成人av| 国产不卡在线观看| 国产探花在线精品一区二区| 农村妇女精品一二区| 国产日韩欧美精品综合| 超碰在线观看91| 国产一区二区三区在线| 成人性片免费| 国产奶头好大揉着好爽视频| 国产一区二区美女| 久久99久久98精品免观看软件| 日韩精品资源二区在线| av色在线观看| 久久综合色一本| 日本三级亚洲精品| 久久噜噜色综合一区二区| 91精品国产欧美日韩| 欧美四级在线| 激情久久av| 日本中文字幕一区| 91香蕉一区二区三区在线观看| 欧美一级片免费看| japanese色国产在线看视频| 精品一区二区国产| 三级不卡在线观看| 视频国产一区二区| 日韩美女在线视频| 日韩精品99| 一级黄色录像免费看| 国产99久久精品| 亚洲毛片一区二区三区| 色悠悠久久久久| h视频久久久| 十八禁视频网站在线观看| 最近日韩中文字幕| 日韩在线一区二区三区四区| 日本人成精品视频在线| 天天综合网网欲色| 中文字幕人妻一区| 在线免费观看视频一区| 成人午夜在线影视| 国产一区国产精品| 美女诱惑一区二区| 国产亚洲色婷婷久久99精品| 亚洲欧美三级伦理| 日韩免费成人| av观看免费在线| 亚洲精品久久嫩草网站秘色| 天堂中文在线资| 91久久精品国产| 久久www成人_看片免费不卡| www.5588.com毛片| 亚洲乱码一区二区| 涩爱av色老久久精品偷偷鲁| 欧美色图色综合| 亚洲免费色视频| 成年午夜在线| 国产在线视频欧美一区二区三区| 免费av网站大全久久| 日产欧产va高清| 久久精品国产2020观看福利| 蜜臀av免费一区二区三区| 亚洲国产欧美91|