傳送門
解讀阿里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ī)約的附加說明
二、異常日志
(一) 異常處理
- 【強制】Java 類庫中定義的一類RuntimeException可以通過預先檢查進行規(guī)避,而不應該通過catch 來處理,比如:IndexOutOfBoundsException,NullPointerException等等。
說明:無法通過預檢查的異常除外,如在解析一個外部傳來的字符串形式數(shù)字時,通過catch NumberFormatException來實現(xiàn)。
正例:
if (obj != null) {
...
}
反例:
try {
obj.method();
} catch (NullPointerException e) {
...
}
對于通過入參或全局上下文獲取的對象,在使用之前,必須先判null
- 【強制】異常不要用來做流程控制,條件控制,因為異常的處理效率比條件分支低。
使用異常來做流程控制有時用起來很方便,例如進行資格校驗的API,可以通過拋出的異常的message來說明資格校驗不通過的原因。但這樣做會犧牲性能,因為異常對象的產生本身就涉及生成stacktrace等比較耗時的行為,最好避免。
- 【強制】對大段代碼進行try-catch,這是不負責任的表現(xiàn)。catch時請分清穩(wěn)定代碼和非穩(wěn)定代碼,穩(wěn)定代碼指的是無論如何不會出錯的代碼。對于非穩(wěn)定代碼的catch盡可能進行區(qū)分異常類型,再做對應的異常處理。
有些工程的頂層代碼中可能存在大段的try-catch,其目的是確保異常不會從業(yè)務代碼中逃逸,導致沒有進入最外層兜底的異常處理邏輯。但考慮代碼的簡潔和可維護性,最好還是通過框架級的統(tǒng)一異常處理邏輯來進行(例如spring-mvc、Struts等都有通用的全局異常處理機制)。
- 【強制】捕獲異常是為了處理它,不要捕獲了卻什么都不處理而拋棄之,如果不想處理它,請將該異常拋給它的調用者。最外層的業(yè)務使用者,必須處理異常,將其轉化為用戶可以理解的內容。
異常處理的原則之一 - 延遲捕獲:
不要在程序有能力處理異常之前捕獲它,將異常交由掌握更多信息的作用域處理
所以說,如果處理不了這個異常,那就干脆不要捕獲它,讓外層的邏輯來處理。當然如果已經是最外層了,那就必須處理
- 【強制】有try塊放到了事務代碼中,catch異常后,如果需要回滾事務,一定要注意手動回滾事務。
使用spring的事務管理能力可以做到在產生異常后自動回滾事務
- 【強制】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();
}
- 【強制】不能在finally塊中使用return,finally塊中的return返回后方法結束執(zhí)行,不會再執(zhí)行try塊中的return語句。
方法的退出方式有兩種:return或拋出異常,而finally塊中的代碼是在return或拋出異常之后執(zhí)行的,所以如果finally塊中有return,會把之前return過的返回值覆蓋掉,如果之前拋出了異常,也會被吞掉
【強制】捕獲異常與拋異常,必須是完全匹配,或者捕獲異常是拋異常的父類。
說明:如果預期對方拋的是繡球,實際接到的是鉛球,就會產生意外情況。【推薦】方法的返回值可以為null,不強制返回空集合,或者空對象等,必須添加注釋充分說明什么情況下會返回null值。調用方需要進行null判斷防止NPE問題。
說明:本手冊明確防止NPE是調用者的責任。即使被調用方法返回空集合或者空對象,對調用者來說,也并非高枕無憂,必須考慮到遠程調用失敗、序列化失敗、運行時異常等場景返回null的情況。
防止NPE是調用者的責任,這一點很對。如果API的提供者拍胸脯說“絕對不會返回null”,你就敢不進行null判斷了嗎?
- 【推薦】防止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
- 【推薦】定義時區(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異常控制機制和異常處理原則
【參考】在代碼中使用“拋異常”還是“返回錯誤碼”,對于公司外的http/api開放接口必須使用“錯誤碼”;而應用內部推薦異常拋出;跨應用間RPC調用優(yōu)先考慮使用Result方式,封裝isSuccess、“錯誤碼”、“錯誤簡短信息”。
說明:關于RPC方法返回方式使用Result方式的理由:
1)使用拋異常返回方式,調用方如果沒有捕獲到就會產生運行時錯誤。
2)如果不加棧信息,只是new自定義異常,加入自己的理解的error message,對于調用端解決問題的幫助不會太多。如果加了棧信息,在頻繁調用出錯的情況下,數(shù)據序列化和傳輸?shù)男阅軗p耗也是問題。【參考】避免出現(xiàn)重復的代碼(Don’t Repeat Yourself),即DRY原則。
說明:隨意復制和粘貼代碼,必然會導致代碼的重復,在以后需要修改時,需要修改所有的副本,容易遺漏。必要時抽取共性方法,或者抽象公共類,甚至是共用模塊。
正例:一個類中有多個public方法,都需要進行數(shù)行相同的參數(shù)校驗操作,這個時候請抽取:
private boolean checkParam(DTO dto) {...}
說的很對,但為啥放在異常處理分類下……?
(二) 日志規(guī)約
- 【強制】應用中不可直接使用日志系統(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,不需要代碼改造)
【強制】日志文件推薦至少保存15天,因為有些異常具備以“周”為頻次發(fā)生的特點。
【強制】應用中的擴展日志(如打點、臨時監(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)控。【強制】對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庫適配
- 【強制】避免重復打印日志,浪費磁盤空間,務必在log4j.xml中設置additivity=false。
正例:
<logger name="com.taobao.dubbo.config" additivity="false">
additivity默認為true,即通過該logger輸出的日志會同時輸出到root logger,如果還為該logger指定了獨立的appender,就會導致這部分日志重復輸出
- 【強制】異常信息應該包括兩類信息:案發(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)
- 【推薦】謹慎地記錄日志。生產環(huán)境禁止輸出debug日志;有選擇地輸出info日志;如果使用warn來記錄剛上線時的業(yè)務行為信息,一定要注意日志輸出量的問題,避免把服務器磁盤撐爆,并記得及時刪除這些觀察日志。
說明:大量地輸出無效日志,不利于系統(tǒng)性能提升,也不利于快速定位錯誤點。記錄日志時請思考:這些日志真的有人看嗎?看到這條日志你能做什么?能不能給問題排查帶來好處?
不要認為日志記錄不怎么消耗性能,我見過不少事無巨細式的日志把系統(tǒng)性能嚴重拖慢的案例
- 【參考】可以使用warn日志級別來記錄用戶輸入參數(shù)錯誤的情況,避免用戶投訴時,無所適從。注意日志輸出的級別,error級別只記錄系統(tǒng)邏輯出錯、異常等重要的錯誤信息。如非必要,請不要在此場景打出error級別。