更多課程 選擇中心
        Java培訓

        400-111-8989

        Java培訓 > Java教程  > 正文

        寫詩一般的 Java 日志

        • 發布:Java培訓
        • 來源:Java教程
        • 時間:2017-09-21 11:32

        工欲善其事,必先利其器

        很多程序員可能都忘了記錄應用程序的行為是一件多么重要的事,當遇到多線程環境下高壓力導致的并發 bug 時,你就能體會到記錄 log 的重要性。

        有的人很高興的就在代碼里加上了這么句:

        log.info("Happy and carefree logging");

        他可能都沒有意識到應用程序的日志在維護,調優和故障識別中的重要性。

        我認為 slf4j 是最好的日志 API,最主要是因為它支持一個很棒的模式注入的方式:

        log.debug("Found {} records matching filter: '{}'", records, filter);

        log4j 的話你只能這樣:

        log.debug("Found " + records + " recordsmatching filter: '" + filter + "'");

        這樣寫不僅更啰嗦和可讀性差,而且字符串拼接影響效率(當這個級別并不需要輸出的時候)。

        slf4j 引入了 {} 注入特性,并且由于避免了每次都進行字符串拼接,toString 方法不會被調用,也不再需要加上 isDebugEnabled 了。

        slf4j 是外觀模式的一種應用,它只是一個門面。具體實現的話我推薦 logback 框架,之前已經做過一次廣告了,而不是已經很完備的 log4j。

        它有許多很有意思的特性,和 log4j 不同的是,它還在積極的開發完善中。

        還有一個要推薦的工具是 perf4j:

        Perf4J is to System.currentTimeMillis() as log4j is to System.out.println()

        就好比 log4j 是 System.out.println 的一種更好的替換方式一樣,perf4j 更像是 System.currentTimeMillis() 的替代。

        我已經在一個項目中引入了 perf4j,并在高負載的情況下觀察它的表現。管理員和企業用戶都被這個小工具提供的漂亮的圖表驚呆了。

        我們可以隨時查看性能問題。perf4j 應該專門開一篇文章來講,現在的話可以先看下它的開發者指南。

        還有一個 Ceki Gülcü(log4j,slf4j 和 logback 工程的創建者)提供了一個簡單的方法供我們移除對 commons-logging 的依賴。

        不要忘了日志級別

        每次你要加一行日志的時候,你都會想,這里該用哪種日志級別?大概有90%的程序員都不太注意這個問題,都是用一個級別來記錄日志,通常不是 INFO 就是 DEBUG。

        為什么?

        日志框架和 System.out 相比有兩大優勢:分類和級別。兩者可以讓你可以選擇性的過濾日志,永久的或者只是在排查錯誤的時候。

        ●ERROR 發生了嚴重的錯誤,必須馬上處理。這種級別的錯誤是任何系統都無法容忍的。比如:空指針異常,數據庫不可用,關鍵路徑的用例無法繼續執行。

        WARN 還會繼續執行后面的流程,但應該引起重視。其實在這里我希望有兩種級別:一個是存在解決方案的明顯的問題(比如,”當前數據不可用,使用緩存數據”),另一個是潛在的問題和建議(比如“程序運行在開發模式下”或者“管理控制臺的密碼不夠安全”)。應用程序可以容忍這些信息,不過它們應該被檢查及修復。

        DEBUG 開發人員關注的事。后面我會講到什么樣的東西應該記錄到這個級別。

        TRACE 更為詳盡的信息,只是開發階段使用。在產品上線之后的一小段時間內你可能還需要關注下這些信息,不過這些日志記錄只是臨時性的,最終應該關掉。DEBUG和TRACE的區別很難區分,不過如果你加了一行日志,在開發測試完后又刪了它的話,這條日志就應該是TRACE級別的。

        上面的列表只是一個建議,你可以根據自己的規則來記錄日志,但最好要有一定的規則。

        我個人的經驗是:在代碼層面不要進行日志過濾,而是用正確的日志級別能夠快速的過濾出想要的信息,這樣能節省你很多時間。

        最后要說的就是這個臭名昭著的 is*Enabled 的條件語句了。有的人喜歡把每次日志前加上這個:

        if(log.isDebugEnabled()) log.debug("Place for your commercial");

        個人認為,應該避免在代碼里加入這個亂哄哄的東西。性能看起來沒有什么提升(尤其是用了 slf4j 之后),更像是過早的優化。

        還有,沒發現這么做有點多余么?很少有時候是明確需要這種顯式的判斷語句的,除非我們證明構造日志消息本身開銷太大。

        不然的話,該怎么記就怎么記,讓日志框架去操心這個吧。

        你清楚你在記錄什么嗎?

        每次你寫下一行日志,花點時間看看你到底在日志文件里打印了些什么。讀一遍你的日志,找出異常的地方。首先,至少要避免空指針異常:

        log.debug("Processing request with id: {}", request.getId());

        你確認過 request 不是 null 了嗎?

        記錄集合也是一個大坑。如果你用 Hibernate 從數據庫里獲取領域對象的集合的時候,不小心寫成了這樣:

        log.debug("Returning users: {}", users);

        slf4j 只會在這條語句確實會打印的時候調用 toString 方法,當然這個很酷。

        不過如果內存溢出了,N+1 選擇問題,線程餓死,延遲初始化異常,日志存儲空間用完了…這些都有可能發生。

        最好的方式是只記錄對象的 ID(或者只記錄集合的大小)。

        不過收集ID需要對每個對象調用 getId 方法,這個在 Java 里可真不是件簡單的事。

        Groovy 有個很棒的展開操作符(users*.id),在 Java 里我們可以用 Commons Beanutils 庫來模擬下:

        log.debug("Returning user ids: {}", collect(users, "id"));

        collect 方法大概是這么實現的:

        public static Collection collect(Collection collection, String propertyName) { return CollectionUtils.collect(collection, new BeanToPropertyValueTransformer(propertyName));}

        最后要說的是,toString 方法可能沒有正確的實現或者使用。

        首先,為了記錄日志,為每個類創建一個 toString 的做法比比皆是,最好用 ToStringBuilder 來生成(不過不是它的反射實現的那個版本)。

        第二,注意數組和非典型的集合。

        數組和一些另類的集合的 toString 實現可能沒有挨個調用每個元素的 toString 方法。

        可以使用JDK提供的Arrays#deepToString方法。經常檢查一下你自己打印的日志,看有沒有格式異常的一些信息。

        避免副作用

        日志打印一般對程序的性能沒有太大影響。最近我一個朋友在一些特殊的平臺上運行的一個系統拋出了 Hibernate 的 LazyInitializationException 異常。

        你可能從這已經猜到了,當會話連接進來的時候,一些日志打印導致延遲初始化的集合被加載。

        在這種情況下,把日志級別提高了,集合也就不再被初始化了。

        如果你不知道這些上下文信息,你得花多長時間來發現這個 BUG。

        另一個副作用就是影響程序的運行速度。

        快速回答一下這個問題:

        如果日志打印的過多或者沒有正確的使用 toString 和字符串拼接,日志打印就會對性能產生負面影響。能有多大?

        好吧,我曾經見過一個程序每15分鐘就重啟一次,因為太多的日志導致的線程餓死。這就是副作用!從我的經驗來看,一小時打印百來兆差不多就是上限了。

        當然如果由于日志打印異常導致的業務進程中止,這個副作用就大了。我經常見到有人為了避免這個而這么寫:

        try {

        log.trace("Id=" + request.getUser().getId() + " accesses " + manager.getPage().getUrl().toString())

        } catch(NullPointerException e) {}

        這是段真實的代碼,但是為了讓世界清凈點,請不要這么寫。

        描述要清晰

        每個日志記錄都會包含數據和描述。看下這個例子:

        log.debug("Message processed");log.debug(message.getJMSMessageID());log.debug("Message with id '{}' processed", message.getJMSMessageID());

        當在一個陌生的系統里排查錯誤的時候,你更希望看到哪種日志?相信我,上面這些例子都很常見。還有一個反面模式:

        if(message instanceof TextMessage) //...else log.warn("Unknown message type");

        在這個警告日志里加上消息類型,消息 ID 等等這些難道很困難嗎?我是知道發生錯誤了,不過到底是什么錯誤?上下文信息是什么?

        第三個反面例子是“魔法日志”。

        一個真實的例子:團隊里的很多程序員都知道,3個&號后面跟著!號再跟著一個#號,再跟著一個偽隨機數的日志意味著”ID為XYZ的消息收到了”。沒人愿意改這個日志,某人敲下鍵盤,選中某個唯一的”&&&!#”字符串,他就能很快找到想要的信息。

        結果是,整個日志文件看起來像一大串隨機字符。有人不禁會懷疑這是不是一個perl程序。

        日志文件應當是可讀性強的,清晰的,自描述的。不要用一些魔數,記錄值,數字,ID還有它們的上下文。記錄處理的數據以及它的含義。記錄程序正在干些什么。好的日志應該是程序代碼的一份好的文檔。

        我有提過不要打印密碼還有個人信息嗎?相信沒有這么傻的程序員。

        調整你的格式

        日志格式是個很有用的工具,無形中在日志添加了很有價值的上下文信息。不過你應該想清楚,在你的格式中包含什么樣的信息。

        比如說,在每小時循環寫入的日志中記錄日期是沒有意義的,因為你的日志名就已經包含了這個信息。

        相反的,如果你沒記錄線程名的話當兩個線程并行的工作的時候,你就無法通過日志跟蹤線程了——日志已經重疊到一起了。

        在單線程的應用程序中,這樣做沒問題,不過那個已經是過去的事兒了。

        從我的經驗來看,理想的日志格式應當包括(當然除了日志信息本身了):當前時間(無日期,毫秒級精度),日志級別,線程名,簡單的日志名稱(不用全稱)還有消息。在logback里會是這樣的:

        寫詩一般的Java日志

        文件名,類名,行號,都不用列進來,盡管它們看起來很有用。我還在代碼里見過空的日志記錄:

        log.info(“”);

        因為程序員認為行號會作為日志格式的一部分,并且他知道如果空日志消息出現在這個文件的67行的話,意味著這個用戶是認證過的。不僅這樣,記錄類名方法名,或者行號對性能都有很大的影響。

        日志框架的一個比較高級的特性是診斷上下文映射(Mapped Diagnostic Context)。MDC只是一個線程本地的一個map。

        你可以把任何鍵值對放到這個map里,這樣的話這個線程的所有日志記錄都能從這個map里取到相應的信息作為輸出格式的一部分。

        記錄方法的參數和返回值

        如果你在開發階段發現了一個BUG,你通常會用調試器來跟蹤具體的原因。現在假設不讓你用調試器了,比如,因為這個BUG幾天前在用戶的環境里出現了,你能拿到的只有一些日志。你能從中發現些什么?

        如果你遵循打印每個方法的入參和出參這個簡單的原則,你根本不需要調試器。當然每個方法可能訪問外部系統,阻塞,等待,等等,這些都應該考慮進來。就參考以下這個格式就好:

        寫詩一般的Java日志

        由于你在方法的開始和結束都記錄了日志,所以你可以人工找出效率不高的代碼,甚至還可以檢測到可能會引起死鎖和饑餓的誘因——你只需看一下“Entering”后面是不是沒有”Leaving“就明白了。

        如果你的方法名的含義很清晰,清日志將是一件愉快的事情。同樣的,分析異常也更得更簡單了,因為你知道每一步都在干些什么。

        代碼里要記錄的方法很多的話,可以用 AOP 切面來完成。這樣減少了重復的代碼,不過使用它得特別小心,不注意的話可能會導致輸出大量的日志。

        這種日志最合適的級別就是 DEBUG和TRACE 了。

        如果你發現某個方法調用 的太頻繁,記錄它的日志可能會影響性能的話,只需要調低它的日志級別就可以了,或者把日志直接刪了(或者整個方法調用只留一個?)不過日志多了總比少了要強。

        把日志記錄當成單元測試來看,你的代碼應該布滿了日志就像它的單元測試到處都是一樣。

        系統沒有任何一部分是完全不需要日志的。記住,有時候要知道你的系統是不是正常工作,你只能查看不斷刷屏的日志。

        觀察外部系統

        這條建議和前面的有些不同:如果你和一個外部系統通信的話,記得記錄下你的系統傳出和讀入的數據。

        系統集成是一件苦差事,而診斷兩個應用間的問題(想像下不同的公司,環境,技術團隊)尤其困難。

        最近我們發現記錄完整的消息內容,包括 Apache CXF 的 SOAP 和 HTTP 頭,在系統的集成和測試階段非常有效。

        這樣做開銷很大,如果影響到了性能的話,你只能把日志關了。

        不過這樣你的系統可能跑的很快,掛的也很快,你還無能為力?

        當和外部系統進行集成的時候,你只能格外小心并做好犧牲一定開銷的準備。

        如果你運氣夠好,系統集成由 ESB 處理了,那在總線把請求和響應給記錄下來就最好不過了。

        可以參考下 Mule 的這個日志組件。

        有時候和外部系統交換的數據量決定了你不可能什么都記下來。

        另一方面,在測試階段和發布初期,最好把所有東西都記到日志里,做好犧牲性能的準備。可以通過調整日志級別來完成這個。

        看下下面這個小技巧:

        寫詩一般的Java日志

        如果這個 logger 是配置成 DEBUG 級別,它會打印完整的請求 ID 的集合。

        如果它配置成了打印 INFO 信息的話,就只會輸出集合的大小。

        你可能會問我是不是忘了 isInfoEnabled 條件了,看下第二點建議吧。

        這里還有一個值得注意的是 ID 的集合不能為 null。

        盡管在 DEBUG 下,它為 NULL 也能正常打印,但是當配置成 INFO 的時候一個大大的空指針。

        還記得第4點建議中提到的副作用吧?

        正確的記錄異常

        首先,不要記錄異常,讓框架或者容器來干這個。

        當然有一個例外:如果你從遠程服務中拋出了異常(RMI,EJB等),異常會被序列化,確保它們能返回給客戶端 (API中的一部分)。

        不然的話,客戶端會收到 NoClassDefFoundError 或者別的古怪的異常,而不是真正的錯誤信息。

        異常記錄是日志記錄的最重要的職責之一,不過很多程序員都傾向于把記錄日志當作處理異常的方式。

        他們通常只是返回默認值(一般是 null,0或者空字符串),裝作什么也沒發生一樣。

        還有的時候,他們會先記錄異常,然后把異常包裝了下再拋出去:

        log.error("IO exception", e);

        throw new CustomException(e);

        這樣寫通常會把棧信息打印兩次,因為捕獲了 MyCustomException 異常的地方也會再打印一次。

        日志記錄,或者包裝后再拋出去,不要同時使用,否則你的日志看起來會讓人很迷惑。

        如果我們真的想記錄日志呢?由于某些原因(大概是不讀 API 和文檔?),大約有一半的日志記錄我認為是錯誤的。

        做個小測試,下面哪個日志語句能夠正確的打印空指針異常?

        try {

        Integer x = null;

        ++x;

        } catch (Exception e) { log.error(e); //A

        log.error(e, e); //B

        log.error("" + e); //C

        log.error(e.toString()); //D

        log.error(e.getMessage()); //E

        log.error(null, e); //F

        log.error("", e); //G

        log.error("{}", e); //H

        log.error("{}", e.getMessage()); //I

        log.error("Error reading configuration file: " + e); //J

        log.error("Error reading configuration file: " + e.getMessage()); //K log.error("Error reading configuration file", e); //L}

        很奇怪吧,只有 G 和 L(這個更好)是對的!

        A 和 B 在 slf4j 下面根本就編譯不過,其它的會把棧跟蹤信息給丟掉了或者打印了不正確的信息。

        比如,E 什么也不打印,因為空指針異常本身沒有提供任何異常信息而棧信息又沒打印出來。

        記住,第一個參數通常都是文本信息,關于這個錯誤本身的。

        不要把異常信息給寫進來,打印日志后它會自動出來的,在棧信息的前面。

        不過想要打印這個,你當然還得把異常傳到第二個參數里面才行。

        日志應當可讀性強且易于解析

        現在有兩組用戶對你的日志感興趣:我們人類(不管你同不同意,碼農也是在這里邊),還有計算機(通常就是系統管理員寫的 shell 腳本)。

        日志應當適合這兩種用戶來理解。如果有人在你后邊看你的程序的日志卻看到了這個:

        寫詩一般的Java日志

        那你肯定沒聽從我的建議。日志應該像代碼一樣易于閱讀和理解。

        另一方面,如果你的程序每小時就生成了半 GB 的日志,沒有誰或者任何圖形化的文本編輯器能把它們看完。

        這時候我們的老家伙們,grep,sed 和 awk 這些上場的時候就來了。

        如果有可能的話,你記錄的日志最好能讓人和計算機都能看明白 ,不要將數字格式化,用一些能讓正則容易匹配的格式等等。

        如果不可能的,用兩個格式來打印數據:

        log.debug("Request TTL set to: {} ({})", new Date(ttl), ttl);// Request TTL set to: Wed Apr 28 20:14:12 CEST 2010 (1272478452437)final String duration = DurationFormatUtils.formatDurationWords(durationMillis, true, true);log.info("Importing took: {}ms ({})", durationMillis, duration);//Importing took: 123456789ms (1 day 10 hours 17 minutes 36 seconds)

        計算機看到“ms after 1970 epoch”這樣的的時間格式會感謝你的,而人們則樂于看到“1天10小時17分36秒”這樣的東西。

        總之,如果你愿意琢磨的話,日志也可以寫得像詩一樣優雅。

         感謝大家閱讀由java培訓機構分享的“寫詩一般的Java日志”希望對大家有所幫助,更多精彩內容請關注Java培訓官網、

        免責聲明:本文由小編轉載自網絡,旨在分享提供閱讀,版權歸原作者所有,如有侵權請聯系我們進行刪除

        預約申請免費試聽課

        填寫下面表單即可預約申請免費試聽!怕錢不夠?可就業掙錢后再付學費! 怕學不會?助教全程陪讀,隨時解惑!擔心就業?一地學習,可全國推薦就業!

        上一篇:Java 中的異常和處理詳解
        下一篇:Java序列化的幾種方式以及序列化的作用
        12大要點讓你的Java開發所向披靡~

        12大要點讓你的Java開發所向披靡~

        學習Java最好的12本免費在線電子書

        學習Java最好的12本免費在線電子書

        Java常用日志框架介紹

        Java常用日志框架介紹

        一篇文章了解RPC框架原理

        一篇文章了解RPC框架原理

        • 掃碼領取資料

          回復關鍵字:視頻資料

          免費領取 達內課程視頻學習資料

        • 視頻學習QQ群

          添加QQ群:1143617948

          免費領取達內課程視頻學習資料

        Copyright ? 2021 Tedu.cn All Rights Reserved 京ICP備08000853號-56 京公網安備 11010802029508號 達內時代科技集團有限公司 版權所有

        選擇城市和中心
        貴州省

        福建省

        • 達內廈門軟件園中心
        廣西省

        海南省

        国产高清情侣视频2019年,限制电影福利在线观看,23伊人大香蕉 百度 好搜 搜狗
        <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <文本链> <文本链> <文本链> <文本链> <文本链> <文本链>