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

傳送門

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

前言

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

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

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

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

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

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

二、異常日志

(一) 異常處理

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

反例:

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

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

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

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

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

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

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

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

  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返回后方法結束執(zhí)行,不會再執(zhí)行try塊中的return語句。

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

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

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

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

  1. 【推薦】防止NPE,是程序員的基本修養(yǎng),注意NPE產生的場景:
    1) 返回類型為基本數(shù)據類型,return包裝數(shù)據類型的對象時,自動拆箱有可能產生NPE。
    反例:public int f() { return Integer對象},如果為null,自動解箱拋NPE。
    2) 數(shù)據庫的查詢結果可能為null。
    3) 集合里的元素即使isNotEmpty,取出的數(shù)據元素也可能為null。
    4) 遠程調用返回對象時,一律要求進行空指針判斷,防止NPE。
    5) 對于Session中獲取的數(shù)據,建議NPE檢查,避免空指針。
    6) 級聯(lián)調用obj.getA().getB().getC();一連串調用,易產生NPE。
    正例:可以使用JDK8的Optional類來防止NPE問題。

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

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

這一條規(guī)約分解一下,有幾條:

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

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

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

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

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

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

(二) 日志規(guī)約

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

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

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

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

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

    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. 【強制】異常信息應該包括兩類信息:案發(fā)現(xiàn)場信息和異常堆棧信息。如果不處理,那么通過關鍵字throws往上拋出。
    正例:
    logger.error(各類參數(shù)或者對象toString + "_" + e.getMessage(), e);

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

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

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

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

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

  1. 【參考】可以使用warn日志級別來記錄用戶輸入參數(shù)錯誤的情況,避免用戶投訴時,無所適從。注意日志輸出的級別,error級別只記錄系統(tǒng)邏輯出錯、異常等重要的錯誤信息。如非必要,請不要在此場景打出error級別。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,746評論 2 370

推薦閱讀更多精彩內容