12, 異常處理

1, 捕獲了異常後直接生吞。在任何時候,我們捕獲了異常都不應該生吞,也就是直接丟棄異常不記錄、不拋出。這樣的處理方式還不如不捕獲異常,因爲被生吞掉的異常一旦導致Bug,就很難在程序中找到蛛絲馬跡,使得Bug排查工作難上加難

2 ,丟棄異常的原始信息。我們來看兩個不太合適的異常處理方式,雖然沒有完全生吞異常,但也丟失了寶貴的異常信息。
比如有這麼一個會拋出受檢異常的方法readFile:

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

像這樣調用readFile方法,捕獲異常後,完全不記錄原始異常,直接拋出一個轉換後異常,導致出了問題不知道IOException具體是哪裏引起的:

@GetMapping("wrong1")
public void wrong1(){
try {
	readFile();
} catch (IOException e) {2020/6/11 12 | 異常處理:別讓自己在出問題的時候變爲瞎子
	//原始異常信息丟失
	throw new RuntimeException("系統忙請稍後再試");
}
}

正確處理方式

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

或把原始異常作爲轉換後新異常的cause,原始異常信息同樣不會丟

catch (IOException e) {
	throw new RuntimeException("系統忙請稍後再試", e);
}
finaly中的異常
@GetMapping("wrong")
public void wrong() {
try {
	log.info("try");
	//異常丟失
	throw new RuntimeException("try");
} finally {
	log.info("finally");
	throw new RuntimeException("finally");
}
}
結果
[dispatcherServlet]:175 ] - Servlet.service() for servlet [dispatcherServlet] in c
java.lang.RuntimeException: finally

最後在日誌中只能看到finally中的異常,雖然try中的邏輯出現了異常,但卻被finally中的異常覆蓋了。

修復方法:
1,finally代碼塊自己負責異常捕獲和處理

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

2,可以把try中的異常作爲主異常拋出,使用addSuppressed方法把finally中的異常附加到主異常上

@GetMapping("right2")
public void right2() throws Exception {
Exception e = null;
try {
	log.info("try");
	throw new RuntimeException("try");
} catch (Exception ex) {
e = ex;
} finally {
	log.info("finally");
	try {
	throw new 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

3, 對於實現了AutoCloseable接口的資源,建議使用try-with-resources來釋放資源,

@GetMapping("useresourceright")
public void useresourceright() throws Exception {
try (TestResource testResource = new TestResource()){
	testResource.read();
}
}
千萬別把異常定義爲靜態變量
把異常定義爲靜態變量會導致異常信息固化,這就和異常的棧一定是需要根據當前調用來動態獲取相矛盾。 ```java 錯誤定義方式 public class Exceptions { public static BusinessException ORDEREXISTS = new BusinessException("訂單已經存在", 3001); ... } ```
@GetMapping("wrong")
public void wrong() {
try {
createOrderWrong();
} catch (Exception ex) {
log.error("createOrder got error", ex);
} t
ry {
cancelOrderWrong();
} catch (Exception ex) {
log.error("cancelOrder got error", ex);
}
} p
rivate void createOrderWrong() {
//這裏有問題
throw Exceptions.ORDEREXISTS;
} p
rivate 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)

修改方式

public class Exceptions {
public static BusinessException orderExists(){
return new BusinessException("訂單已經存在", 3001);
}
}
提交線程池的任務出了異常會怎麼樣
確保正確處理了線程池中任務的異常,如果任務通過execute提交,那麼出現異常會導致線程退出,大量的異常會導致線程重複創建引起性能問題,我們應該儘可能確保任務不出異常,同時設置默認的未捕獲異常處理程序來兜底;

如果任務通過submit提交意味着我們關心任務
的執行結果,應該通過拿到的Future調用其get方法來獲得任務運行結果和可能出現的異常,否則異常可能就被生吞了

new ThreadFactoryBuilder()
.setNameFormat(prefix+"%d")
.setUncaughtExceptionHandler((thread, throwable)-> log.error("ThreadPool {} got exception", thread, throwable))
.get()
@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) throw new 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, 以execute方法提交到線程池的異步任務,最好在任務內部做好異常處理;
2. 設置自定義的異常處理程序作爲保底,比如在聲明線程池時自定義線程池的未捕獲異常處理程序:

修改後

[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

submit提交方式

List<Future> tasks = IntStream.rangeClosed(1, 10).mapToObj(i -> threadPool.submit(() -> {
if (i == 5) throw new RuntimeException("error");2020/6/11 12 | 異常處理:別讓自己在出問題的時候變爲瞎子
https://time.geekbang.org/column/article/220230 8/13
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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章