|
做一個苦逼的Java攻城師, 我們除了關(guān)心系統(tǒng)的架構(gòu)這種high level的問題, 還需要了解一些語言的陷阱, 異常的處理, 以及日志的輸出, 這些"雞毛蒜皮"的細(xì)節(jié). 這篇文章是JCP成員, Tomasz Nurkiewicz( http://nurkiewicz./ )總結(jié)的10條如何正確使用日志的技巧(參見原文). 跟那篇"java編程最差實(shí)踐"一樣, 也是針對一些細(xì)節(jié)的, 因?yàn)槿罩臼俏覀兣挪閱栴}, 了解系統(tǒng)狀況的重要線索. 我覺得對我們平常coding非常有借鑒意義. 所以轉(zhuǎn)換成中文, 加深一下印象, 也作為自己工作的一個參考.
1)選擇正確的Log開源框架 在代碼中為了知道程序的行為的狀態(tài), 我們一般會打印一條日志: Java代碼
在所有的日志框架中, 我認(rèn)為最好的是SLF4J. 比如在Log4J中我們會這樣寫: Java代碼
而在SLF4J中我們會這樣寫: Java代碼
從可讀性和系統(tǒng)效率來說, SLF4J( http://logback./ )比Log4J都要優(yōu)秀(Log4J涉及到字符串連接和toString()方法的調(diào)用). 這里的{}帶來的另一個好處, 我們在盡量不損失性能的情況, 不必為了不同的日志輸出級別, 而加上類似isDebugEnabled()判斷. SLF4J只是各種日志實(shí)現(xiàn)的一個接口抽象(facade), 而最佳的實(shí)現(xiàn)是Logback, 相對于Log4J的同門兄弟(皆出自Ceki Gülcü之手), 它在開源社區(qū)的活躍度更高. 最后要推薦的是Perf4J( http://perf4j./ ). 用一句話來概括就是: 如果把log4j看做System.out.println()的話, 那么Perf4J就是System.currentTimeMillis() Perf4J可以幫助我們更方便的輸出系統(tǒng)性能的日志信息. 然后借助其他報(bào)表工具將日志以圖表的形式加以展現(xiàn), 從而方便我們分析系統(tǒng)的性能瓶頸. 關(guān)于Perf4J的使用可以參考它的開發(fā)者指南(http://perf4j./devguide.html). 下面是一份關(guān)于日志jar包依賴的pom.xml參考模板: Xml代碼
下面是測試代碼: Java代碼
上面的代碼, 無論你采用哪個log框架輸出日志, 底層采用的都是logback, 至于為什么, 可以看這里(http://www./legacy.html), 另外這里為了在classpath里面不引入common-logging, 用了一個小技巧, 就是將依賴版本設(shè)置為99.0-does-not-exist, 關(guān)于這種用法的說明可以看這里(http://day-to-day-stuff./2007/10/announcement-version-99-does-not-exist.html), 不過log4j的作者認(rèn)為最簡單的做法就是直接去掉對common-logging的依賴, 相關(guān)內(nèi)容可以看這里的說明(http://www./faq.html#excludingJCL) 2) 理解正確的日志輸出級別 很多程序員都忽略了日志輸出級別, 甚至不知道如何指定日志的輸出級別. 相對于System.out來說, 日志框架有兩個最大的優(yōu)點(diǎn)就是可以指定輸出類別(category)和級別(level). 對于日志輸出級別來說, 下面是我們應(yīng)該記住的一些原則: ERROR:系統(tǒng)發(fā)生了嚴(yán)重的錯誤, 必須馬上進(jìn)行處理, 否則系統(tǒng)將無法繼續(xù)運(yùn)行. 比如, NPE, 數(shù)據(jù)庫不可用等. WARN:系統(tǒng)能繼續(xù)運(yùn)行, 但是必須引起關(guān)注. 對于存在的問題一般可以分為兩類: 一種系統(tǒng)存在明顯的問題(比如, 數(shù)據(jù)不可用), 另一種就是系統(tǒng)存在潛在的問題, 需要引起注意或者給出一些建議(比如, 系統(tǒng)運(yùn)行在安全模式或者訪問當(dāng)前系統(tǒng)的賬號存在安全隱患). 總之就是系統(tǒng)仍然可用, 但是最好進(jìn)行檢查和調(diào)整. INFO:重要的業(yè)務(wù)邏輯處理完成. 在理想情況下, INFO的日志信息要能讓高級用戶和系統(tǒng)管理員理解, 并從日志信息中能知道系統(tǒng)當(dāng)前的運(yùn)行狀態(tài). 比如對于一個機(jī)票預(yù)訂系統(tǒng)來說, 當(dāng)一個用戶完成一個機(jī)票預(yù)訂操作之后, 提醒應(yīng)該給出"誰預(yù)訂了從A到B的機(jī)票". 另一個需要輸出INFO信息的地方就是一個系統(tǒng)操作引起系統(tǒng)的狀態(tài)發(fā)生了重大變化(比如數(shù)據(jù)庫更新, 過多的系統(tǒng)請求). DEBUG:主要給開發(fā)人員看, 下面會進(jìn)一步談到. TRACE: 系統(tǒng)詳細(xì)信息, 主要給開發(fā)人員用, 一般來說, 如果是線上系統(tǒng)的話, 可以認(rèn)為是臨時輸出, 而且隨時可以通過開關(guān)將其關(guān)閉. 有時候我們很難將DEBUG和TRACE區(qū)分開, 一般情況下, 如果是一個已經(jīng)開發(fā)測試完成的系統(tǒng), 再往系統(tǒng)中添加日志輸出, 那么應(yīng)該設(shè)為TRACE級別. 以上只是建議, 你也可以建立一套屬于你自己的規(guī)則. 但是一套良好的日志系統(tǒng), 應(yīng)該首先是能根據(jù)情況快速靈活的調(diào)整日志內(nèi)容的輸出. 最后要提到的就是"臭名昭著"的is*Enabled()條件, 比如下面的寫法: Java代碼
這種做法對性能的提高幾乎微乎其微(前面在提到SLF4J的時候已經(jīng)說明), 而且是一種過度優(yōu)化的表現(xiàn). 極少情況下需要這樣寫, 除非構(gòu)造日志信息非常耗性能. 最后必須記住一點(diǎn): 程序員應(yīng)該專注于日志內(nèi)容, 而將日志的輸出的決定權(quán)交給日志框架去非處理. 3) 你真的知道log輸出的內(nèi)容嗎? 對于你輸出的每一條log信息, 請仔細(xì)檢查最終輸出的內(nèi)容是否存在問題, 其中最重要的就是避免NPE, 比如想下面這樣: Java代碼
這里我們能否保證request不為null? 除了NPE之外, 有時候我們可能還需要考慮, 是否會導(dǎo)致OOM? 越界訪問? 線程饑餓(log是同步的)? 延遲初始化異常? 日志打爆磁盤等等. 另外一個問題就是在日志中輸出集合(collection), 有時候我們輸出的集合內(nèi)容可能是由Hibernate從數(shù)據(jù)庫中取出來的, 比如下面這條日志信息: Java代碼
這里最佳的處理方式是僅僅輸出domain對象的id或者集合的大小(size), 而對Java來說, 不得不要吐槽幾句, 要遍歷訪問集合中每一個元素的getId方法非常繁瑣. 這一點(diǎn)Groovy就做的非常簡單(users*.id), 不過我們可以借助Commons Beanutils工具包來幫我們簡化: Java代碼
這里的collect方法的實(shí)現(xiàn)如下: Java代碼
不過不幸的是, 在給Commons Beanutils提了一個patch(BEANUTILS-375 https://issues./jira/browse/BEANUTILS-375)之后, 并沒有被接受:( 最后是關(guān)于toString()方法. 為了讓日志更容易理解, 最好為每一個類提供合適的toString()方法. 這里可以借助ToStringBuilder工具類. 另外一個就是關(guān)于數(shù)組和某些集合類型. 因?yàn)閿?shù)組是使用的默認(rèn)的toString方法. 而某些集合沒有很好的實(shí)現(xiàn)toString方法. 對于數(shù)組我們可以使用JDK的Arrays.deepToString()方法(http://docs.oracle.com/javase/6/docs/api/java/util/Arrays.html#deepToString%28java.lang.Object[]%29). 4) 小心日志的副作用 有時候日志或多或少的會影響系統(tǒng)的行為, 比如最近碰到的一個情況就是在某些條件下, Hibernate會拋出LazyInitializationException. 這是因?yàn)槟承┹敵鋈罩緦?dǎo)致延遲初始化的集合在session建立時被加載. 而在某些場景下當(dāng)提高日志輸出級別時, 問題就會消失. 另一個副作用就是日志導(dǎo)致系統(tǒng)運(yùn)行越來越慢. 比如不恰當(dāng)?shù)氖褂胻oString方法或者字符串連接, 使得系統(tǒng)出現(xiàn)性能問題, 曾經(jīng)碰到的一個現(xiàn)象, 某個系統(tǒng)每隔15分鐘重啟一次, 這個主要是執(zhí)行l(wèi)og輸出出現(xiàn)線程饑餓導(dǎo)致, 一般情況下, 如果一個系統(tǒng)一小時內(nèi)生成的日志有幾百M(fèi)B的時候, 就要小心了. 而如果因?yàn)槿罩据敵霰旧淼膯栴}導(dǎo)致正常的業(yè)務(wù)邏輯被中斷, 那就更嚴(yán)重了. 比如下面這種代碼, 最好不要這樣寫: Java代碼
5) 日志信息應(yīng)該簡潔且可描述 一般, 每一條日志數(shù)據(jù)會包括描述和上下文兩部分, 比如下面的日志: Java代碼
第一條只有描述, 第二條只有上下文, 第三條才算完整的一條日志, 還有下面這種日志: Java代碼
在上面的警告日志中沒有包含實(shí)際的message type, message id等信息, 只是表明程序有問題, 那么是什么導(dǎo)致了問題呢? 上下文是什么? 我們什么都不知道. 另外一個問題就是在日志中加上一個莫名其妙的內(nèi)容, 即所謂的"magic log". 比如有些程序員會在日志中隨手敲上"&&&!#"這樣一串字符, 用來幫助他們定位. 一個日志文件中的內(nèi)容應(yīng)該易讀, 清晰, 可描述. 而不要使用莫名其妙的字符, 日志要給出當(dāng)前操作做了什么, 使用的什么數(shù)據(jù). 好的日志應(yīng)該被看成文檔注釋的一部分. 最后一點(diǎn), 切記不要在日志中包含密碼和個人隱私信息! 6) 正確的使用輸出模式 log輸出模式可以幫助我們在日志中增加一些清晰的上下文信息. 不過對添加的信息還是要多加小心. 比如說, 如果你是每小時輸出一個文件, 這樣你的日志文件名中已經(jīng)包含了部分日期時間信息, 因此就沒必要在日志中再包含這些信息. 另外在多線程環(huán)境下也不要在自己在日志中包含線程名稱, 因?yàn)檫@個也可以在模式中配置. 根據(jù)我的經(jīng)驗(yàn), 一個理想的日志模式將包含下列信息:
比如像下面這個logback配置: Xml代碼
千萬不要在日志信息中包含下列內(nèi)容:
還有下面這種寫法也是要避免的: Java代碼
因?yàn)槌绦騿T知道, 在日志模式中會指定行號, 因此他就可以根據(jù)日志輸?shù)男刑柍雠袛嘀付ǖ姆椒ㄊ欠癖徽{(diào)用了(比如這里可能是authenticate()方法, 進(jìn)而判斷登錄的用戶已經(jīng)經(jīng)過了驗(yàn)證). 另外, 大家也要清楚一點(diǎn), 在日志模式中指定類名, 方法名以及行號會帶來嚴(yán)重的性能問題. 下面是我針對這個做的一個簡單的測試, 配置如下: Xml代碼
下面是測試代碼: Java代碼
在上面的測試代碼中, CLASS_INFO日志輸出了1500萬次, 而NO_CLASS_INFO輸出了3億次. 后者采用一個靜態(tài)的文本來取代日志模式中的動態(tài)類信息. 從下面的對比圖可以看出, 直接在日志模式中指定類名的日志比使用反射動態(tài)獲取類名的要快13倍(平均輸出每條日志耗時:8.8 vs 115微秒). 對于一個java程序員來說, 一條日志耗時100微秒是可以接受的. 這也就是說, 一個后臺應(yīng)用其中1%的時間消耗在了輸出日志上. 因此我們有時候也需要權(quán)衡一下, 每秒100條日志輸出是否是合理的. ![]() 最后要提到的是日志框架中比較高級的功能: Mapped Diagnostic Context. MDC(http://www./api/org/slf4j/MDC.html )主要用來簡化基于thread-local的map參數(shù)管理. 你可以往這個map中增加任何key-value內(nèi)容, 然后在隨后的日志輸出中作為模式的一部分, 與當(dāng)前線程一起輸出. 7) 給方法的輸入輸出加上日志 當(dāng)我們在開發(fā)過程中發(fā)現(xiàn)了一個bug, 一般我們會采用debug的方式一步步的跟蹤, 直到定位到最終的問題位置(如果能通過寫一個失敗的單元測試來暴露問題, 那就更帥了^_^). 但是如果實(shí)際情況不允許你debug時, 比如在客戶的系統(tǒng)上幾天前出現(xiàn)的bug. 如果你沒有詳細(xì)的日志的話, 你能找到問題的根源么? 如果你能根據(jù)一些簡單的規(guī)則來輸出每個方法的輸入和輸出(參數(shù)和返回值). 你基本上可以扔掉調(diào)試器了. 當(dāng)然針對每一個方法加上日志必須是合理的: 訪問外部資源(比如數(shù)據(jù)庫), 阻塞, 等待等等, 這些地方可以考慮加上日志. 比如下面的代碼: Java代碼
因?yàn)樵诜椒ㄕ{(diào)用前后加上了日志, 我們可以非常方便的發(fā)現(xiàn)代碼的性能問題, 甚至找出死鎖和線程饑餓(starvation)等嚴(yán)重問題:這種情況下都只有輸入(entering)日志, 不會有輸出(leaving)日志. 如果方法名類名使用得當(dāng), 那么輸出的日志信息也將會非常賞心悅目. 因?yàn)槟憧梢愿鶕?jù)日志完整了解系統(tǒng)的運(yùn)行情況, 因此分析問題的時候, 也將變得更加輕而易舉. 為了減少日志代碼, 也可以采用簡單的AOP來做日志輸出. 但是也要小心, 這種做法可能產(chǎn)生大量的日志. 對于這種日志, 一般采用DEBUG/TRACE級別. 當(dāng)某些方法的調(diào)用非常頻繁, 那么大量的日志輸出將會影響到系統(tǒng)的性能, 此時我們可以提高相關(guān)類的日志級別或者干脆去掉日志輸出. 不過一般情況下, 還是建議大家多輸出一些日志. 另外也可以將日志看成一種單元測試. 輸出的日志將像單元測試一樣, 會覆蓋到整個方法的執(zhí)行過程. 沒有日志的系統(tǒng)是不可想象的. 因此通過觀察日志的輸出將是我們了解系統(tǒng)是在正確的運(yùn)行還是掛了的唯一方式. 8) 用日志檢查外部系統(tǒng) 這里是針對前面的一種場景: 如果你的系統(tǒng)需要和其他系統(tǒng)通信, 那么需要考慮是否需要用日志記錄這種交互. 一般情況下, 如果一個應(yīng)用需要與多個系統(tǒng)進(jìn)行集成, 那么診斷和分析問題將非常困難. 比如在最近的一個項(xiàng)目中, 由于我們在Apache CXF web服務(wù)上完整的記錄了消息數(shù)據(jù)(包括SOAP和HTTP頭信息), 使得我們在系統(tǒng)集成和測試階段非常happy. 如果通過ESB的方式來多個系統(tǒng)進(jìn)行集成, 那么可以在總線(bus)上使用日志來記錄請求和響應(yīng). 這里可以參考Mules(http://www./)的<log-component/>(http://www./documentation/display/MULE2USER/Configuring+Components). 有時候與外部系統(tǒng)進(jìn)行通信產(chǎn)生的大量日志可能讓我們無法接受. 另一方面, 我們希望打開日志臨時進(jìn)行一下測試, 或者在系統(tǒng)出現(xiàn)問題的時候我們希望打開短暫的輸出日志. 這樣我們可以在輸出日志和保證系統(tǒng)性能之間取得一個平衡. 這里我們需要借助日志日別. 比如像下面的這樣做: Java代碼
在上面的代碼中, 如果日志級別配置為DEBUG, 那么將但應(yīng)所有的requestIds信息. 但是如果我們配置INFO級別, 那么只會輸出requestId的數(shù)量. 不過就像我們前面提到的日志的副作用那樣, 如果在INFO級別下requestIds為null將產(chǎn)生NullPointerException. 9) 正確輸出異常日志 對于日志輸出的第一條原則就是不要用日志輸出異常, 而是讓框架或者容器去處理. 記錄異常本不應(yīng)該是logger的工作. 而許多程序員都會用日志輸出異常, 然后可能返回一個默認(rèn)值(null, 0或者空字符串). 也可能在將異常包裝一下再拋出. 比如像下面的代碼這樣: Java代碼
這樣做的結(jié)果可能會導(dǎo)致異常信息打印兩次(拋出的地方打一次, 捕獲處理的地方再打一次). 但是有時候我們確實(shí)希望打印異常, 那么應(yīng)該如何處理呢? 比如下面對NPE的處理: Java代碼
上面的代碼, 正確輸出異常信息的只有G和L, A和B甚至不能在SLF4J中編譯通過, 其他的都會丟失異常堆棧信息或者打印了不恰當(dāng)?shù)男畔? 這里只要記住一條, 在日志中輸出異常信息, 第一個參數(shù)一定是一個字符串, 一般都是對問題的描述信息, 而不能是異常message(因?yàn)槎褩@锩鏁?, 第二個參數(shù)才是具體的異常實(shí)例. 注: 對于遠(yuǎn)程調(diào)用類型的服務(wù)拋出的異常,一定要注意實(shí)現(xiàn)序列化, 否則在客戶端將拋出NoClassDefFoundError異常, 而掩蓋了真實(shí)的異常信息 10) 讓日志易讀,易解析 對日志感興趣的可以分為兩類:
日志的內(nèi)容必須照顧到這兩個群體. 引用鮑勃大叔"Clean Code(http://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882)"一書的話來說:日志應(yīng)該像代碼一樣可讀并且容易理解. 另一方面, 如果一個系統(tǒng)每小時要輸出幾百M(fèi)B甚至上G的日志, 那么我們需要借助grep, sed以及awk來分析日志. 如果可能, 我們應(yīng)該讓日志盡可能的被人和機(jī)器理解. 比如, 避免格式化數(shù)字, 使用日志模式則可以方便用正則表達(dá)式進(jìn)行識別. 如果無法兼顧, 那么可以將數(shù)據(jù)用兩種格式輸出, 比如像下面這樣: Java代碼
上面的日志, 既照顧了計(jì)算機(jī)("ms after 1970 epoch"這種時間格式), 又能更好的讓人能理解("1 day 10 hours 17 minutes 36 seconds") . 另外, 這里順便廣告一下DurationFormatUtils(http://commons./lang/api-release/org/apache/commons/lang/time/DateFormatUtils.html), 一個非常不錯的工具:) |
|
|