項目中處理異常的四種常見錯誤!你知道幾個?
應用程序難免會出現異常,而捕獲和處理異常是考驗編程功力的精細活。在一些業務項目中,我曾見過這樣的場景:開發同學在編寫業務邏輯時完全不考慮異常處理,等項目接近完成時再采用"流水線"的方式進行異常處理——統一為所有方法添加 try...catch... 來捕獲所有異常并記錄日志,技巧熟練的同學甚至會使用 AOP 來實現類似的"統一異常處理"。
然而,這種處理異常的方式是極其不可取的。今天,我就和你深入探討這種做法不可取的原因,以及異常處理中的常見陷阱和最佳實踐。
異常處理的常見誤區
誤區一:框架層面的粗暴"統一處理"
"統一異常處理"正是我要說的第一個誤區:不在業務代碼層面考慮異常處理,僅在框架層面粗暴地捕獲和處理異常。
要理解這種做法錯在何處,我們先來回顧一下大多數業務應用采用的三層架構:
- Controller 層:負責信息收集、參數校驗、數據轉換適配前端,承載輕量級業務邏輯
- Service 層:負責核心業務邏輯,包括各種外部服務調用、數據庫訪問、緩存處理、消息處理等
- Repository 層:負責數據訪問實現,通常不包含業務邏輯
圖片
由于每層架構的工作性質不同,加上從業務性質角度異常可分為業務異常和系統異常兩大類,這就決定了很難進行統一的異常處理。讓我們從底向上分析三層架構:
Repository 層的異常處理策略
- 異常可能需要忽略、降級處理,或者轉化為更友好的異常
- 如果一律捕獲異常僅記錄日志,很可能業務邏輯已經出錯,而用戶和程序本身完全感知不到
Service 層的異常處理策略
- 往往涉及數據庫事務,出現異常不應該被捕獲,否則事務無法自動回滾
- 涉及復雜業務邏輯,某些業務異常發生后可能需要轉入分支業務流程
- 如果業務異常都被框架捕獲,業務功能就會異常
Controller 層的異常處理策略
- 如果下層異常上升到這里仍無法處理,通常需要給用戶友好提示
- 或者根據每個 API 的異常表返回指定的異常類型
- 同樣無法對所有異常一視同仁
因此,我強烈不建議在框架層面進行異常的自動、統一處理,尤其不要隨意捕獲異常。不過,框架可以承擔兜底工作。如果異常上升到最上層邏輯仍無法處理,可以通過統一的方式進行異常轉換,比如使用 @RestControllerAdvice + @ExceptionHandler 來捕獲這些"未處理"異常:
- 自定義業務異常:以 Warn 級別記錄異常及當前 URL、執行方法等信息,提取異常中的錯誤碼和消息,轉換為合適的 API 響應體返回給調用方
- 無法處理的系統異常:以 Error 級別記錄異常和上下文信息(如 URL、參數、用戶 ID),轉換為通用的"服務器忙,請稍后再試"異常信息,同樣以 API 響應體返回給調用方
示例代碼如下:
@RestControllerAdvice
@Slf4j
publicclass RestControllerExceptionHandler {
privatestaticint GENERIC_SERVER_ERROR_CODE = 2000;
privatestatic String GENERIC_SERVER_ERROR_MESSAGE = "服務器忙,請稍后再試";
@ExceptionHandler
public APIResponse handle(HttpServletRequest req, HandlerMethod method, Exception ex) {
if (ex instanceof BusinessException) {
BusinessException exception = (BusinessException) ex;
log.warn(String.format("訪問 %s -> %s 出現業務異常!", req.getRequestURI(), method.toString()), ex);
returnnew APIResponse(false, null, exception.getCode(), exception.getMessage());
} else {
log.error(String.format("訪問 %s -> %s 出現系統異常!", req.getRequestURI(), method.toString()), ex);
returnnew APIResponse(false, null, GENERIC_SERVER_ERROR_CODE, GENERIC_SERVER_ERROR_MESSAGE);
}
}
}
運行時系統異常發生后,異常處理程序會直接將異常轉換為 JSON 返回給調用方:
圖片
?? 提升建議:可以將相關的請求參數、響應數據、用戶信息在脫敏后記錄到日志中,方便出現問題時根據上下文進一步排查。
誤區二:捕獲異常后"生吞"
在任何時候,我們捕獲異常后都不應該"生吞",也就是直接丟棄異常而不記錄、不拋出。這種處理方式還不如不捕獲異常,因為被生吞的異常一旦導致 Bug,就很難在程序中找到蛛絲馬跡,使得 Bug 排查難上加難。
通常情況下,生吞異常的原因可能是:
- 不希望自己的方法拋出受檢異常,只是為了把異常"處理掉"而捕獲并生吞
- 想當然地認為異常并不重要或不可能產生
但無論什么原因,無論你認為異常多么不重要,都不應該生吞,哪怕記錄一個日志也好。
誤區三:丟棄異常的原始信息
讓我們看兩個不太合適的異常處理方式,雖然沒有完全生吞異常,但也丟失了寶貴的異常信息。
假設有這樣一個會拋出受檢異常的方法:
private void readFile() throws IOException {
Files.readAllLines(Paths.get("a_file"));
}
錯誤處理方式一:完全不記錄原始異常
@GetMapping("wrong1")
public void wrong1(){
try {
readFile();
} catch (IOException e) {
// 原始異常信息丟失
throw new RuntimeException("系統忙請稍后再試");
}
}
這樣處理后,出現問題時我們根本不知道 IOException 具體是哪里引起的。
錯誤處理方式二:只記錄異常消息
catch (IOException e) {
// 只保留了異常消息,棧信息沒有記錄
log.error("文件讀取錯誤, {}", e.getMessage());
throw new RuntimeException("系統忙請稍后再試");
}
留下的日志如下,看完一臉茫然,只知道文件讀取錯誤和文件名,至于為什么讀取錯誤、是不存在還是沒權限,完全不知道:
[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("系統忙請稍后再試");
}
或者將原始異常作為轉換后新異常的 cause,這樣原始異常信息同樣不會丟失:
catch (IOException e) {
throw new RuntimeException("系統忙請稍后再試", e);
}
JDK 內部的類似問題
有趣的是,JDK 內部也會犯類似的錯誤。我曾遇到一個使用 JDK10 的應用偶發啟動失敗的案例,日志中出現這樣的錯誤信息:
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 方法源碼,發現異常 e 既沒有記錄,也沒有作為新拋出異常的 cause。當時讀取文件具體出現什么異常(權限問題還是 IO 問題)可能永遠無法知道,這給問題定位造成了很大困擾。
圖片
誤區四:拋出無消息異常
我見過一些代碼中的偷懶做法,直接拋出沒有 message 的異常:
throw new RuntimeException();
寫這種代碼的同學可能覺得永遠不會走到這個邏輯,永遠不會出現這樣的異常。但當這樣的異常真的出現,被 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) 出現系統異常!
java.lang.RuntimeException: null
...
這里的 null 非常容易引起誤解,讓人以為是空指針問題,實際上是異常的 message 為空。
異常處理的三種模式
總結一下,如果你捕獲了異常打算處理,除了通過日志正確記錄異常原始信息外,通常還有三種處理模式:
- 轉換:轉換為新的異常拋出。新異常最好具有特定的分類和明確的異常消息,而不是隨便拋一個無關或沒有任何信息的異常,并最好通過 cause 關聯原異常
- 重試:重試之前的操作。比如遠程調用服務端過載超時的情況,需要考慮當前情況是否適合重試,盲目重試會讓問題更嚴重
- 恢復:嘗試進行降級處理,或使用默認值替代原始數據
以上就是通過 catch 捕獲處理異常的一些最佳實踐。
finally 中的異常陷阱
有時候,我們希望無論是否遇到異常,邏輯完成后都要釋放資源,這時可以使用 finally 代碼塊。但要格外小心 finally 代碼塊中的異常,因為資源釋放處理等收尾操作同樣可能出現異常。
異常覆蓋問題
看下面這段代碼,我們在 finally 中拋出一個異常:
@GetMapping("wrong")
public void wrong() {
try {
log.info("try");
// 異常丟失
throw new RuntimeException("try");
} finally {
log.info("finally");
throw new RuntimeException("finally");
}
}
最后在日志中只能看到 finally 中的異常,雖然 try 中的邏輯出現了異常,但卻被 finally 中的異常覆蓋了。這非常危險,特別是 finally 中出現的異常是偶發的,就會在部分時候覆蓋 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
異常被覆蓋的原因很簡單:一個方法無法同時出現兩個異常。
解決方案
方案一: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 的優勢
這正是 try-with-resources 語句的做法。對于實現了 AutoCloseable 接口的資源,建議使用 try-with-resources 來釋放資源,否則也可能產生剛才提到的釋放資源時的異常覆蓋主異常的問題。
比如定義一個測試資源,其 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");
}
}
使用傳統 try-finally 語句
@GetMapping("useresourcewrong")
public void useresourcewrong() throws Exception {
TestResource testResource = new TestResource();
try {
testResource.read();
} finally {
testResource.close();
}
}
可以看到,同樣出現了 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
不要將異常定義為靜態變量
既然我們通常會自定義業務異常類型來包含更多異常信息(如異常錯誤碼、友好的錯誤提示等),那就需要在業務邏輯各處手動拋出各種業務異常來返回指定的錯誤碼描述(比如對于下單操作,用戶不存在返回 2001,商品缺貨返回 2002 等)。
對于這些異常的錯誤代碼和消息,我們期望能夠統一管理,而不是散落在程序各處定義。這個想法很好,但稍有不慎就可能掉入將異常定義為靜態變量的陷阱。
問題復現
我在排查某項目生產問題時,遇到了一件非常詭異的事情:異常堆棧信息顯示的方法調用路徑,在當前入參的情況下根本不可能產生。項目的業務邏輯又很復雜,我始終沒往異常信息是錯的這方面想,總覺得是因為某個分支流程導致業務沒有按照期望的流程進行。
經過艱難的排查,最終定位到原因是將異常定義為了靜態變量,導致異常棧信息錯亂。類似于定義一個 Exceptions 類來匯總所有的異常,把異常存放在靜態字段中:
public class Exceptions {
public static BusinessException ORDEREXISTS = new BusinessException("訂單已經存在", 3001);
// ...
}
根本問題:將異常定義為靜態變量會導致異常信息固化,這與異常的棧信息需要根據當前調用動態獲取的特性相矛盾。
問題演示
定義兩個方法 createOrderWrong 和 cancelOrderWrong,它們內部都會通過 Exceptions 類獲得一個訂單已存在的異常,然后先后調用兩個方法并拋出:
@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 的提示對應了 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: 訂單已經存在
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)
正確的解決方案
修復方式很簡單,改一下 Exceptions 類的實現,通過不同的方法將每一種異常都 new 出來拋出即可:
public class Exceptions {
public static BusinessException orderExists(){
return new BusinessException("訂單已經存在", 3001);
}
}
線程池任務異常處理
在介紹線程池時我提到,線程池常用于異步處理或并行處理。那么,將任務提交到線程池處理時,任務本身出現異常會怎樣呢?
execute 方法提交任務的異常處理
我們來看一個例子:提交 10 個任務到線程池異步處理,第 5 個任務拋出 RuntimeException,每個任務完成后都會輸出一行日志:
@GetMapping("execute")
public void execute() throws InterruptedException {
String prefix = "test";
ExecutorService threadPool = Executors.newFixedThreadPool(1,
new ThreadFactoryBuilder().setNameFormat(prefix+"%d").get());
// 提交10個任務到線程池處理,第5個任務會拋出運行時異常
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);
}
觀察日志可以發現兩個現象:
...
[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
...
現象分析:
- 任務 1 到 4 所在的線程是 test0,任務 6 開始運行在線程 test1
- 由于線程池通過線程工廠為線程使用統一的前綴 test 加上計數器進行命名,從線程名的改變可以知道因為異常的拋出,舊線程退出了,線程池只能重新創建一個線程
- 如果每個異步任務都以異常結束,那么線程池可能完全起不到線程重用的作用
因為沒有手動捕獲異常進行處理,ThreadGroup 幫我們進行了未捕獲異常的默認處理,向標準錯誤輸出打印了出現異常的線程名稱和異常信息。顯然,這種沒有統一錯誤日志格式的打印形式對生產級代碼是不合適的。ThreadGroup 的相關源碼如下:
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);
}
}
}
修復方式有兩步:
- 以 execute 方法提交到線程池的異步任務,最好在任務內部做好異常處理
- 設置自定義的異常處理程序作為保底,比如在聲明線程池時自定義線程池的未捕獲異常處理程序:
new ThreadFactoryBuilder()
.setNameFormat(prefix+"%d")
.setUncaughtExceptionHandler((thread, throwable)->
log.error("ThreadPool {} got exception", thread, throwable))
.get()
或者設置全局的默認未捕獲異常處理程序:
static {
Thread.setDefaultUncaughtExceptionHandler((thread, throwable)->
log.error("Thread {} got exception", thread, throwable));
}
submit 方法提交任務的異常處理
通過線程池 ExecutorService 的 execute 方法提交任務到線程池處理,如果出現異常會導致線程退出,控制臺輸出中可以看到異常信息。那么,把 execute 方法改為 submit,線程還會退出嗎?異常還能被處理程序捕獲到嗎?
修改代碼后重新執行程序可以看到如下日志,說明線程沒退出,異常也被生吞了:
[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 源碼可以發現,在執行任務出現異常之后,異常存儲到了一個 outcome 字段中,只有在調用 get 方法獲取 FutureTask 結果的時候,才會以 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 方式提交任務,那么我們應該關心任務的執行結果,否則應該以 execute 來提交任務。修改后的代碼如下,我們把 submit 返回的 Future 放到 List 中,隨后遍歷 List 來捕獲所有任務的異常:
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);
}
});
執行這段程序可以看到如下的日志輸出:
[15:44:13.543] [http-nio-45678-exec-1] [ERROR] [e.d.ThreadPoolAndExceptionController:69 ] - Got exception
java.util.concurrent.ExecutionException: java.lang.RuntimeException: error
總結與最佳實踐
通過今天的文章,我介紹了處理異常容易犯的幾個錯誤和最佳實踐:
核心原則
第一,精細化異常處理策略
- 不應該用 AOP 對所有方法進行統一異常處理
- 異常要么不捕獲不處理,要么根據不同的業務邏輯、不同的異常類型進行精細化、針對性處理
- 處理異常應該杜絕生吞,并確保異常棧信息得到保留
- 如果需要重新拋出異常,請使用具有意義的異常類型和異常消息
第二,小心 finally 代碼塊中的資源處理
- 確保 finally 代碼塊不出現異常,內部把異常處理完畢,避免 finally 中的異常覆蓋 try 中的異常
- 或者考慮使用 addSuppressed 方法把 finally 中的異常附加到 try 中的異常上,確保主異常信息不丟失
- 使用實現了 AutoCloseable 接口的資源,務必使用 try-with-resources 模式,確保資源正確釋放和異常正確處理
第三,正確定義業務異常
- 雖然在統一的地方定義收口所有的業務異常是個不錯的實踐,但務必確保異常是每次 new 出來的
- 不能使用預先定義的 static 字段存放異常,否則可能引起棧信息錯亂
第四,正確處理線程池中任務的異常
- 如果任務通過 execute 提交,出現異常會導致線程退出,大量異常會導致線程重復創建引起性能問題
- 應該盡可能確保任務不出異常,同時設置默認的未捕獲異常處理程序來兜底
- 如果任務通過 submit 提交,意味著我們關心任務的執行結果,應該通過拿到的 Future 調用其 get 方法來獲得任務運行結果和可能出現的異常,否則異常可能就被生吞了
實用建議
- 記錄完整的上下文信息:異常日志中應包含請求參數、用戶信息等上下文,方便問題排查
- 使用合適的日志級別:業務異常使用 Warn 級別,系統異常使用 Error 級別
- 提供友好的錯誤消息:面向用戶的異常信息應該友好且有意義
- 建立異常處理規范:團隊應該建立統一的異常處理規范和最佳實踐
掌握了這些異常處理的最佳實踐,相信你能寫出更加健壯和優雅的代碼。記住,異常處理不是可有可無的"裝飾",而是保證系統穩定性和可維護性的重要基石。