異常日志
(一) 異常處理
異常接口及類結(jié)構(gòu)
- 【強制】Java 類庫中定義的一類 RuntimeException 可以通過預(yù)先檢查進(jìn)行規(guī)避床未,而不應(yīng)該通過 catch 來處理塑娇,比如:IndexOutOfBoundsException垢啼,NullPointerException 等等业稼。
正例:if (obj != null) {...}
反例:try { obj.method() } catch (NullPointerException e) {...}
理解:判空是一個永恒的話題,只要你不確定變量是否為空氢惋,都應(yīng)該判空洞翩,否則后患無窮。另外焰望,catch語句塊中不要寫業(yè)務(wù)邏輯骚亿,就打印或拋出異常日志就可以了,當(dāng)然這可能只對開發(fā)階段有用熊赖,最好采用錯誤代碼結(jié)合輸出日志進(jìn)行處理来屠。
- 【強制】異常不要用來做流程控制,條件控制震鹉,因為異常的處理效率比條件分支低俱笛。
理解:禁止使用異常來封裝業(yè)務(wù)邏輯,業(yè)務(wù)異常應(yīng)該用錯誤碼來表示传趾,系統(tǒng)異常則使用Java原生異常迎膜。
異常處理是通過異常表查詢來實現(xiàn)的,肯定沒有跳轉(zhuǎn)語句性能高墨缘。
- 【強制】對大段代碼進(jìn)行 try-catch星虹,這是不負(fù)責(zé)任的表現(xiàn)零抬。catch 時請分清穩(wěn)定代碼和非穩(wěn) 定代碼镊讼,穩(wěn)定代碼指的是無論如何不會出錯的代碼宽涌。對于非穩(wěn)定代碼的 catch 盡可能進(jìn)行區(qū)分異常類型,再做對應(yīng)的異常處理蝶棋。
理解:不能簡單的catch Throwable卸亮,然后打印日志,這是不負(fù)責(zé)任的表現(xiàn)玩裙,應(yīng)該有針對的抓住和處理異常兼贸。
- 【強制】捕獲異常是為了處理它,不要捕獲了卻什么都不處理而拋棄之吃溅,如果不想處理它溶诞,請將該異常拋給它的調(diào)用者。最外層的業(yè)務(wù)使用者决侈,必須處理異常螺垢,將其轉(zhuǎn)化為用戶可以理解的內(nèi)容。
理解:我們不能讓客戶看到的是Java原生異常碼赖歌。
- 【強制】有 try 塊放到了事務(wù)代碼中枉圃,catch 異常后,如果需要回滾事務(wù)庐冯,一定要注意手動回滾事務(wù)孽亲。
理解:框架我們一般基本采用聲明式事務(wù),出現(xiàn)異常需要回滾的情況展父,建議繼續(xù)拋出異常讓聲明式事務(wù)自動回滾返劲,不建議代碼中手工控制事務(wù)。
- 【強制】finally 塊必須對資源對象栖茉、流對象進(jìn)行關(guān)閉旭等,有異常也要做 try-catch。
說明:如果 JDK7 及以上衡载,可以使用 try-with-resources 方式搔耕。
理解:資源對象如數(shù)據(jù)庫連接,流對象如文件流痰娱,關(guān)于try catch finally的使用弃榨。
1、不管有沒有異常梨睁,finally中的代碼都會執(zhí)行
2鲸睛、當(dāng)try、catch中有return時坡贺,finally中的代碼依然會繼續(xù)執(zhí)行
3官辈、finally是在return后面的表達(dá)式運算之后執(zhí)行的箱舞,此時并沒有返回運算之后的值,而是把值保存起來拳亿,不管finally對該值做任何的改變晴股,返回的值都不會改變,依然返回保存起來的值肺魁。也就是說方法的返回值是在finally運算之前就確定了的电湘。
4、finally代碼中最好不要包含return鹅经,程序會提前退出寂呛,也就是說返回的值不是try或catch中的值
舉例: ?
情況1:try{}catch{}finally{} return;
程序正常進(jìn)行
情況2:try{return;}catch{}finally{} return;
無異常:a、先執(zhí)行try中的語句瘾晃,b贷痪、然后執(zhí)行finally中的語句,c蹦误、最后執(zhí)行try中的return
有異常:返回值是finally后面的return語句
情況3:try{} catch{return;} finally{} return;
無異常:執(zhí)行finally中的代碼塊劫拢,然后執(zhí)行return語句
有異常:a、先執(zhí)行catch中的代碼塊 b胖缤、然后執(zhí)行finally中的語句尚镰,c、最后執(zhí)行catch中的return哪廓,finally后面的return不會被執(zhí)行
情況4:try{return;} catch{} finally{return;}
無異常:a狗唉、先執(zhí)行try中的語句,包括return后面的表達(dá)式涡真,b分俯、然后執(zhí)行finally中的語句,c哆料、最后執(zhí)行finally中的return
有異常:返回的值是finally中return后面的表達(dá)式的值缸剪,因為finally中有return語句,所以會提前退出东亦。
情況5:try{} catch{return} finally{return};
先執(zhí)行try中的代碼塊
無異常:執(zhí)行finally中的代碼塊杏节,然后執(zhí)行finally中的return
有異常:a、先執(zhí)行catch中的語句典阵,包括return后面的表達(dá)式奋渔,b、然后執(zhí)行finally中的語句壮啊,c嫉鲸、最后執(zhí)行finally中的return,因為finally中有return語句歹啼,所以會提前退出
情況6:try{return;} catch{return;} finally{return;}
先執(zhí)行try中的代碼塊玄渗,包括return后面的表達(dá)式
無異常:執(zhí)行finally中的代碼塊座菠,然后執(zhí)行finally中的return
有異常:a、先執(zhí)行catch中的語句藤树,包括return后面的表達(dá)式浴滴,b、然后執(zhí)行finally中的語句也榄,c巡莹、最后執(zhí)行finally中的return司志,因為finally中有return語句甜紫,所以會提前退出。 ?
JDK7之前骂远,通常的try catch finally用法
public static void main(String[] args) {
FileInputStream inputStream = null;
try {
inputStream = new FileInputStream(new File("test"));
System.out.println(inputStream.read());
} catch (IOException e) {
throw new RuntimeException(e.getMessage(), e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
}
}
?
JDk7 try-with-resources
public static void main(String[] args) {
try (FileInputStream inputStream = new FileInputStream(new File("test"))) {
System.out.println(inputStream.read());
} catch (IOException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
- 【強制】不能在 finally 塊中使用 return囚霸,finally 塊中的 return 返回后方法結(jié)束執(zhí)行,不會再執(zhí)行 try 塊中的 return 語句激才。
理解:見6的詳細(xì)分析拓型。
- 【強制】捕獲異常與拋異常,必須是完全匹配瘸恼,或者捕獲異常是拋異常的父類劣挫。
說明:如果預(yù)期對方拋的是繡球,實際接到的是鉛球东帅,就會產(chǎn)生意外情況压固。
理解:異常處理后,讓異常變得更小靠闭,而不是變大帐我,大而化小,小而化了愧膀。
- 【推薦】方法的返回值可以為 null拦键,不強制返回空集合,或者空對象等檩淋,必須添加注釋充分說明什么情況下會返回 null 值芬为。調(diào)用方需要進(jìn)行 null 判斷防止 NPE 問題。
說明:本手冊明確防止 NPE 是調(diào)用者的責(zé)任蟀悦。即使被調(diào)用方法返回空集合或者空對象媚朦,對調(diào)用者來說,也并非高枕無憂熬芜,必須考慮到遠(yuǎn)程調(diào)用失敗莲镣、序列化失敗、運行時異常等場景返回 null 的情況涎拉。
理解:只要不確定的變量瑞侮,一定要判空的圆,別自找麻煩。
- 【推薦】防止 NPE半火,是程序員的基本修養(yǎng)越妈,注意 NPE 產(chǎn)生的場景:
1)返回類型為基本數(shù)據(jù)類型,return 包裝數(shù)據(jù)類型的對象時钮糖,自動拆箱有可能產(chǎn)生 NPE梅掠。
反例:public int f() { return Integer 對象}, 如果為 null店归,自動解箱拋 NPE
2) 數(shù)據(jù)庫的查詢結(jié)果可能為 null阎抒。
3) 集合里的元素即使 isNotEmpty,取出的數(shù)據(jù)元素也可能為 null消痛。
4) 遠(yuǎn)程調(diào)用返回對象時且叁,一律要求進(jìn)行空指針判斷,防止 NPE秩伞。
5) 對于 Session 中獲取的數(shù)據(jù)逞带,建議 NPE 檢查,避免空指針纱新。
6) 級聯(lián)調(diào)用 obj.getA().getB().getC()展氓;一連串調(diào)用,易產(chǎn)生 NPE脸爱。
正例:使用 JDK8 的 Optional 類來防止 NPE 問題遇汞。
理解:自J2SE 5.0開始提供的基本數(shù)據(jù)類型的自動裝箱(autoboxing)、拆箱(unboxing)功能阅羹。
何為自動裝箱:
當(dāng)我們創(chuàng)建一個Integer對象時勺疼,卻可以這樣:
Integer i = 100; (注意:不是 int i = 100; )
實際上,執(zhí)行上面那句代碼的時候捏鱼,系統(tǒng)為我們執(zhí)行了:Integer i = new Integer(100); 此即基本數(shù)據(jù)類型的自動裝箱功能执庐。
?
何為自動拆箱
自動拆箱(unboxing),也就是將對象中的基本數(shù)據(jù)從對象中自動取出导梆。如下可實現(xiàn)自動拆箱:
Integer integer=100;
int flag=integer; //該語句即實現(xiàn)了自動拆箱轨淌。
?
JDK8 的 Optional 類來防止 NPE詳見如下鏈接。
- 【推薦】定義時區(qū)分 unchecked / checked 異常看尼,避免直接拋出 new RuntimeException()递鹉, 更不允許拋出 Exception 或者 Throwable,應(yīng)使用有業(yè)務(wù)含義的自定義異常藏斩。推薦業(yè)界已定義過的自定義異常躏结,如:DAOException / ServiceException 等。
理解:將派生于Error或者RuntimeException的異常稱為unchecked異常狰域,所有其他的異常稱為checked異常媳拴。
盡量不要在拋出異常時throw new RuntimeException("xxxx"); 應(yīng)該使用具備業(yè)務(wù)含義的自定義異常類黄橘,這樣做可以在捕獲異常時提供方便
絕對不要在拋出異常時throw new Exception("xxx")或throw new Throwable("xxx"),這樣做不僅僅是屏蔽了異常本身的業(yè)務(wù)含義屈溉,同時也屏蔽了異常的分類(checked/unchecked)塞关,甚至連Exception和Error的區(qū)別也屏蔽了
異常處理機(jī)制見如下鏈接。
- 【參考】在代碼中使用“拋異撤”還是“返回錯誤碼”,對于公司外的 http/api 開放接口必須 使用“錯誤碼”线梗;而應(yīng)用內(nèi)部推薦異常拋出椰于;跨應(yīng)用間 RPC 調(diào)用優(yōu)先考慮使用 Result 方式,封 裝 isSuccess()方法缠导、“錯誤碼”廉羔、“錯誤簡短信息”溉痢。
說明:關(guān)于 RPC 方法返回方式使用 Result 方式的理由:
1)使用拋異常返回方式僻造,調(diào)用方如果沒有捕獲到就會產(chǎn)生運行時錯誤。
2)如果不加棧信息孩饼,只是 new 自定義異常髓削,加入自己的理解的 error message,對于調(diào)用端解決問題的幫助不會太多镀娶。如果加了棧信息立膛,在頻繁調(diào)用出錯的情況下,數(shù)據(jù)序列化和傳輸?shù)男阅軗p耗也是問題梯码。
理解:業(yè)務(wù)異常使用Result模式宝泵,系統(tǒng)異常用Java原生異常。
RPC建議使用Result模式轩娶,不想讓一個異常在系統(tǒng)間跳來跳去的儿奶,異常是包含調(diào)用棧的。
- 【參考】避免出現(xiàn)重復(fù)的代碼(Don’t Repeat Yourself)鳄抒,即 DRY 原則闯捎。
說明:隨意復(fù)制和粘貼代碼,必然會導(dǎo)致代碼的重復(fù)许溅,在以后需要修改時瓤鼻,需要修改所有的副本,容易遺漏贤重。必要時抽取共性方法茬祷,或者抽象公共類,甚至是組件化并蝗。
正例:一個類中有多個 public 方法祭犯,都需要進(jìn)行數(shù)行相同的參數(shù)校驗操作耐朴,這個時候請抽取: private boolean checkParam(DTO dto) {...}
理解:DRY原則對于優(yōu)化和維護(hù)程序都很有用處盹憎。這一條放這里有點莫名其妙筛峭。
(二) 日志規(guī)約
- 【強制】應(yīng)用中不可直接使用日志系統(tǒng)(Log4j、Logback)中的 API陪每,而應(yīng)依賴使用日志框架 SLF4J 中的 API影晓,使用門面模式的日志框架,有利于維護(hù)和各個類的日志處理方式統(tǒng)一檩禾。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(Abc.class);
理解:設(shè)計模式分為三大類:
創(chuàng)建型模式挂签,共五種:工廠方法模式、抽象工廠模式盼产、單例模式饵婆、建造者模式、原型模式戏售。
結(jié)構(gòu)型模式侨核,共七種:適配器模式、裝飾器模式灌灾、代理模式搓译、外觀模式、橋接模式锋喜、組合模式些己、享元模式。
行為型模式嘿般,共十一種:策略模式段标、模板方法模式、觀察者模式炉奴、迭代子模式逼庞、責(zé)任鏈模式、命令模式盆佣、備忘錄模式往堡、狀態(tài)模式、訪問者模式共耍、中介者模式虑灰、解釋器模式。
所謂門面模式(外觀模式)是對象的結(jié)構(gòu)模式痹兜,外部與一個子系統(tǒng)的通信必須通過一個統(tǒng)一的門面對象進(jìn)行穆咐。門面模式提供一個高層次的接口,使得子系統(tǒng)更易于使用。門面模式實例如下:
- 【強制】日志文件推薦至少保存 15 天对湃,因為有些異常具備以“周”為頻次發(fā)生的特點崖叫。
理解:日志文件也不要保存太久,日志的增長很快拍柒,如果不及時處理(最好能自動化處理)心傀,就會導(dǎo)致系統(tǒng)不可以用,譬如nginx做負(fù)載均衡拆讯,日志如果達(dá)到70%脂男,會嚴(yán)重影響訪問性能。處理方法參見:
nginx訪問日志文件過大導(dǎo)致服務(wù)器性能降低解決方法
- 【強制】應(yīng)用中的擴(kuò)展日志(如打點(數(shù)據(jù)統(tǒng)計的日志稱為打點日志)种呐、臨時監(jiān)控宰翅、訪問日志等)命名方式:
appName_logType_logName.log。
logType:日志類型爽室,推薦分類有 stats/desc/monitor/visit 等汁讼;
logName:日志描述。
這種命名的好處:通過文件名就可知道日志文件屬于什么應(yīng)用阔墩,什么類型嘿架,什么目的,也有利于歸類查找戈擒。
正例:mppserver 應(yīng)用中單獨監(jiān)控時區(qū)轉(zhuǎn)換異常眶明,如:mppserver_monitor_timeZoneConvert.log
說明:推薦對日志進(jìn)行分類,如將錯誤日志和業(yè)務(wù)日志分開存放筐高,便于開發(fā)人員查看,也便于通過日志對系統(tǒng)進(jìn)行及時監(jiān)控丑瞧。
- 【強制】對 trace/debug/info 級別的日志輸出柑土,必須使用條件輸出形式或者使用占位符的方 式。
說明:logger.debug("Processing trade with id: " + id + " and symbol: " + symbol);
如果日志級別是 warn绊汹,上述日志不會打印稽屏,但是會執(zhí)行字符串拼接操作,如果 symbol 是對象西乖, 會執(zhí)行 toString()方法狐榔,浪費了系統(tǒng)資源,執(zhí)行了上述操作获雕,最終日志卻沒有打印薄腻。
正例:(條件) if (logger.isDebugEnabled()) {
logger.debug("Processing trade with id: " + id + " and symbol: " + symbol);
}
正例:(占位符) logger.debug("Processing trade with id: {} and symbol : {} ", id, symbol);
理解:Log4j建議只使用四個級別,優(yōu)先級從高到低分別是 ERROR届案、WARN庵楷、INFO、DEBUG。通過在這里定義的級別尽纽,您可以控制到應(yīng)用程序中相應(yīng)級別的日志信息的開關(guān)咐蚯。比如在這里定義了INFO級別,則應(yīng)用程序中所有DEBUG級別的日志信息將不被打印出來弄贿,也是說大于等于的級別的日志才輸出春锋。
- 【強制】避免重復(fù)打印日志,浪費磁盤空間差凹,務(wù)必在 log4j.xml 中設(shè)置 additivity=false看疙。
正例:<logger name="com.taobao.dubbo.config" additivity="false">
理解:log4j配置詳細(xì)說明參見:
- 【強制】異常信息應(yīng)該包括兩類信息:案發(fā)現(xiàn)場信息和異常堆棧信息。如果不處理直奋,那么通過 關(guān)鍵字 throws 往上拋出能庆。
正例:logger.error(各類參數(shù)或者對象 toString + "_" + e.getMessage(), e);
理解:打印日志一定要包含環(huán)境
- 【推薦】謹(jǐn)慎地記錄日志。生產(chǎn)環(huán)境禁止輸出 debug 日志脚线;有選擇地輸出 info 日志搁胆;如果使用 warn 來記錄剛上線時的業(yè)務(wù)行為信息,一定要注意日志輸出量的問題邮绿,避免把服務(wù)器磁盤撐爆渠旁,并記得及時刪除這些觀察日志。
說明:大量地輸出無效日志船逮,不利于系統(tǒng)性能提升顾腊,也不利于快速定位錯誤點。記錄日志時請思考:這些日志真的有人看嗎挖胃?看到這條日志你能做什么杂靶?能不能給問題排查帶來好處?
理解:打印日志只需要打印核心內(nèi)容酱鸭,不要隨便就把對象json序列化打印出來吗垮,如果是列表會很大。
- 【參考】可以使用 warn 日志級別來記錄用戶輸入?yún)?shù)錯誤的情況凹髓,避免用戶投訴時烁登,無所適從。注意日志輸出的級別蔚舀,error 級別只記錄系統(tǒng)邏輯出錯饵沧、異常等重要的錯誤信息。如非必 要赌躺,請不要在此場景打出 error 級別狼牺。
理解:合理利用warn級別日志,error級別日志最重要寿谴,理想情況下生產(chǎn)上產(chǎn)生的error和warn日志開發(fā)要定期的梳理锁右。
- 【推薦】盡量用英文來描述日志錯誤信息,如果日志中的錯誤信息用英文描述不清楚的話使用 中文描述即可,否則容易產(chǎn)生歧義咏瑟。國際化團(tuán)隊或海外部署的服務(wù)器由于字符集問題拂到,【強制】 使用全英文來注釋和描述日志錯誤信息。
討論
異常處理和日志處理在combiz上可能需要改進(jìn)一下码泞。