? ? ? ? ? ? 背景? ? ? ? ? ? ??
最近專門負責團隊的項目質量。我在治理異常日志過程中,總結了一下Java的異常處理。上面是我整理的最近自己比較常見的異常知識地圖。
? 異常知識地圖概述??
從異常知識地圖最左邊的根開始看,地圖從左到右的連線連接的類之間有實實在在的父子關系,在java里通過繼承來實現(除了非RuntimeException是個虛擬父節點)。
☆Java所有異常的父類是Throwable,它又分為Error和Exception。
☆?Error是程序判定如果執行了XX邏輯,則應該是至少JVM層面出現了問題。正常情況下不應該發生的。
☆?Exception意思是環境沒有什么問題,出現Exception請開發人員自己搞定。
☆?Exception分為RuntimeException運行時異常和非運行時異常。
說到這里,我們從另外一個維度給異常分類。Java異常又分為檢查異常和非檢查異常。Error和RuntimeException以及RuntimeException的子類是非檢查異常。其他是檢查異常。這個很好區分。在寫Java代碼的時候,編譯器提示需要try catch或者throws的就是檢查異常。其他是非檢查異常。后面在具體代碼實現里有體現。
☆?異常分為檢查異常和非檢查異常。? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
? ?典型異常發生場景? ?
典型異常發生的場景我做了一些demo,上傳到了github,地址:
https://github.com/xiexiaojing/yuna
為了方面展示使用一個統一的切面來截獲異常:
@RestControllerAdvicepublic class ControllerThrowableAdvice { @ExceptionHandler(Throwable.class) public String handleThrowable(Throwable e) { return "ControllerThrowableAdvice消息:" + e.toString(); }}
ErrorError及其子類一般不是用來捕獲的,而用來拋出的。因為Error的發生意味著環境有問題,該停下來檢修了。所以一般的處理是一旦發生Error,會停止JVM。也就是平時看到的程序起不來。如下java.awt.image.Kernel的源碼。
Error除了手工拋出,在常用的類庫中不用黑科技是不能穩定復現的。所以我測試類是這么寫的
@GetMapping("/errorThrowable")
public String showErrorThrowable(){
Error error = new Error("人工拋出一個Error");
throw error;
}
直接訪問頁面的結果
ControllerThrowableAdvice消息:?
org.springframework.web.util.NestedServletException:Handler dispatch failed; nested exception is java.lang.Error: 人工拋出一個Error
上面錯誤消息意思是spring mvc通過其核心邏輯DispatcherServlet沒有找到任何一個可以處理這個返回model的,因為直接返回就是一個Error。最后顯示的消息通過ControllerThrowableAdvice進行展示。
注意Error是非檢查異常,不用顯示處理。
NPE
NPE也就是平時說的空指針異常,它非常常見,很多類都沒有對null做支持。直到apache提供了common包專門來處理這種情況。防不勝防,時不時項目還是需要為了處理這個異常上線個bugfix。
@GetMapping("/npe")
public String showNullPointerException() {
new HashSet<String>(null);
return prefix + "異常未拋出";
}
直接訪問頁面的結果
ControllerThrowableAdvice消息:java.lang.NullPointerException
這是因為new HashSet的時候傳入null。程序走不到return就拋出異常了。最后顯示的消息通過ControllerThrowableAdvice進行展示。
注意NullPointerException是非檢查異常,不用顯示處理。
算數異常
算數異常非常常見,比如做除0,會拋出異常java.lang.ArithmeticException: / by zero,提示我們該找數學老師幫我們檢查作業了。值得注意的是如果使用BigDecimal.divide來做除法,請直接使用divide(BigDecimal divisor, int scale, RoundingMode roundingMode)這個傳3個參數的,避免divide(BigDecimal divisor)這個傳1個參數的,因為如果傳的值除不盡會拋出java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.?帶三個參數的方法會在除不盡的時候按照傳入的攝入模式和保留小數點后的位數對數據做處理。
注意ArithmeticException是非檢查異常,不用顯示處理。未聲明異常
未聲明異常代碼量稍大,想知道測試源碼的直接去我github里下載。地址:https://github.com/xiexiaojing/yuna拋出異常的原理是使用動態代理時,如果被代理的類拋出了一個異常。但是卻沒有throws聲明。代理類找不到匹配的異常類型會拋出InvocationTargetException。從知識地圖上可以看到它是非檢查異常。最后會被UndeclaredThrowableException來處理。這是java動態代理不優雅的處理方式。建議喜歡看源碼、模仿源碼的朋友對這一點不要借鑒哦。重點來看一下運行拋出異常的打印堆棧java.lang.reflect.UndeclaredThrowableException at com.sun.proxy.$Proxy17.showException(Unknown Source) at com.brmayi.yuna.controller.ExceptionController.showUndeclaredThrowableException(ExceptionController.java:62) at com.brmayi.yuna.controller.IndexControllerTest.showUndeclaredThrowableException(IndexControllerTest.java:34) 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:498) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) at org.mockito.internal.runners.DefaultInternalRunner$1$1.evaluate(DefaultInternalRunner.java:44) at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268) at org.junit.runners.ParentRunner.run(ParentRunner.java:363) at org.mockito.internal.runners.DefaultInternalRunner$1.run(DefaultInternalRunner.java:74) at org.mockito.internal.runners.DefaultInternalRunner.run(DefaultInternalRunner.java:80) at org.mockito.internal.runners.StrictRunner.run(StrictRunner.java:39) at org.mockito.junit.MockitoJUnitRunner.run(MockitoJUnitRunner.java:163) at org.junit.runner.JUnitCore.run(JUnitCore.java:137) at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:119) at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:42) at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:234) at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:74) 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:498) at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)Caused by: java.lang.reflect.InvocationTargetException 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:498) at com.brmayi.yuna.util.ObjProxy.invoke(ObjProxy.java:14) ... 35 moreCaused by: java.lang.ArithmeticException: / by zero at com.brmayi.yuna.service.ShowUndeclaredThrowableExceptionService.showException(ShowUndeclaredThrowableExceptionService.java:5) ... 40 more
從打印的堆棧可以看到這個是最終的ArithmeticException拋出時被InvocationTargetException捕獲。將原來的參數傳給了InvocationTargetException后繼續拋出,最后被UndeclaredThrowableException捕獲。注意UndeclaredThrowableException是非檢查異常,不用顯示處理。不合法參數異常不合法參數異常是很多類或者方法自己定義了java基本規則外的一些規則,不滿足會拋出的異常。比如java動態代理的源碼里就寫了被代理的接口不能超過65535個。否則就拋不合法參數異常。注意IllegalArgumentException是非檢查異常,不用顯示處理。
以上都說的是非檢查異常。下面開始檢查異常。由于IO異常很常見好構造,我們直接來看它的子類。套接字異常套接字異常在通信編程時非常常見。比如如下代碼 @GetMapping("/socket")public String showSocketException() throws Exception { ServerSocket socket = new ServerSocket(8081); socket.close(); socket.setReuseAddress(true); return prefix + "異常未拋出";}
啟動了一個套接口服務端,馬上關閉。關閉后才去調用setReuseAddress。這時候就會拋出java.net.SocketException: Socket is closed。
注意SocketException是檢查異常,需要顯示處理。
綁定異常????套接字異常有一種情況,可以明確的知道是綁定異常,就不用拋出套接字異常這樣模糊的異常了。@GetMapping("/binding")public String showBindingException() throws Exception { ServerSocket socket = new ServerSocket(80); socket.setReuseAddress(true); return prefix + "異常未拋出";}
如上,80端口是http默認端口,不能在自定義通信程序里使用。這時候就會拋出java.net.BindException: Permission denied。
注意BindException是檢查異常,需要顯示處理。
主機名未知異常主機名未知異常在比如內網DNS出現問題、或者遠程調用時由于機器下線等原因找不到主機時出現。可以人為連接一個未啟用的端口來構造。
@GetMapping("/unknownHost")public String showUnknownHostException() throws Exception { new Socket("ttt", 5300); return prefix + "異常未拋出";}
注意UnknownHostException是檢查異常,需要顯示處理。超時異常超時異常因為在分布式系統中涉及程序內部線程間、程序之間的通信多,所以非常常見。具體代碼有點長,詳見
https://github.com/xiexiaojing/yuna
拋出java.util.concurrent.TimeoutException。它是concurrent包里的一個類。
注意TimeoutException是檢查異常,需要顯示處理。
反射操作異常及其子類反射操作異常一般只在啟動時看到,線上程序運行中一般不會發生。因為常見類里它是這么處理的
@GetMapping("/classNotFound")public String showClassNotFoundException() throws Exception { Class.forName("com.XXX"); return prefix + "異常未拋出";}
由于com.XXX不存在。會直接拋出java.lang.ClassNotFoundException: com.XXX。它是反射操作異常的子類。平時反射操作異常及它的子類異常一旦發生就會拋出Error,JVM停止。如下面的源碼:
Spring對于異常的處理
默認異常處理Spring的MVC在默認情況下對不能處理的異常如404、500會拋出白頁。像下面這樣:
mappedHandler = getHandler(processedRequest);// 找到合適的請求處理器if (mappedHandler == null || mappedHandler.getHandler() == null) { // 原則上如果沒有找到則會進入到這里,并且設置response的狀態碼為404 // 但是經過調試并沒有進入到這里 noHandlerFound(processedRequest, response); return;}
它最終處理是返回/error頁。也就是白頁。不知道大家有沒有注意到我前面在介紹Error的時候,定義了error頁面的url地址為errorThrowable。
這是因為error是被Spring自身占用了。如果定義為error,我們將看不到預期的結果,而是下面的白頁
在典型異常的發生場景里一開始就介紹了定義了一個統一錯誤處理如下:
@RestControllerAdvicepublic class ControllerThrowableAdvice { @ExceptionHandler(Throwable.class) public String handleThrowable(Throwable e) { return "ControllerThrowableAdvice消息:" + e.toString(); }}
這是使用了spring aop做了統一攔截。Advice在AOP的概念中翻譯成增強。包括Before、After、Around等增強時機。這里類名用到了Advice意思是在controller發生Throwable時做的增強。看到有的項目喜歡用
@ExceptionHandler(Exception.class)
這個也OK。但是我會假設Everything fails! 程序在發生平時不會遇到的問題時也可控。
??? ? ? ? ? ? 總結??? ? ? ? ? ???
本文先圍繞著異常知識地圖介紹了各種異常及出現場景,最后結合Spring論述了在實際工作中如何統一處理異常。這里推薦一個學習方法:梳理知識地圖,給地圖框架填充內容,讓自己的知識體系化。