- 個人經驗總結
- 一定要想好在發生異常時,最外層應該給用戶或使用者怎樣的信息,內層應該有怎樣的正確邏輯處理,還要保留適當的日志和現場信息等。
- 為什么要做異常處理(在可預期的無法提前避免的異常發生時)
- 保證程序不崩潰
- 保證異常信息不會一直上拋到最上層,暴露給用戶(要做適當的異常處理,或者轉成對用戶友好的信息)
- 有日志或異常堆棧等現場信息可以存下來
- 一些底層的異常(如依賴的library拋出的異常),對於調用者系統來說,不一定就是異常,可以轉換成對用戶友好的信息。
- 比如使用依賴的library調用http接口,如果接口返回404,library可能會當成異常拋出,而調用者不一定把它當成異常。
- 分層異常處理規約
- 對於web的多層架構,應該在哪一層做異常處理?是否繼續上拋?
- 關鍵要看發生異常以后,業務上應該要怎么處理。
-
- (看具體場景,常用)如果發生異常后,上層會有統一的異常處理(也可能是通過切面的方式來做統一的異常處理),那么本層就可以不做異常處理,或者catch住后再重新拋出一個該層的自定義Exception,方便上層做專門的catch和處理,比如DAOException。然后上層依次處理各種類型的異常。
-
- (看具體場景,偶爾用)如果發生異常后要返回給上層false,那么當然就要catch以后return一個false,讓上層知道操作失敗了。但是這樣有弊端,一個是如果DAO方法的返回類型不是bool類型的話,通過其他方式傳出比較麻煩;一個是這樣的話上層service層雖然不需要做異常處理了(但有可能因為其他原因,這個異常處理的代碼還是免不了的),但是每個DAO操作都要做返回值判斷。
-
- (不太常用)如果無論怎樣都上層都要繼續執行,下層異常也業務上沒關系,那么就可以在下層catch住記個日志就行了,不需要往上拋了。
-
- DAO層。
- 產生的異常類型有很多,無法用細粒度的異常進行catch,使用catch(Exception e)方式,並throw new DAOException(e),方便上層對該DAOException類型做專門的catch和處理,不需要打印日志,因為日志在Manager/Service層一定需要捕獲並打印到日志文件中去,如果同台服務器再打日志,浪費性能和存儲。
- 當然還有一種方式,是catch(Exception e)后返回false,讓上層service層知道操作失敗了。但是這樣有弊端,一個是如果DAO方法的返回類型不是bool類型的話,通過其他方式傳出比較麻煩;一個是這樣的話上層service層雖然不需要做異常處理了(但有可能因為其他原因,這個異常處理的代碼還是免不了的),但是每個DAO操作都要做返回值判斷。
- Service層。
- 在Service層出現異常時,必須記錄出錯日志到磁盤,盡可能帶上參數信息,相當於保護案發現場。如果Manager層與Service同機部署,日志方式與DAO層處理一致,如果是單獨部署,則采用與Service一致的處理方式。
- 注意結合其他原則,比如通過預檢查方式規避的RuntimeException異常、要分清穩定代碼和非穩定代碼等。
- Web層或開放接口層。
- 絕不應該繼續往上拋異常,因為已經處於頂層,如果意識到這個異常將導致頁面無法正常渲染,那么就應該直接跳轉到友好錯誤頁面,加上用戶容易理解的錯誤提示信息。
- 要將異常處理成錯誤碼和錯誤信息方式返回。
- 可以有通用的異常處理filter,轉換各種異常到合適的錯誤信息
- 前后台交互中。
- 可以考慮后台返回信息中包含錯誤碼、錯誤信息,但不要包含錯誤堆棧(因為前端在瀏覽器中會暴露給客戶),然后前台可以直接顯示錯誤信息,也可以自己再根據錯誤碼展示相應信息,尤其在做了國際化時,得看國際化是前端還是后端做的,如果是前端做的並且錯誤信息要國際化,那只能前端再轉一次找到對應的錯誤信息了。
- 自定義異常類型
- 可以使用更有用的異常信息。
- 方便上層進行異常處理時做分類、針對性的處理。
- 一般最后再用或者不要用通用的Exception類型,這樣可以在處理不同的異常類型時做不同的處理,或者記錄不同的日志,或者返回不同的信息。
- 不一定在catch中重新throw時使用,也可以在適當的業務場景下直接throw。
- catch的異常類型
- 在全局異常處理類中,或者上層的異常處理邏輯中,先處理自定義異常,再處理特殊需要的異常,最后所有的異常全部歸在Exception中,統一告訴前端用戶操作失敗請聯系管理員就好啦。
- 不過很多時候不會去catch通用的Exception,都是catch自定義異常、依賴包指明會拋出的具體異常類型等。
- 像NullPointerException,IndexOutOfBoundsException等可以通過預檢查方式規避的RuntimeException異常,就不要再做異常處理了,提前做一下判斷比較好。
- 不要用異常處理做流程控制、條件控制。
- 捕獲異常是為了處理它,不要捕獲了卻什么都不處理而拋棄之。應該采取怎樣的處理方式?記一下日志完事?記完再拋給上一層?
- 不要不分青紅皂白就把所有的代碼catch住,然后匹配一個通用的Exception,再處理下就完事。要分清穩定代碼和非穩定代碼,只處理非穩定代碼的異常。
- 有try塊放到了事務代碼中(比如Spring的@Transaction),catch異常后,如果需要回滾事務,一定要注意手動回滾事務
- finally塊必須對資源對象、流對象進行關閉,有異常也要做try-catch。
- 不要在finally塊中使用return
- 設計思路
- 強制
- Java 類庫中定義的可以通過預檢查方式規避的RuntimeException異常不應該通過catch 的方式來處理,比如:NullPointerException,IndexOutOfBoundsException等等。
- 無法通過預檢查的異常除外,比如,在解析字符串形式的數字時,可能存在數字格式錯誤,不得不通過catch NumberFormatException來實現。
- 正例:if (obj != null) {...}
- 反例:try { obj.method(); } catch (NullPointerException e) {…}
- 經驗:也就是說,這些能夠提前通過代碼判斷出來的問題,就提前判斷算了,不要再讓異常拋出來增加復雜度了。只有不容易判斷,運行時才出現的問題,才catch,比如接口調用失敗、HttpClientErrorException、InterruptedException、JwtException、NoSuchAlgorithmException、InvalidKeySpecException、IOException、業務自定義異常等。
- 異常不要用來做流程控制,條件控制。
- 異常設計的初衷是解決程序運行中的各種意外情況,且異常的處理效率比條件判斷方式要低很多。
- 經驗:異常和流程控制、條件控制根本就是兩回事,目的不同。
- 捕獲異常是為了處理它,不要捕獲了卻什么都不處理而拋棄之,如果不想處理它,請將該異常拋給它的調用者。
- 經驗:也就是說不要平白無故的吃掉異常,至少要做個日志記錄,或者直接拋給上層,當然這要看業務上怎么考慮的,怎么做比較合適,和上層是怎么配合的。
- 最外層的業務使用者,必須處理異常,將其轉化為用戶可以理解的內容。
- 經驗:比如在controller層,就可以用一個@ControllerAdvice來定義一個全局的異常處理類,能指定哪些異常轉換成哪種響應碼,來返回有效的異常信息和響應碼。
- 捕獲異常與拋異常,必須是完全匹配,或者捕獲異常是拋異常的父類。
- 如果預期對方拋的是綉球,實際接到的是鉛球,就會產生意外情況。
- catch時請分清穩定代碼和非穩定代碼,穩定代碼指的是無論如何不會出錯的代碼。對於非穩定代碼的catch盡可能進行區分異常類型,再做對應的異常處理。
- 對大段代碼進行try-catch,使程序無法根據不同的異常做出正確的應激反應,也不利於定位問題,這是一種不負責任的表現。
- 經驗:這是因為有的代碼出現了異常,這是正常的要交給上層處理的,不能一股腦全括起來catch住,那這種正常的異常就拋不出來。
- 正例:用戶注冊的場景中,如果用戶輸入非法字符,或用戶名稱已存在,或用戶輸入密碼過於簡單,在程序上作出分門別類的判斷,並提示給用戶。
- 經驗:也就是說要自己判斷哪些代碼會拋異常,哪些不會拋,不要用一個大try把所有代碼都catch住,這樣不利於出異常時定位問題,只是為了圖省事。
- 經驗:如前面提到的做法,最外層的業務使用者,必須處理異常,這時倒是可以考慮把最外層全做異常處理,但也不一定要用try全包住,可以用前面說的@ControllerAdvice來進行切面處理各種異常,當然這種做法事實上好像和全用try包住也差不多。。。
- 對大段代碼進行try-catch,使程序無法根據不同的異常做出正確的應激反應,也不利於定位問題,這是一種不負責任的表現。
- Java 類庫中定義的可以通過預檢查方式規避的RuntimeException異常不應該通過catch 的方式來處理,比如:NullPointerException,IndexOutOfBoundsException等等。
- 參考
- 對於公司外的http/api開放接口必須使用“錯誤碼”;而應用內部推薦異常拋出;跨應用間RPC調用優先考慮使用Result方式,封裝isSuccess()方法、“錯誤碼”、“錯誤簡短信息”。
- 關於RPC方法返回方式使用Result方式的理由:
- 1)使用拋異常返回方式,調用方如果沒有捕獲到就會產生運行時錯誤。
- 2)如果不加棧信息,只是new自定義異常,加入自己的理解的error message,對於調用端解決問題的幫助不會太多。如果加了棧信息,在頻繁調用出錯的情況下,數據序列化和傳輸的性能損耗也是問題。
- 關於RPC方法返回方式使用Result方式的理由:
- 避免出現重復的代碼(Don't Repeat Yourself),即DRY原則。
- 隨意復制和粘貼代碼,必然會導致代碼的重復,在以后需要修改時,需要修改所有的副本,容易遺漏。必要時抽取共性方法,或者抽象公共類,甚至是組件化。
- 正例:一個類中有多個public方法,都需要進行數行相同的參數校驗操作,這個時候請抽取:private boolean checkParam(DTO dto) {...}
- 隨意復制和粘貼代碼,必然會導致代碼的重復,在以后需要修改時,需要修改所有的副本,容易遺漏。必要時抽取共性方法,或者抽象公共類,甚至是組件化。
- 對於公司外的http/api開放接口必須使用“錯誤碼”;而應用內部推薦異常拋出;跨應用間RPC調用優先考慮使用Result方式,封裝isSuccess()方法、“錯誤碼”、“錯誤簡短信息”。
- 強制
- 具體情況
- 強制
- 有try塊放到了事務代碼中(比如Spring的@Transaction),catch異常后,如果需要回滾事務,一定要注意手動回滾事務。
- finally塊必須對資源對象、流對象進行關閉,有異常也要做try-catch。
- 如果JDK7及以上,可以使用try-with-resources方式。
- 不要在finally塊中使用return。
- try塊中的return語句執行成功后,並不馬上返回,而是繼續執行finally塊中的語句,如果此處存在return語句,則在此直接返回,無情丟棄掉try塊中的返回點。
- 反例:finally中在return前又修改了返回值,那么調用方收到的就是修改后的返回值了。
- 在調用RPC、二方包、或動態生成類的相關方法時,捕捉異常必須使用Throwable類來進行攔截。
- 通過反射機制來調用方法,如果找不到方法,拋出NoSuchMethodException。什么情況會拋出NoSuchMethodError呢?二方包在類沖突時,仲裁機制可能導致引入非預期的版本使類的方法簽名不匹配,或者在字節碼修改框架(比如:ASM)動態創建或修改類時,修改了相應的方法簽名。這些情況,即使代碼編譯期是正確的,但在代碼運行期時,會拋出NoSuchMethodError。
- 推薦
- 方法的返回值可以為null,不強制返回空集合,或者空對象等,必須添加注釋充分說明什么情況下會返回null值。
- 本手冊明確防止NPE是調用者的責任。即使被調用方法返回空集合或者空對象,對調用者來說,也並非高枕無憂,必須考慮到遠程調用失敗、序列化失敗、運行時異常等場景返回null的情況。
- 經驗:不要只要求下層被調用者返回的結果怎樣怎樣,自己一定還是要做NPE(NullPointerException)等檢查。
- 防止NPE,是程序員的基本修養,注意NPE產生的場景:
- 1) 返回類型為基本數據類型,return包裝數據類型的對象時,自動拆箱有可能產生NPE。
- 反例:public int f() { return Integer對象}, 如果為null,自動解箱拋NPE。
- 2) 數據庫的查詢結果可能為null。
- 3) 集合里的元素即使isNotEmpty,取出的數據元素也可能為null。
- 4) 遠程調用返回對象時,一律要求進行空指針判斷,防止NPE。
- 5) 對於Session中獲取的數據,建議進行NPE檢查,避免空指針。
- 6) 級聯調用obj.getA().getB().getC();一連串調用,易產生NPE。
- 正例:使用JDK8的Optional類來防止NPE問題。
- 1) 返回類型為基本數據類型,return包裝數據類型的對象時,自動拆箱有可能產生NPE。
- 定義時區分unchecked / checked 異常,避免直接拋出new RuntimeException(),更不允許拋出Exception或者Throwable,應使用有業務含義的自定義異常。推薦業界已定義過的自定義異常,如:DAOException / ServiceException等。
- 方法的返回值可以為null,不強制返回空集合,或者空對象等,必須添加注釋充分說明什么情況下會返回null值。
- 強制