解讀阿里Java開發手冊(v1.1.1) - 異常日志

傳送門

解讀阿里Java開發手冊(v1.1.1) - 編程規約

前言

阿里Java開發手冊談不上圣經,但確實是大量程序員踩坑踩出來的一部非常有價值的寶典。其從代碼規范性、性能、健壯性、安全性等方面出發,對程序員提出了一系列簡單直觀的要求,對于人員流動性強,程序員技術水平參差不齊的團隊來說,尤其具備價值。

阿里Java開發手冊中,有一部分規約是針對阿里自己的工程環境特點設置的,其他團隊可以用于借鑒,無需照搬,而大部分的規約,都是具備推廣價值的。

然而這本手冊中的規約眾多,部分搭配了簡短的說明,相當一部分規約則對原理說明的不夠詳細。本著“知道為什么要這樣做”強于“知道應該這樣做”的思想,本文在列出阿里Java開發手冊的同時,對其中部分語焉不詳的規約進行了比較詳細的說明,并盡可能搭配代碼樣例。

本文覆蓋阿里Java開發手冊中的前兩章,即編程規約和異常日志兩章,后三章MySQL規約、工程規約、安全規約不列入主要有兩個考慮,一是這三章的內容與Java不緊密相關,二是這三章中除MySQL之外的規約與阿里現行的技術架構捆綁的比較緊,普適性較低。

本文中,在阿里Java開發手冊基礎上增加的說明內容全部以引用的形式出現,即

引用部分的文字是本文作者對阿里Java規約的附加說明

二、異常日志

(一) 異常處理

  1. 【強制】Java 類庫中定義的一類RuntimeException可以通過預先檢查進行規避,而不應該通過catch 來處理,比如:IndexOutOfBoundsException,NullPointerException等等。
    說明:無法通過預檢查的異常除外,如在解析一個外部傳來的字符串形式數字時,通過catch NumberFormatException來實現。
    正例:
    if (obj != null) {
        ...
    }

反例:

    try {
        obj.method();
    } catch (NullPointerException e) {
        ...
    }

對于通過入參或全局上下文獲取的對象,在使用之前,必須先判null

  1. 【強制】異常不要用來做流程控制,條件控制,因為異常的處理效率比條件分支低。

使用異常來做流程控制有時用起來很方便,例如進行資格校驗的API,可以通過拋出的異常的message來說明資格校驗不通過的原因。但這樣做會犧牲性能,因為異常對象的產生本身就涉及生成stacktrace等比較耗時的行為,最好避免。

  1. 【強制】對大段代碼進行try-catch,這是不負責任的表現。catch時請分清穩定代碼和非穩定代碼,穩定代碼指的是無論如何不會出錯的代碼。對于非穩定代碼的catch盡可能進行區分異常類型,再做對應的異常處理。

有些工程的頂層代碼中可能存在大段的try-catch,其目的是確保異常不會從業務代碼中逃逸,導致沒有進入最外層兜底的異常處理邏輯。但考慮代碼的簡潔和可維護性,最好還是通過框架級的統一異常處理邏輯來進行(例如spring-mvc、Struts等都有通用的全局異常處理機制)。

  1. 【強制】捕獲異常是為了處理它,不要捕獲了卻什么都不處理而拋棄之,如果不想處理它,請將該異常拋給它的調用者。最外層的業務使用者,必須處理異常,將其轉化為用戶可以理解的內容。

異常處理的原則之一 - 延遲捕獲:
不要在程序有能力處理異常之前捕獲它,將異常交由掌握更多信息的作用域處理
所以說,如果處理不了這個異常,那就干脆不要捕獲它,讓外層的邏輯來處理。當然如果已經是最外層了,那就必須處理

  1. 【強制】有try塊放到了事務代碼中,catch異常后,如果需要回滾事務,一定要注意手動回滾事務。

使用spring的事務管理能力可以做到在產生異常后自動回滾事務

  1. 【強制】finally塊必須對資源對象、流對象進行關閉,有異常也要做try-catch。 說明:如果JDK7及以上,可以使用try-with-resources方式。

try-with-resources非常方便

try (BufferedReader br = new BufferedReader(new FileReader(path))) {
    return br.readLine();
}
等價于
```java
BufferedReader br = new BufferedReader(new FileReader(path));
try {
    return br.readLine();
} finally {
    if (br != null)
        br.close();
}
  1. 【強制】不能在finally塊中使用return,finally塊中的return返回后方法結束執行,不會再執行try塊中的return語句。

方法的退出方式有兩種:return或拋出異常,而finally塊中的代碼是在return或拋出異常之后執行的,所以如果finally塊中有return,會把之前return過的返回值覆蓋掉,如果之前拋出了異常,也會被吞掉

  1. 【強制】捕獲異常與拋異常,必須是完全匹配,或者捕獲異常是拋異常的父類。
    說明:如果預期對方拋的是繡球,實際接到的是鉛球,就會產生意外情況。

  2. 【推薦】方法的返回值可以為null,不強制返回空集合,或者空對象等,必須添加注釋充分說明什么情況下會返回null值。調用方需要進行null判斷防止NPE問題。
    說明:本手冊明確防止NPE是調用者的責任。即使被調用方法返回空集合或者空對象,對調用者來說,也并非高枕無憂,必須考慮到遠程調用失敗、序列化失敗、運行時異常等場景返回null的情況。

防止NPE是調用者的責任,這一點很對。如果API的提供者拍胸脯說“絕對不會返回null”,你就敢不進行null判斷了嗎?

  1. 【推薦】防止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問題。

簡單來說,拿到的對象只要不是你自己的代碼產生的,那么都有可能是null,均需要進行NPE檢查
Optional類既可以用來裝B,又實實在在的有用。如果升級JDK8有困難,google guava庫中也提供了Optional類。
關于Optional類的具體使用,可參考http://www.tuicool.com/articles/uIzeYjf

  1. 【推薦】定義時區分unchecked / checked 異常,避免直接使用RuntimeException拋出,更不允許拋出Exception或者Throwable,應使用有業務含義的自定義異常。推薦業界已定義過的自定義異常,如:DAOException / ServiceException等。

這一條規約分解一下,有幾條:

  • 自定義異常時,想好要定義的異常是unchecked還是checked異常,如果是前者,繼承RuntimeException,如果是后者,繼承Exception
  • 盡量不要在拋出異常時throw new RuntimeException("xxxx"); 應該使用具備業務含義的自定義異常類,這樣做可以在捕獲異常時提供方便
  • 絕對不要在拋出異常時throw new Exception("xxx")或throw new Throwable("xxx"),這樣做不僅僅是屏蔽了異常本身的業務含義,同時也屏蔽了異常的分類(checked/unchecked),甚至連Exception和Error的區別也屏蔽了

如果不清楚Throwable/Exception/Error的關系,或不清楚unchecked/checked異常的含義,建議先閱讀筆者的另一篇文章Java異??刂茩C制和異常處理原則

  1. 【參考】在代碼中使用“拋異常”還是“返回錯誤碼”,對于公司外的http/api開放接口必須使用“錯誤碼”;而應用內部推薦異常拋出;跨應用間RPC調用優先考慮使用Result方式,封裝isSuccess、“錯誤碼”、“錯誤簡短信息”。
    說明:關于RPC方法返回方式使用Result方式的理由:
    1)使用拋異常返回方式,調用方如果沒有捕獲到就會產生運行時錯誤。
    2)如果不加棧信息,只是new自定義異常,加入自己的理解的error message,對于調用端解決問題的幫助不會太多。如果加了棧信息,在頻繁調用出錯的情況下,數據序列化和傳輸的性能損耗也是問題。

  2. 【參考】避免出現重復的代碼(Don’t Repeat Yourself),即DRY原則。
    說明:隨意復制和粘貼代碼,必然會導致代碼的重復,在以后需要修改時,需要修改所有的副本,容易遺漏。必要時抽取共性方法,或者抽象公共類,甚至是共用模塊。
    正例:一個類中有多個public方法,都需要進行數行相同的參數校驗操作,這個時候請抽?。?/p>

private boolean checkParam(DTO dto) {...}

說的很對,但為啥放在異常處理分類下……?

(二) 日志規約

  1. 【強制】應用中不可直接使用日志系統(Log4j、Logback)中的API,而應依賴使用日志框架SLF4J中的API,使用門面模式的日志框架,有利于維護和各個類的日志處理方式統一。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(Abc.class);

slf4j是日志門面框架,其僅提供日志記錄的API,而不實現日志記錄的功能,slf4j需要通過適配庫適配到log4j或logback等日至系統來實現日志的記錄。
使用slf4j api能夠提升代碼和應用的可移植性,在使用不同日志系統的應用之間能夠做到無縫的適配。
同時,使用slf4j api的應用,在切換日志系統時(比如從logback切換到log4j2,不需要代碼改造)

  1. 【強制】日志文件推薦至少保存15天,因為有些異常具備以“周”為頻次發生的特點。

  2. 【強制】應用中的擴展日志(如打點、臨時監控、訪問日志等)命名方式:appName_logType_logName.log。
    logType:日志類型,推薦分類有stats/desc/monitor/visit等;
    logName:日志描述。這種命名的好處:通過文件名就可知道日志文件屬于什么應用,什么類型,什么目的,也有利于歸類查找。
    正例:mppserver應用中單獨監控時區轉換異常,如: mppserver_monitor_timeZoneConvert.log
    說明:推薦對日志進行分類,如將錯誤日志和業務日志分開存放,便于開發人員查看,也便于通過日志對系統進行及時監控。

  3. 【強制】對trace/debug/info級別的日志輸出,必須使用條件輸出形式或者使用占位符的方式。
    說明:logger.debug("Processing trade with id: " + id + " symbol: " + symbol); 如果日志級別是warn,上述日志不會打印,但是會執行字符串拼接操作,如果symbol是對象,會執行toString()方法,浪費了系統資源,執行了上述操作,最終日志卻沒有打印。
    正例:(條件)

    if (logger.isDebugEnabled()) {
        logger.debug("Processing trade with id: " + id + " symbol: " + symbol);
    }

正例:(占位符)

    logger.debug("Processing trade with id: {} symbol : {} ", id, symbol);

占位符方式,log4j2/logback支持,log4j1.x是不直接支持的,只能通過slf4j庫適配

  1. 【強制】避免重復打印日志,浪費磁盤空間,務必在log4j.xml中設置additivity=false。
    正例:
<logger name="com.taobao.dubbo.config" additivity="false">

additivity默認為true,即通過該logger輸出的日志會同時輸出到root logger,如果還為該logger指定了獨立的appender,就會導致這部分日志重復輸出

  1. 【強制】異常信息應該包括兩類信息:案發現場信息和異常堆棧信息。如果不處理,那么通過關鍵字throws往上拋出。
    正例:
    logger.error(各類參數或者對象toString + "_" + e.getMessage(), e);

記錄異常日志的常見錯誤:

logger.error(e);
logger.error(e.getMessage());
logger.error("上下文"+e.getMessage());

上面這幾種都是錯的!請確保使用的是兩個入參的API,如error(String s, Throwable t)

  1. 【推薦】謹慎地記錄日志。生產環境禁止輸出debug日志;有選擇地輸出info日志;如果使用warn來記錄剛上線時的業務行為信息,一定要注意日志輸出量的問題,避免把服務器磁盤撐爆,并記得及時刪除這些觀察日志。
    說明:大量地輸出無效日志,不利于系統性能提升,也不利于快速定位錯誤點。記錄日志時請思考:這些日志真的有人看嗎?看到這條日志你能做什么?能不能給問題排查帶來好處?

不要認為日志記錄不怎么消耗性能,我見過不少事無巨細式的日志把系統性能嚴重拖慢的案例

  1. 【參考】可以使用warn日志級別來記錄用戶輸入參數錯誤的情況,避免用戶投訴時,無所適從。注意日志輸出的級別,error級別只記錄系統邏輯出錯、異常等重要的錯誤信息。如非必要,請不要在此場景打出error級別。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 阿里巴巴 JAVA 開發手冊 1 / 32 Java 開發手冊 版本號 制定團隊 更新日期 備 注 1.0.0 阿...
    糖寶_閱讀 7,679評論 0 5
  • 一、編程規約 (一)命名規約 【強制】 代碼中的命名均不能以下劃線或美元符號開始,也不能以下劃線或美元符號結束。反...
    喝咖啡的螞蟻閱讀 1,540評論 0 2
  • 命名風格 【強制】代碼中的命名均不能以下劃線或美元符號開始,也不能以下劃線或美元符號結束 【強制】代碼中的命名嚴禁...
    云A00000閱讀 3,725評論 0 0
  • 目錄 一、 編程規約..................................................
    owen_he閱讀 4,986評論 0 4
  • 讀小學那會總喜歡去周末去鄉下田野里抓野兔野雞這些野味,然而有一天晚上兩個同村的小伙伴喊我夜里去小樹林里抓野雞的時候...
    老槐樹下酣睡的貓閱讀 734評論 0 0