Java 編碼規範9(異常日誌)
一. 異常處理
[強制] Java 類庫中定義的可以通過預檢查方式規避的
RuntimeException
異常不應該通過catch 的方式來處理,比如:NullPointerException
,IndexOutOfBoundsException
等等。- 說明:無法通過預檢查的異常除外,比如,在解析字串形式的數字時,不得不通過catch NumberFormatException來實現。
- 正例:
if (obj != null) {...}
- 反例:
try { obj.method() } catch (NullPointerException e) {…}
[強制] 異常不要用來做流程控制,條件控制。
- 異常設計的初衷是解決程式執行中的各種意外情況,且異常的處理效率比條件判斷方式要低很多。
[強制] catch時請分清穩定程式碼和非穩定程式碼.
- 穩定程式碼: 是無論如何不會出錯的程式碼。
- 對於非穩定程式碼的catch儘可能進行區分異常型別,再做對應的異常處理。
[敲黑板] 對大段程式碼進行
try-catch
,使程式無法根據不同的異常做出正確的應激反應,也不利於定位問題,這是一種不負責任的表現。示例: 使用者註冊的場景中,如果使用者輸入非法字元,或使用者名稱稱已存在,或使用者輸入密碼過於簡單,在程式上作出分門別類的判斷,並提示給使用者。
[強制] 捕獲異常是為了處理它,不要捕獲了卻什麼都不處理而拋棄之,如果不想處理它,請將該異常拋給它的呼叫者。最外層的業務使用者,必須處理異常,將其轉化為使用者可以理解的內容。
[強制] 有try塊放到了事務程式碼中,catch異常後,如果需要回滾事務,一定要注意手動回滾事務。
[強制] finally塊必須對資源物件、流物件進行關閉,有異常也要做try-catch。
- 如果JDK7及以上,可以使用try-with-resources方式。
[強制] 不要在
finally
塊中使用return
。- finally塊中的 return返回後方法結束執行 ,不會再返回後方法結束執行 ,不會再try塊中的 return語句。
[強制] 捕獲異常與拋異常,必須是完全匹配,或者捕獲異常是拋異常的父類。
- 說明:如果預期對方拋的是繡球,實際接到的是鉛球,就會產生意外情況。
[強制]
- 本手冊明確防止NPE是呼叫者的責任。即使被呼叫方法返回空集合或者空物件,對呼叫者來說,也並非高枕無憂,必須考慮到遠端呼叫失敗、序列化失敗、執行時異常等場景返回null的情況。
[推薦] 防止NPE,是程式設計師的基本修養。
注意NPE產生的場景:
- 返回型別為基本資料型別,return包裝資料型別的物件時,自動拆箱有可能產生NPE。
- 反例:public int f() { return Integer物件}, 如果為null,自動解箱拋NPE。
- 資料庫的查詢結果可能為null。
- 集合裡的元素即使isNotEmpty,取出的資料元素也可能為null。
- 遠端呼叫返回物件時,一律要求進行空指標判斷,防止NPE。
- 對於Session中獲取的資料,建議NPE檢查,避免空指標。
- 級聯呼叫obj.getA().getB().getC();一連串呼叫,易產生NPE。
- 返回型別為基本資料型別,return包裝資料型別的物件時,自動拆箱有可能產生NPE。
正例: 使用JDK8的Optional類來防止NPE問題。
[推薦] 定義時區分
unchecked / checked
異常,避免直接丟擲new RuntimeException()
,更不允許丟擲Exception
或者Throwable
,應使用有業務含義的自定義異常。- 推薦業界已定義過的自定義異常,如:
DAOException
/ServiceException
等。
- 推薦業界已定義過的自定義異常,如:
[推薦] 對於公司外的http/api開放介面必須使用“錯誤碼” 。
- 應用內部推薦異常丟擲;
- 跨應用間RPC呼叫優先考慮使用Result方式,封裝
isSuccess()
方法、“錯誤碼”、“錯誤簡簡訊息”。
- 關於RPC方法返回方式使用Result方式的理由:
- 使用拋異常返回方式,呼叫方如果沒有捕獲到就會產生執行時錯誤。
- 如果不加棧資訊,只是new自定義異常,加入自己的理解的error message,對於呼叫端解決問題的幫助不會太多。如果加了棧資訊,在頻繁調用出錯的情況下,資料序列化和傳輸的效能損耗也是問題。
- 關於RPC方法返回方式使用Result方式的理由:
[推薦] 避免出現重複的程式碼(Don’t Repeat Yourself),即DRY原則。
- 說明: 隨意複製和貼上程式碼,必然會導致程式碼的重複,在以後需要修改時,需要修改所有的副本,容易遺漏。必要時抽取共性方法,或者抽象公共類,甚至是元件化。
正例: 一個類中有多個public方法,都需要進行數行相同的引數校驗操作,這個時候請抽取:
private boolean checkParam(DTO dto) { ... }
二. 日誌規約
[強制] 應用中不可直接使用日誌系統(Log4j、Logback)中的API,而應依賴使用日誌框架SLF4J中的API,使用門面模式的日誌框架,有利於維護和各個類的日誌處理方式統一。
``` import org.slf4j.Logger; import org.slf4j.LoggerFactory; private static final Logger logger = LoggerFactory.getLogger(Abc.class); ```
[強制] 日誌檔案推薦至少儲存15天,因為有些異常具備以“周”為頻次發生的特點。
[強制] 應用中的擴充套件日誌(如打點、臨時監控、訪問日誌等)命名方式:
appName_logType_logName.log
logType
: 日誌型別,推薦分類有stats/monitor/visit等;logName
: 日誌描述。- 優點:通過檔名就可知道日誌檔案屬於什麼應用,什麼型別,什麼目的,也有利於歸類查詢。
- 正例: :mppserver應用中單獨監控時區轉換異常,如:
mppserver_monitor_timeZoneConvert.log
- 說明: 推薦對日誌進行分類,如將錯誤日誌和業務日誌分開存放,便於開發人員檢視,也便於通過日誌對系統進行及時監控。
[強制] 對trace/debug/info級別的日誌輸出,必須使用條件輸出形式或者使用佔位符的方式。
- 說明:
logger.debug("Processing trade with id: " + id + " and symbol: " + symbol);
- 如果日誌級別是warn,上述日誌不會列印,但是會執行字串拼接操作,如果symbol是物件,會執行toString()方法,浪費了系統資源,執行了上述操作,最終日誌卻沒有列印。
正例:
if (logger.isDebugEnabled()) { logger.debug("Processing trade with id: " + id + " and symbol: " + symbol); }
反例:
logger.debug("Processing trade with id: {} and symbol : {} ", id, symbol);
- 說明:
[強制] 避免重複列印日誌,浪費磁碟空間,務必在log4j.xml中設定
additivity=false
。- 正例:
[強制] 異常資訊應該包括兩類資訊:案發現場資訊和異常堆疊資訊。如果不處理,那麼通過關鍵字throws往上丟擲。
- 正例
:logger.error(各類引數或者物件toString + "_" + e.getMessage(), e);
- 正例
[推薦] 謹慎地記錄日誌。
- 生產環境禁止輸出debug日誌;有選擇地輸出info日誌;
- 如果使用warn來記錄剛上線時的業務行為資訊,一定要注意日誌輸出量的問題,避免把伺服器磁碟撐爆,並記得及時刪除這些觀察日誌。
[推薦] 可以使用warn日誌級別來記錄使用者輸入引數錯誤的情況,避免投訴時無所適從。如非必要,請不在此場景打出error級別,避免頻繁報警。
- 注意日誌輸出的級別, error級別只記錄系統邏輯出錯、異常級別只記錄系統邏輯出錯、異常。