Java 如何正確地輸出日誌

蔣老溼發表於2019-03-04

Java 如何正確地輸出日誌

什麼是日誌

簡單的說,日誌就是記錄程式的執行軌跡,方便查詢關鍵資訊,也方便快速定位解決問題。

我們 Java 程式設計師在開發專案時都是依賴 Eclipse/ Idea 等開發工具的 Debug 除錯功能來跟蹤解決 Bug,在開發環境可以這麼做,但專案釋出到了測試、生產環境呢?你有可能會說可以使用遠端除錯,但實際並不能允許讓你這麼做。

所以,日誌的作用就是在測試、生產環境沒有 Debug 除錯工具時開發、測試人員定位問題的手段。日誌打得好,就能根據日誌的軌跡快速定位並解決線上問題,反之,日誌輸出不好不能定位到問題不說反而會影響系統的效能。

優秀的專案都是能根據日誌定位問題的,而不是線上除錯,或者半天找不到有用的日誌而抓狂…

常用日誌框架

log4j、Logging、commons-logging、slf4j、logback,開發的同學對這幾個日誌相關的技術不陌生吧,為什麼有這麼多日誌技術,它們都是什麼區別和聯絡呢?相信大多數人搞不清楚它們的關係,下面我將一一介紹一下,以後大家再也不用傻傻分不清楚了。

Logging

如圖所示,這是 Java 自帶的日誌工具類,在 JDK 1.5 開始就已經有了,在 java.util.logging 包下。

Java 如何正確地輸出日誌

更多關於 Java Logging 的介紹可以看官方文件

Log4j

Log4j 是 Apache 的一個開源日誌框架,也是市場佔有率最多的一個框架。大多數沒用過 Java Logging, 但沒人敢說沒用過 Log4j 吧,反正從我接觸 Java 開始就是這種情況,做 Java 專案必有 Log4j 日誌框架。

注意:log4j 在 2015/08/05 這一天被 Apache 宣佈停止維護了,使用者需要切換到 Log4j2上面去。

下面是官方宣佈原文:

On August 5, 2015 the Logging Services Project Management Committee announced that Log4j 1.x had reached end of life. For complete text of the announcement please see the Apache Blog. Users of Log4j 1 are recommended to upgrade to Apache Log4j 2.
複製程式碼

Log4j2 的官方地址

commons-logging

上面介紹的 log4j 是一個具體的日誌框架的實現,而 commons-logging 就是日誌的門面介面,它也是 apache 最早提供的日誌門面介面,使用者可以根據喜好選擇不同的日誌實現框架,而不必改動日誌定義,這就是日誌門面的好處,符合面對介面抽象程式設計。

更多的詳細說明可以參考官方說明

Slf4j

全稱:Simple Logging Facade for Java,即簡單日誌門面介面,和 Apache 的 commons-logging 是一樣的概念,它們都不是具體的日誌框架,你可以指定其他主流的日誌實現框架。

Slf4j 的官方地址

Slf4j 也是現在主流的日誌門面框架,使用 Slf4j 可以很靈活的使用佔位符進行引數佔位,簡化程式碼,擁有更好的可讀性,這個後面會講到。

Logback

Logback 是 Slf4j 的原生實現框架,同樣也是出自 Log4j 一個人之手,但擁有比 log4j 更多的優點、特性和更做強的效能,現在基本都用來代替 log4j 成為主流。

Logback 的官方地址

為什麼 Logback 會成為主流?

無論從設計上還是實現上,Logback相對log4j而言有了相對多的改進。不過儘管難以一一細數,這裡還是列舉部分理由為什麼選擇logback而不是log4j。牢記logback與log4j在概念上面是很相似的,它們都是有同一群開發者建立。所以如果你已經對log4j很熟悉,你也可以很快上手logback。如果你喜歡使用log4j,你也許會迷上使用logback。

更快的執行速度

基於我們先前在log4j上的工作,logback 重寫了內部的實現,在某些特定的場景上面,甚至可以比之前的速度快上10倍。在保證logback的元件更加快速的同時,同時所需的記憶體更加少。

更多請參考《從Log4j遷移到LogBack的理由 》

日誌框架總結

commons-loggin、slf4j 只是一種日誌抽象門面,不是具體的日誌框架。 log4j、logback 是具體的日誌實現框架。 一般首選強烈推薦使用 slf4j + logback。當然也可以使用slf4j + log4j、commons-logging + log4j 這兩種日誌組合框架。

Java 如何正確地輸出日誌

從上圖可以看出 slf4j 很強大吧,不但能和各種日誌框架對接,還能和日誌門面 commons-logging 進行融合。

日誌級別詳解

日誌的輸出都是分級別的,不同的設定不同的場合列印不同的日誌。下面拿最普遍用的 Log4j 日誌框架來做個日誌級別的說明,這個也比較奇全,其他的日誌框架也都大同小異。

Log4j 的級別類 org.apache.log4j.Level 裡面定義了日誌級別,日誌輸出優先順序由高到底分別為以下8種。

Java 如何正確地輸出日誌

日誌級別 描述
OFF 關閉:最高階別,不輸出日誌。
FATAL 致命:輸出非常嚴重的可能會導致應用程式終止的錯誤。
ERROR 錯誤:輸出錯誤,但應用還能繼續執行。
WARN 警告:輸出可能潛在的危險狀況。
INFO 資訊:輸出應用執行過程的詳細資訊。
DEBUG 除錯:輸出更細緻的對除錯應用有用的資訊。
TRACE 跟蹤:輸出更細緻的程式執行軌跡。
ALL 所有:輸出所有級別資訊。

所以,日誌優先順序別標準順序為:

ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF

如果日誌設定為 L ,一個級別為 P 的輸出日誌只有當 P >= L 時日誌才會輸出。

即如果日誌級別 L 設定 INFO,只有 P 的輸出級別為 INFO、WARN,後面的日誌才會正常輸出。

具體的輸出關係可以參考下圖:

Java 如何正確地輸出日誌

知道了日誌級別,這還只是基礎,如何瞭解打日誌的規範,以及如何正確地打日誌姿勢呢?!

Java 如何正確地輸出日誌

打日誌的規範準則

最開始也說過了,日誌不能亂打,不然起不到日誌本應該起到的作用不說,還會造成系統的負擔。在 BAT、華為一些大公司都是對日誌規範有要求的,什麼時候該打什麼日誌都是有規範的。

阿里去年釋出的《Java 開發手冊》,裡面有一章節就是關於日誌規範的,讓我們再來回顧下都有什麼內容。

下面是阿里的《Java開發手冊》終極版日誌規約篇。

Java 如何正確地輸出日誌

規範有很多,這裡就不再一一詳述了,完整終極版可以在微信公眾號 "Java 技術棧" 中回覆 "手冊" 獲取。這裡只想告訴大家,在大公司打日誌都是有嚴格規範的,不是你隨便打就行的。

阿里是一線網際網路公司,所制定的日誌規範也都符合我們的要求,很有參考意義,能把阿里這套日誌規約普及也真很不錯了。

專案中該如何正確的打日誌?

  1. 正確的定義日誌 private static final Logger LOG = LoggerFactory.getLogger(this.getClass()); 通常一個類只有一個 LOG 物件,如果有父類可以將 LOG 定義在父類中。
    日誌變數型別定義為門面介面(如 slf4j 的 Logger),實現類可以是 Log4j、Logback 等日誌實現框架,不要把實現類定義為變數型別,否則日誌切換不方便,也不符合抽象程式設計思想。

  2. 使用引數化形式{}佔位,[] 進行引數隔離 LOG.debug("Save order with order no:[{}], and order amount:[{}]"); 這種可讀性好,這樣一看就知道[]裡面是輸出的動態引數,{}用來佔位類似繫結變數,而且只有真正準備列印的時候才會處理引數,方便定位問題。
    如果日誌框架不支援引數化形式,且日誌輸出時不支援該日誌級別時會導致物件冗餘建立,浪費記憶體,此時就需要使用 isXXEnabled 判斷,如:

    if(LOG.isDebugEnabled()){
        // 如果日誌不支援引數化形式,debug又沒開啟,那字串拼接就是無用的程式碼拼接,影響系統效能
        logger.debug("Save order with order no:" + orderNo + ", and order amount:" + orderAmount);
    }
    複製程式碼

    至少 debug 級別是需要開啟判斷的,線上日誌級別至少應該是 info 以上的。 這裡推薦大家用 SLF4J 的門面介面,可以用引數化形式輸出日誌,debug 級別也不必用 if 判斷,簡化程式碼。

  3. 輸出不同級別的日誌,專案中最常用有日誌級別是ERROR、WARN、INFO、DEBUG四種了,這四個都有怎樣的應用場景呢。

    ERROR(錯誤) 一般用來記錄程式中發生的任何異常錯誤資訊(Throwable),或者是記錄業務邏輯出錯。
    WARN(警告) 一般用來記錄一些使用者輸入引數錯誤、
    INFO(資訊) 這個也是平時用的最低的,也是預設的日誌級別,用來記錄程式執行中的一些有用的資訊。如程式執行開始、結束、耗時、重要引數等資訊,需要注意有選擇性的有意義的輸出,到時候自己找問題看一堆日誌卻找不到關鍵日誌就沒意義了。
    DEBUG(除錯) 這個級別一般記錄一些執行中的中間引數資訊,只允許在開發環境開啟,選擇性在測試環境開啟。

幾個錯誤的打日誌方式

  1. 不要使用 System.out.print.. 輸出日誌的時候只能通過日誌框架來輸出日誌,而不能使用 System.out.print.. 來列印日誌,這個只會列印到 tomcat 控制檯,而不會記錄到日誌檔案中,不方便管理日誌,如果通過服務形式啟動把日誌丟棄了那更是找不到日誌了。

  2. 不要使用 e.printStackTrace() 首先來看看它的原始碼:

    public void printStackTrace() {
        printStackTrace(System.err);
    }
    複製程式碼

    它其實也是利用 System.err 輸出到了 tomcat 控制檯。

  3. 不要丟擲異常後又輸出日誌 如捕獲異常後又丟擲了自定義業務異常,此時無需記錄錯誤日誌,由最終捕獲方進行異常處理。不能又丟擲異常,又列印錯誤日誌,不然會造成重複輸出日誌。

    try {
        // ...
    } catch (Exception e) {
        // 錯誤
        LOG.error("xxx", e);
        throw new RuntimeException();
    }
    複製程式碼
  4. 不要使用具體的日誌實現類 InterfaceImpl interface = new InterfaceImpl(); 這段程式碼大家都看得懂吧?應該面向介面的物件程式設計,而不是面向實現,這也是軟體設計模式的原則,正確的做法應該是。

    Interface interface = new InterfaceImpl(); 日誌框架裡面也是如此,上面也說了,日誌有門面介面,有具體實現的實現框架,所以大家不要面向實現程式設計。

  5. 沒有輸出全部錯誤資訊 看以下程式碼,這樣不會記錄詳細的堆疊異常資訊,只會記錄錯誤基本描述資訊,不利於排查問題。

    try {
        // ...
    } catch (Exception e) {
        // 錯誤
        LOG.error('XX 發生異常', e.getMessage());
        // 正確
        LOG.error('XX 發生異常', e);
    }   
    複製程式碼
  6. 不要使用錯誤的日誌級別 曾經線上上定位一個問題,同事自信地和我說:明明輸出了日誌啊,為什麼找不到...,後來我去看了下他的程式碼,是這樣的:

    try {
        // ...
    } catch (Exception e) {
        // 錯誤
        LOG.info("XX 發生異常...", e);
    }
    複製程式碼

    大家看出了什麼問題嗎?用 info 記錄 error 日誌,日誌輸出到了 info 日誌檔案中了,同事拼命地在 error 錯誤日誌檔案裡面找怎麼能找到呢?

  7. 不要在千層迴圈中列印日誌 這個是什麼意思,如果你的框架使用了效能不高的 Log4j 框架,那就不要在上千個 for 迴圈中列印日誌,這樣可能會拖垮你的應用程式,如果你的程式響應時間變慢,那要考慮是不是日誌列印的過多了。

    for(int i=0; i<2000; i++){
        LOG.info("XX");
    }
    複製程式碼

    最好的辦法是在迴圈中記錄要點,在迴圈外面總結列印出來。

  8. 禁止線上上環境開啟 debug 這是最後一點,也是最重要的一點。 一是因為專案本身 debug 日誌太多,二是各種框架中也大量使用 debug 的日誌,線上開啟 debug 不久就會打滿磁碟,影響業務系統的正常執行。

相關文章