精品欧美一区二区三区在线观看 _久久久久国色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ù)棧公眾號

一区二区三区福利| 青青操综合网| 亚洲精品自拍动漫在线| 国产精品亚洲不卡a| 女人十八岁毛片| 成人午夜av| 欧美欧美欧美欧美首页| 无码 制服 丝袜 国产 另类| 韩国中文免费在线视频| 国产制服丝袜一区| 91国内精品久久| 成人信息集中地| 美女网站色精品尤物极品姐弟| 欧美三级免费观看| 亚洲制服中文| 婷婷在线观看视频| 久久精品国产精品亚洲红杏| 7777免费精品视频| 免费在线观看a级片| 亚州av日韩av| 日韩精品专区在线| 在线免费av播放| 蜜臀久久精品| 亚洲精品国产高清久久伦理二区| 欧美亚洲另类在线一区二区三区 | 精品国产91久久久久久老师| 亚洲永久激情精品| 可以在线观看的黄色| 国产suv精品一区二区883| 国产精品video| 91视频免费网址| 欧美日韩网站| 亚洲视频在线看| 国产一级免费片| 日本免费精品| 7777精品伊人久久久大香线蕉经典版下载 | 一本久道久久久| 免费99精品国产自在在线| 精品无码在线观看| 免费短视频成人日韩| 亚洲国产成人精品久久久国产成人一区| 色国产在线视频| 澳门av一区二区三区| 舔着乳尖日韩一区| 日韩一级性生活片| 日韩激情av| 亚洲美女偷拍久久| 日本一二三区视频在线| 黄色成人在线| 亚洲欧美自拍偷拍色图| 亚洲精品免费在线看| 川上优的av在线一区二区| 国产日产精品1区| 欧洲精品亚洲精品| 免费看男男www网站入口在线 | av电影在线不卡| 曰本一区二区三区视频| 亚洲美女性生活视频| 国产亚洲色婷婷久久99精品91| 99久久香蕉| 欧美不卡激情三级在线观看| 亚洲欧美综合视频| 精品精品精品| 日韩av最新在线观看| 性久久久久久久久久久| 欧美日韩一区二区三区四区不卡 | 亚洲第一香蕉网| 亚洲素人在线| 国产一区二区激情| www.4hu95.com四虎| 97欧美在线视频| 麻豆国产精品va在线观看不卡| www.av免费| 欧美不卡在线| 97在线视频免费看| 日韩熟女一区二区| 久久综合综合久久综合| 2014亚洲精品| 日日夜夜精品免费| 久久久久久久久蜜桃| 水蜜桃亚洲精品| 黄网站在线播放| 亚洲午夜免费视频| 男人天堂网视频| 黄色欧美视频| 日韩一区二区三区免费看 | 91久久国语露脸精品国产高跟| 久久国产视频网| 成人在线视频网址| 免费在线黄色网址| 亚洲免费看黄网站| 亚洲熟妇av一区二区三区漫画| 性欧美1819sex性高清| 欧美高清性hdvideosex| 欧美激情 亚洲| 美日韩中文字幕| 免费97视频在线精品国自产拍| 欧美另类一区二区| 另类成人小视频在线| 国产精品.com| 91高清在线| 午夜免费久久看| 亚洲一区在线不卡| 福利片一区二区| 中文字幕亚洲欧美日韩2019| 久久人人爽人人爽人人| 日本午夜精品视频在线观看| 91影院未满十八岁禁止入内| 国产精品四虎| 亚洲一区二区成人在线观看| 免费一级特黄录像| 精品中国亚洲| 久久精品免费电影| 中文字幕在线天堂| 成人免费视频一区| 91免费视频黄| 视频在线日韩| 日韩精品极品视频| 久久久久亚洲av片无码下载蜜桃| 久久久久久9| 国产免费一区二区三区| 国产精品剧情| 欧美日韩一区二区三区不卡| chinese麻豆新拍video| 欧美在线影院| 91免费视频网站| 国产香蕉在线| 欧美性猛交xxxx黑人猛交| wwwxxxx在线观看| 四虎国产精品免费观看| 国产精品电影观看| 青青草av免费在线观看| 亚洲高清中文字幕| 男生和女生一起差差差视频| 久久一区91| 国产精品久久久久秋霞鲁丝| 每日更新av在线播放| 亚洲成人7777| 99热超碰在线| 日韩一级精品| 国产视频一区二区三区四区| 免费在线播放电影| 欧美成人女星排名| 草视频在线观看| 国产一区二区三区在线观看免费| 致1999电视剧免费观看策驰影院| 韩国精品主播一区二区在线观看 | 成年人免费观看视频网站| 日韩午夜在线| 九九九九久久久久| 99re6在线精品视频免费播放| 精品欧美一区二区三区精品久久| 国产十六处破外女视频| 国产精品亚洲专一区二区三区| 波多野结衣激情| 激情久久免费视频| 欧美疯狂做受xxxx高潮| www天堂在线| 亚洲国产日韩一级| 亚洲调教欧美在线| 亚洲永久在线| 水蜜桃亚洲一二三四在线| 欧美极品在线| 久久综合五月天| www.com欧美| 亚洲电影中文字幕在线观看| 亚洲最大的黄色网| 日韩在线a电影| 亚洲精品中文字幕乱码三区不卡| 亚洲日日夜夜| 欧美富婆性猛交| 四虎在线免费观看| 在线免费精品视频| 99久久99久久精品国产| 成人性色生活片| 欧美精品色婷婷五月综合| 欧美在线观看视频一区| 91日韩在线视频| 韩国成人免费视频| 亚洲人成人99网站| 136福利视频导航| 一级日本不卡的影视| 丰满少妇一区二区三区| 免费精品视频最新在线| 欧美一二三不卡| 你懂的视频欧美| 成人免费视频网址| 国产拍在线视频| 亚洲欧美日韩成人| 国产精品永久久久久久久久久| 亚洲国产日韩精品| 91视频免费看片| 国产91在线|亚洲| 91蝌蚪视频在线观看| 综合天天久久| 日本精品免费| 亚洲天堂av资源在线观看| 欧美在线激情网| 国产网站在线免费观看| 亚洲精品美女久久久久| 国产又粗又大又黄| 精品国产乱码久久久久久婷婷| 久久久久99精品成人| 波多野结衣中文一区| mm131国产精品| 一本色道久久综合亚洲精品不| 一本一本久久a久久精品综合妖精| 最新国产一区二区| 国产综合视频在线观看| 中文字幕乱码在线播放| 欧美成人一区二区三区电影| 精品影院一区| 亚洲国产精品女人久久久| 一级二级三级视频| 一本色道久久综合精品竹菊| 日本在线一级片| 日本一区二区三区dvd视频在线| 亚洲欧美日韩偷拍| 国产一区二区三区四区在线观看| 最新中文字幕免费视频| 国产欧美激情| 青青青青草视频| 亚洲精品成人无限看| 亚洲欧美丝袜| 精品国产一区二区三区小蝌蚪| 国内精品久久国产| 91欧美极品| 亚洲一区美女视频在线观看免费| 少妇精品视频一区二区免费看| 欧美亚洲午夜视频在线观看| 欧美巨大xxxx做受沙滩| 欧美精品在线免费观看| 乱人伦中文视频在线| 一本色道久久88精品综合| 色视频在线看| 日韩电影中文字幕av| 日韩一级片免费| 欧美精品一区在线观看| 精品国产亚洲一区二区麻豆| 欧美一区二区视频免费观看| 一级黄色免费片| 欧美日韩精品一区二区在线播放| 国产精品免费无遮挡无码永久视频| 都市激情亚洲色图| 五月婷婷亚洲综合| 精品欧美一区二区三区| 色播视频在线播放| 欧美日韩国产一中文字不卡| 亚洲免费激情视频| 狠狠色狠色综合曰曰| 精品国产乱码一区二区| 精品久久久久国产| 日韩中文字幕在线观看视频| 欧美性黄网官网| 波多野结衣一二区| 欧美视频在线观看一区二区| 亚洲一区在线观| 91精品国产麻豆| 性生交生活影碟片| 亚洲精品在线三区| 五月婷婷六月激情| 亚洲视频日韩精品| 日本高清在线观看wwwww色| 日韩中文字幕网| 日本在线视频网址| 午夜精品在线视频| 桃子视频成人app| 国产日韩欧美一二三区| 亚洲va欧美va人人爽成人影院| 风间由美久久久| 亚洲深夜福利在线观看| 亚洲国产激情一区二区三区| 国产精品国产三级国产在线观看| 亚洲色婷婷久久精品av蜜桃| 日韩香蕉视频| 污色网站在线观看| 国产风韵犹存在线视精品| 性色av蜜臀av浪潮av老女人| 久久久国产综合精品女国产盗摄| 人妻熟人中文字幕一区二区| 尤物在线观看一区| 97人人澡人人爽人人模亚洲 | 国产真实乱子伦| 看片的网站亚洲| xxxxxx黄色| 亚洲国产成人午夜在线一区| h色网站在线观看| 疯狂做受xxxx高潮欧美日本| 亚洲天堂国产精品| 亚洲国产福利在线| 波多野结衣在线网站| 欧美丰满少妇xxxx| 国产福利亚洲| 国产精品午夜av在线| 9999国产精品| 日本少妇高潮喷水视频| 麻豆精品国产91久久久久久| 中文视频在线观看| 国产精品成人一区二区三区夜夜夜| 日本一区二区不卡在线| 欧美日本一区二区| 少妇精品高潮欲妇又嫩中文字幕| 深夜福利国产精品| 毛片电影在线| 999在线免费观看视频| 国产一区二区三区探花| www污在线观看| 另类的小说在线视频另类成人小视频在线| youjizz.com日本| 成人免费在线视频| 日韩综合在线观看| 日韩欧美电影一区| 男人的天堂在线视频免费观看| 7m第一福利500精品视频| 久久久精品区| 亚洲永久激情精品| 老牛影视一区二区三区| 国产精品久久久久久亚洲色 | 欧美一级高潮片| 欧美精品黑人性xxxx| 国产小视频在线| 91国语精品自产拍在线观看性色| 经典三级久久| 亚洲欧美日本国产有色| 老司机一区二区三区| 星空大象在线观看免费播放| 一级精品视频在线观看宜春院| 国产精品无码天天爽视频| 在线播放精品一区二区三区 | 91久久久在线| 色欧美自拍视频| 免费看污黄网站| 久久久精品一品道一区| 国产成人一级片| 日韩hd视频在线观看| 97人人爽人人澡人人精品| 国产不卡一区二区三区在线观看| 久久久久久久久99精品大| 五月天av在线播放| 国产精品久久久久婷婷二区次| av手机天堂网| 最新国产精品亚洲| 国产一区二区三区四区五区3d| 日韩电影免费观看在| 日韩黄色在线观看| 摸摸摸bbb毛毛毛片| 欧美综合视频在线观看| h视频网站在线观看| 国产精品网站大全| 天天操夜夜操国产精品| 欧美大片久久久| 亚洲精品五月天| www.久久精品.com| 久久久久女教师免费一区| 中文在线免费一区三区| 成年人午夜免费视频| 99精品热视频| 欧美超碰在线观看| 正在播放欧美一区| 999色成人| cao在线观看| 久久亚洲一区二区三区四区| 最近免费中文字幕大全免费版视频| 亚洲色图五月天| 国语自产精品视频在线看抢先版结局| 亚洲精品成人三区| 国产精品亚洲专一区二区三区| 国产精品99re| 精品亚洲夜色av98在线观看| 台湾成人免费视频| 在线观看三级网站| 不卡视频免费播放| 国产成人a v| 日韩视频免费在线观看| 136国产福利精品导航网址应用| 日本网站免费在线观看| 欧美激情中文字幕| a级片免费观看| 欧美又大又硬又粗bbbbb| 成人精品电影| caopor在线| 欧美亚洲丝袜传媒另类| 最新av在线播放| 欧美第一黄网| 国精产品一区一区三区mba视频| 久视频在线观看| 国产一区二区三区18| 日韩高清在线观看一区二区| 午夜精品久久久久久久无码| 欧美极品xxx| 免费国产羞羞网站视频| 国产精品一区二区久久久| 欧美日韩一视频区二区| 国产精品成人无码免费| 欧美不卡123| 久久日本片精品aaaaa国产| 国产 日韩 亚洲 欧美| 国产精品国产自产拍高清av王其 | 国产一二三在线视频| 亚洲国产精品精华液2区45|