傳送門
解讀阿里Java開發(fā)手冊(cè)(v1.1.1) - 編程規(guī)約
前言
阿里Java開發(fā)手冊(cè)談不上圣經(jīng),但確實(shí)是大量程序員踩坑踩出來(lái)的一部非常有價(jià)值的寶典搓蚪。其從代碼規(guī)范性霎箍、性能六敬、健壯性锌蓄、安全性等方面出發(fā),對(duì)程序員提出了一系列簡(jiǎn)單直觀的要求涯雅,對(duì)于人員流動(dòng)性強(qiáng)鲜结,程序員技術(shù)水平參差不齊的團(tuán)隊(duì)來(lái)說,尤其具備價(jià)值活逆。
阿里Java開發(fā)手冊(cè)中精刷,有一部分規(guī)約是針對(duì)阿里自己的工程環(huán)境特點(diǎn)設(shè)置的,其他團(tuán)隊(duì)可以用于借鑒划乖,無(wú)需照搬贬养,而大部分的規(guī)約挤土,都是具備推廣價(jià)值的琴庵。
然而這本手冊(cè)中的規(guī)約眾多,部分搭配了簡(jiǎn)短的說明仰美,相當(dāng)一部分規(guī)約則對(duì)原理說明的不夠詳細(xì)迷殿。本著“知道為什么要這樣做”強(qiáng)于“知道應(yīng)該這樣做”的思想,本文在列出阿里Java開發(fā)手冊(cè)的同時(shí)咖杂,對(duì)其中部分語(yǔ)焉不詳?shù)囊?guī)約進(jìn)行了比較詳細(xì)的說明庆寺,并盡可能搭配代碼樣例。
本文覆蓋阿里Java開發(fā)手冊(cè)中的前兩章诉字,即編程規(guī)約和異常日志兩章懦尝,后三章MySQL規(guī)約、工程規(guī)約壤圃、安全規(guī)約不列入主要有兩個(gè)考慮陵霉,一是這三章的內(nèi)容與Java不緊密相關(guān),二是這三章中除MySQL之外的規(guī)約與阿里現(xiàn)行的技術(shù)架構(gòu)捆綁的比較緊伍绳,普適性較低踊挠。
本文中,在阿里Java開發(fā)手冊(cè)基礎(chǔ)上增加的說明內(nèi)容全部以引用的形式出現(xiàn)冲杀,即
引用部分的文字是本文作者對(duì)阿里Java規(guī)約的附加說明
二效床、異常日志
(一) 異常處理
- 【強(qiáng)制】Java 類庫(kù)中定義的一類RuntimeException可以通過預(yù)先檢查進(jìn)行規(guī)避,而不應(yīng)該通過catch 來(lái)處理权谁,比如:IndexOutOfBoundsException剩檀,NullPointerException等等。
說明:無(wú)法通過預(yù)檢查的異常除外旺芽,如在解析一個(gè)外部傳來(lái)的字符串形式數(shù)字時(shí)沪猴,通過catch NumberFormatException來(lái)實(shí)現(xiàn)卤妒。
正例:
if (obj != null) {
...
}
反例:
try {
obj.method();
} catch (NullPointerException e) {
...
}
對(duì)于通過入?yún)⒒蛉稚舷挛墨@取的對(duì)象,在使用之前字币,必須先判null
- 【強(qiáng)制】異常不要用來(lái)做流程控制则披,條件控制,因?yàn)楫惓5奶幚硇时葪l件分支低洗出。
使用異常來(lái)做流程控制有時(shí)用起來(lái)很方便士复,例如進(jìn)行資格校驗(yàn)的API,可以通過拋出的異常的message來(lái)說明資格校驗(yàn)不通過的原因翩活。但這樣做會(huì)犧牲性能阱洪,因?yàn)楫惓?duì)象的產(chǎn)生本身就涉及生成stacktrace等比較耗時(shí)的行為,最好避免菠镇。
- 【強(qiáng)制】對(duì)大段代碼進(jìn)行try-catch冗荸,這是不負(fù)責(zé)任的表現(xiàn)。catch時(shí)請(qǐng)分清穩(wěn)定代碼和非穩(wěn)定代碼利耍,穩(wěn)定代碼指的是無(wú)論如何不會(huì)出錯(cuò)的代碼蚌本。對(duì)于非穩(wěn)定代碼的catch盡可能進(jìn)行區(qū)分異常類型,再做對(duì)應(yīng)的異常處理隘梨。
有些工程的頂層代碼中可能存在大段的try-catch程癌,其目的是確保異常不會(huì)從業(yè)務(wù)代碼中逃逸,導(dǎo)致沒有進(jìn)入最外層兜底的異常處理邏輯轴猎。但考慮代碼的簡(jiǎn)潔和可維護(hù)性嵌莉,最好還是通過框架級(jí)的統(tǒng)一異常處理邏輯來(lái)進(jìn)行(例如spring-mvc、Struts等都有通用的全局異常處理機(jī)制)捻脖。
- 【強(qiáng)制】捕獲異常是為了處理它锐峭,不要捕獲了卻什么都不處理而拋棄之,如果不想處理它可婶,請(qǐng)將該異常拋給它的調(diào)用者沿癞。最外層的業(yè)務(wù)使用者,必須處理異常扰肌,將其轉(zhuǎn)化為用戶可以理解的內(nèi)容抛寝。
異常處理的原則之一 - 延遲捕獲:
不要在程序有能力處理異常之前捕獲它,將異常交由掌握更多信息的作用域處理
所以說曙旭,如果處理不了這個(gè)異常盗舰,那就干脆不要捕獲它,讓外層的邏輯來(lái)處理桂躏。當(dāng)然如果已經(jīng)是最外層了钻趋,那就必須處理
- 【強(qiáng)制】有try塊放到了事務(wù)代碼中,catch異常后剂习,如果需要回滾事務(wù)蛮位,一定要注意手動(dòng)回滾事務(wù)较沪。
使用spring的事務(wù)管理能力可以做到在產(chǎn)生異常后自動(dòng)回滾事務(wù)
- 【強(qiáng)制】finally塊必須對(duì)資源對(duì)象、流對(duì)象進(jìn)行關(guān)閉失仁,有異常也要做try-catch尸曼。 說明:如果JDK7及以上,可以使用try-with-resources方式萄焦。
try-with-resources非常方便
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
等價(jià)于 ```java
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
if (br != null)
br.close();
}
- 【強(qiáng)制】不能在finally塊中使用return控轿,finally塊中的return返回后方法結(jié)束執(zhí)行,不會(huì)再執(zhí)行try塊中的return語(yǔ)句拂封。
方法的退出方式有兩種:return或拋出異常茬射,而finally塊中的代碼是在return或拋出異常之后執(zhí)行的,所以如果finally塊中有return冒签,會(huì)把之前return過的返回值覆蓋掉在抛,如果之前拋出了異常,也會(huì)被吞掉
【強(qiáng)制】捕獲異常與拋異常萧恕,必須是完全匹配刚梭,或者捕獲異常是拋異常的父類。
說明:如果預(yù)期對(duì)方拋的是繡球廊鸥,實(shí)際接到的是鉛球望浩,就會(huì)產(chǎn)生意外情況。【推薦】方法的返回值可以為null惰说,不強(qiáng)制返回空集合,或者空對(duì)象等缘回,必須添加注釋充分說明什么情況下會(huì)返回null值吆视。調(diào)用方需要進(jìn)行null判斷防止NPE問題。
說明:本手冊(cè)明確防止NPE是調(diào)用者的責(zé)任酥宴。即使被調(diào)用方法返回空集合或者空對(duì)象啦吧,對(duì)調(diào)用者來(lái)說,也并非高枕無(wú)憂拙寡,必須考慮到遠(yuǎn)程調(diào)用失敗授滓、序列化失敗、運(yùn)行時(shí)異常等場(chǎng)景返回null的情況肆糕。
防止NPE是調(diào)用者的責(zé)任般堆,這一點(diǎn)很對(duì)。如果API的提供者拍胸脯說“絕對(duì)不會(huì)返回null”诚啃,你就敢不進(jìn)行null判斷了嗎淮摔?
- 【推薦】防止NPE,是程序員的基本修養(yǎng)始赎,注意NPE產(chǎn)生的場(chǎng)景:
1) 返回類型為基本數(shù)據(jù)類型和橙,return包裝數(shù)據(jù)類型的對(duì)象時(shí)仔燕,自動(dòng)拆箱有可能產(chǎn)生NPE。
反例:public int f() { return Integer對(duì)象}魔招,如果為null晰搀,自動(dòng)解箱拋NPE。
2) 數(shù)據(jù)庫(kù)的查詢結(jié)果可能為null办斑。
3) 集合里的元素即使isNotEmpty厕隧,取出的數(shù)據(jù)元素也可能為null。
4) 遠(yuǎn)程調(diào)用返回對(duì)象時(shí)俄周,一律要求進(jìn)行空指針判斷吁讨,防止NPE。
5) 對(duì)于Session中獲取的數(shù)據(jù)峦朗,建議NPE檢查建丧,避免空指針。
6) 級(jí)聯(lián)調(diào)用obj.getA().getB().getC()波势;一連串調(diào)用翎朱,易產(chǎn)生NPE。
正例:可以使用JDK8的Optional類來(lái)防止NPE問題尺铣。
簡(jiǎn)單來(lái)說拴曲,拿到的對(duì)象只要不是你自己的代碼產(chǎn)生的,那么都有可能是null凛忿,均需要進(jìn)行NPE檢查
Optional類既可以用來(lái)裝B澈灼,又實(shí)實(shí)在在的有用。如果升級(jí)JDK8有困難店溢,google guava庫(kù)中也提供了Optional類叁熔。
關(guān)于Optional類的具體使用,可參考http://www.tuicool.com/articles/uIzeYjf
- 【推薦】定義時(shí)區(qū)分unchecked / checked 異常床牧,避免直接使用RuntimeException拋出荣回,更不允許拋出Exception或者Throwable,應(yīng)使用有業(yè)務(wù)含義的自定義異常戈咳。推薦業(yè)界已定義過的自定義異常心软,如:DAOException / ServiceException等。
這一條規(guī)約分解一下著蛙,有幾條:
- 自定義異常時(shí)删铃,想好要定義的異常是unchecked還是checked異常,如果是前者册踩,繼承RuntimeException泳姐,如果是后者,繼承Exception
- 盡量不要在拋出異常時(shí)throw new RuntimeException("xxxx"); 應(yīng)該使用具備業(yè)務(wù)含義的自定義異常類暂吉,這樣做可以在捕獲異常時(shí)提供方便
- 絕對(duì)不要在拋出異常時(shí)throw new Exception("xxx")或throw new Throwable("xxx")胖秒,這樣做不僅僅是屏蔽了異常本身的業(yè)務(wù)含義缎患,同時(shí)也屏蔽了異常的分類(checked/unchecked),甚至連Exception和Error的區(qū)別也屏蔽了
如果不清楚Throwable/Exception/Error的關(guān)系阎肝,或不清楚unchecked/checked異常的含義挤渔,建議先閱讀筆者的另一篇文章Java異常控制機(jī)制和異常處理原則
【參考】在代碼中使用“拋異撤缣猓”還是“返回錯(cuò)誤碼”判导,對(duì)于公司外的http/api開放接口必須使用“錯(cuò)誤碼”;而應(yīng)用內(nèi)部推薦異常拋出沛硅;跨應(yīng)用間RPC調(diào)用優(yōu)先考慮使用Result方式眼刃,封裝isSuccess、“錯(cuò)誤碼”摇肌、“錯(cuò)誤簡(jiǎn)短信息”擂红。
說明:關(guān)于RPC方法返回方式使用Result方式的理由:
1)使用拋異常返回方式,調(diào)用方如果沒有捕獲到就會(huì)產(chǎn)生運(yùn)行時(shí)錯(cuò)誤围小。
2)如果不加棧信息昵骤,只是new自定義異常,加入自己的理解的error message肯适,對(duì)于調(diào)用端解決問題的幫助不會(huì)太多变秦。如果加了棧信息,在頻繁調(diào)用出錯(cuò)的情況下框舔,數(shù)據(jù)序列化和傳輸?shù)男阅軗p耗也是問題蹦玫。【參考】避免出現(xiàn)重復(fù)的代碼(Don’t Repeat Yourself),即DRY原則雨饺。
說明:隨意復(fù)制和粘貼代碼钳垮,必然會(huì)導(dǎo)致代碼的重復(fù),在以后需要修改時(shí)额港,需要修改所有的副本,容易遺漏歧焦。必要時(shí)抽取共性方法移斩,或者抽象公共類,甚至是共用模塊绢馍。
正例:一個(gè)類中有多個(gè)public方法向瓷,都需要進(jìn)行數(shù)行相同的參數(shù)校驗(yàn)操作,這個(gè)時(shí)候請(qǐng)抽冉⒂俊:
private boolean checkParam(DTO dto) {...}
說的很對(duì)猖任,但為啥放在異常處理分類下……?
(二) 日志規(guī)約
- 【強(qiáng)制】應(yīng)用中不可直接使用日志系統(tǒng)(Log4j瓷耙、Logback)中的API朱躺,而應(yīng)依賴使用日志框架SLF4J中的API刁赖,使用門面模式的日志框架,有利于維護(hù)和各個(gè)類的日志處理方式統(tǒng)一长搀。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(Abc.class);
slf4j是日志門面框架宇弛,其僅提供日志記錄的API,而不實(shí)現(xiàn)日志記錄的功能源请,slf4j需要通過適配庫(kù)適配到log4j或logback等日至系統(tǒng)來(lái)實(shí)現(xiàn)日志的記錄枪芒。
使用slf4j api能夠提升代碼和應(yīng)用的可移植性,在使用不同日志系統(tǒng)的應(yīng)用之間能夠做到無(wú)縫的適配谁尸。
同時(shí)舅踪,使用slf4j api的應(yīng)用,在切換日志系統(tǒng)時(shí)(比如從logback切換到log4j2良蛮,不需要代碼改造)
【強(qiáng)制】日志文件推薦至少保存15天抽碌,因?yàn)橛行┊惓>邆湟浴爸堋睘轭l次發(fā)生的特點(diǎn)。
【強(qiáng)制】應(yīng)用中的擴(kuò)展日志(如打點(diǎn)背镇、臨時(shí)監(jiān)控咬展、訪問日志等)命名方式:appName_logType_logName.log。
logType:日志類型瞒斩,推薦分類有stats/desc/monitor/visit等破婆;
logName:日志描述。這種命名的好處:通過文件名就可知道日志文件屬于什么應(yīng)用胸囱,什么類型祷舀,什么目的,也有利于歸類查找烹笔。
正例:mppserver應(yīng)用中單獨(dú)監(jiān)控時(shí)區(qū)轉(zhuǎn)換異常裳扯,如: mppserver_monitor_timeZoneConvert.log
說明:推薦對(duì)日志進(jìn)行分類,如將錯(cuò)誤日志和業(yè)務(wù)日志分開存放谤职,便于開發(fā)人員查看饰豺,也便于通過日志對(duì)系統(tǒng)進(jìn)行及時(shí)監(jiān)控。【強(qiáng)制】對(duì)trace/debug/info級(jí)別的日志輸出允蜈,必須使用條件輸出形式或者使用占位符的方式冤吨。
說明:logger.debug("Processing trade with id: " + id + " symbol: " + symbol); 如果日志級(jí)別是warn,上述日志不會(huì)打印饶套,但是會(huì)執(zhí)行字符串拼接操作漩蟆,如果symbol是對(duì)象,會(huì)執(zhí)行toString()方法妓蛮,浪費(fèi)了系統(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庫(kù)適配
- 【強(qiáng)制】避免重復(fù)打印日志翘簇,浪費(fèi)磁盤空間撬码,務(wù)必在log4j.xml中設(shè)置additivity=false。
正例:
<logger name="com.taobao.dubbo.config" additivity="false">
additivity默認(rèn)為true版保,即通過該logger輸出的日志會(huì)同時(shí)輸出到root logger呜笑,如果還為該logger指定了獨(dú)立的appender,就會(huì)導(dǎo)致這部分日志重復(fù)輸出
- 【強(qiáng)制】異常信息應(yīng)該包括兩類信息:案發(fā)現(xiàn)場(chǎng)信息和異常堆棧信息彻犁。如果不處理叫胁,那么通過關(guān)鍵字throws往上拋出。
正例:
logger.error(各類參數(shù)或者對(duì)象toString + "_" + e.getMessage(), e);
記錄異常日志的常見錯(cuò)誤:
logger.error(e); logger.error(e.getMessage()); logger.error("上下文"+e.getMessage())汞幢;
上面這幾種都是錯(cuò)的驼鹅!請(qǐng)確保使用的是兩個(gè)入?yún)⒌腁PI,如error(String s, Throwable t)
- 【推薦】謹(jǐn)慎地記錄日志森篷。生產(chǎn)環(huán)境禁止輸出debug日志输钩;有選擇地輸出info日志;如果使用warn來(lái)記錄剛上線時(shí)的業(yè)務(wù)行為信息仲智,一定要注意日志輸出量的問題买乃,避免把服務(wù)器磁盤撐爆,并記得及時(shí)刪除這些觀察日志钓辆。
說明:大量地輸出無(wú)效日志剪验,不利于系統(tǒng)性能提升,也不利于快速定位錯(cuò)誤點(diǎn)前联。記錄日志時(shí)請(qǐng)思考:這些日志真的有人看嗎功戚?看到這條日志你能做什么?能不能給問題排查帶來(lái)好處似嗤?
不要認(rèn)為日志記錄不怎么消耗性能啸臀,我見過不少事無(wú)巨細(xì)式的日志把系統(tǒng)性能嚴(yán)重拖慢的案例
- 【參考】可以使用warn日志級(jí)別來(lái)記錄用戶輸入?yún)?shù)錯(cuò)誤的情況,避免用戶投訴時(shí)烁落,無(wú)所適從壳咕。注意日志輸出的級(jí)別,error級(jí)別只記錄系統(tǒng)邏輯出錯(cuò)顽馋、異常等重要的錯(cuò)誤信息。如非必要幌羞,請(qǐng)不要在此場(chǎng)景打出error級(jí)別寸谜。