哪來這麼多日誌程式庫?


iThome 網站首載:哪來這麼多日誌程式 庫?

剛寫 Java 的開發者在需要日誌時,應該會知道有 java.util.logging(簡稱JUL),接下來使用 某些開放原始碼專案,在應用程式啟動 時,可能會看到日誌訊息抱怨找不到 log4j.properties,撰寫程式碼時卻是使用 commons-logging 的 API,後來使用其他框 架,發現它使用的是 SLF4J 的 API、設定檔名稱是 logback.xml,有天連到了 Log4j 官網,竟然又看到了 Log4j2?

混亂的歷史

就現今語言來說,本身內建日誌程式庫是個基本需求,一般來說,總是建議使用標準 API,避免對第三方程式庫的依賴,然而在 Java 這塊談到內建的日誌 程式庫,稍有經驗的開發者總是搖搖頭,過去有段很長的時候,他們會告訴你「使用 Log4j」。

Java標準程式庫中一開始並沒有 JUL,在第三方程式庫的實現中,Ceki Gülcü 創建的 Log4j 受到廣泛使用,雖然如此,當時 Java 擁有者的 Sun 並不接受 Log4j,而在 JDK1.4 中放入了 JUL,然而因為功能比 Log4j 簡單,而且一開始又有著效能不彰等問題(在 JDK1.5 才有了改善),開發者依然建議使用 Log4j。

無論如何,Java 界現在有了受歡迎的 Log4j 與標準內建的 JUL,總會遇到要做出選擇及處理不同日誌程式庫的問題,當時出現的 Jakarta Commons Logging(後來改名為 Apache Commons Logging)定義了一層介面(本身有個簡易 SimpleLog 實現),可以動態地綁定 JUL 或者是 Log4j 作為日誌實現,開發者使用 commons-logging 的 API,不需要依賴在 JUL 或者是 Log4j 的程式碼。

commons-logging 的動態綁定有著規則複雜等問題,Log4j 本身也有些效能議題,Log4j 的創建者 Ceki Gülcü 後來開發了 SLF4J,全名 Simple Logging Facade for Java,顧名思義,SLF4J 也是定義一組介面,實作上可以綁定 Log4j、JUL 或者是 Ceki Gülcü 實現的 Logback。SLF4J 也提供了橋接套件,只要使用對應的 JAR,就可以替換掉 Log4j、JUL或 commons-logging,然而現有的程式 碼不用更改,底層實際上卻使用 SLF4J。

這一切 Log4J 的維護團隊看在眼裏,他們吸收了 SLF4J、Logback 的經驗與優點,在 2014 年正式推出了 Log4j2,也採取了介面與實現 分離的設計(分為 Log4j2 API 與 Log4j2 Core),在效能上有很大的提昇,還提供了非同步日誌等功能;Log4j 於 2012 年發佈最後一個版本之後,也在 2015 年正式宣布终止。

認識實作品

日誌程式庫在實作品的設計上,基本概念是類似的,例如 Log4j 中的 LoggerAppenderLayout 等概念,在 JUL 中換成了 LoggerHandlerFormatter 之類的角色,這部份可參考先前專欄〈從日誌 API 認識日誌需求〉;認識 LoggerAppender 等概念以及之間的組合關係,主要是知道如何在日誌設定檔中組合出應用程式的日誌需求,而不是在程式碼中調整,也因此,認識日誌實作品 的重要任務之一,就是知道如何撰寫設定檔,以及是否可彈性地選擇設定檔。

JUL 最令人詬病的就是設定檔只能是 .properties,而且只能透過 java.util.logging.config.file 指定 .properties 的位置。Log4j、Logback、Log4j2 Core 都可以有不同的設定檔格式,像是 .properties、XML、Groovy(Logback)或JSON(Log4j2,然而 Log4j2 不支 援 .properties)等,也可以有多個設定檔來源。可以依賴在介面的話,就不要依賴在實作,因此,程式碼撰寫上建議使用 commons- logging、SLF4J 或者是 Log4j2 API,透過設定檔等方式來決定使用哪個實作品。

commons-logging 在尋找實作品上,有著一套複雜的規則,才能決定最後要使用 Log4j、JUL或者自身的SimpleLog(輸 出至 System.err),由於其動態綁定是基於 ClassLoader 實現,在自定義 ClassLoader 的環境中,會引發 NoClassDefFoundError 等問題(詳見 Ceki Gülcü 的文件)。

SLF4 採用靜態綁定的方式,具體來說,類別路徑下只能有一個對應的 SLF4J 綁定(SLF4J bindings),例如,若底層想要用 Log4j,可以使用 slf4j-log4j12 的 JAR 檔案,各個綁定 JAR 中都有 org.slf4j.impl.StaticLoggerBinder 綁定實作品,如果想更換日誌實作,只要更換 JAR 檔案。

至於在不修改程式碼下,就可以將既有的日誌輸出橋接至 SLF4 的方式,則是使用直接實作了 Log4j、JUL 或 commons-logging 的 JAR 檔案來替換,例如,想將原本使用 commons-logging 的日誌橋接至 SLF4J,在最簡單的情況下,只要將 commons- logging.jar 替換為 jcl-over-slf4j.jar 就可以了。

如果不想修改程式碼,又想要令使用 Log4j 的應用程式可以獲得 Log4j2 Core 的效能改進,可以使用 Log4j2 的 log4j-1.2-api.jar 將 log4j 的 JAR 替換掉(以 及額外的三點要求)。

API上的著墨

在撰寫程式碼進行輸出日誌時,基本上都是透過一個工廠類別的靜態方法取得 Logger 實例,然後呼叫對應的等級方法進行輸出。不過,還是有些可以留意 的部份。例如 Ceki Gülcü 認為,有些日誌 API 容易讓開發者寫出錯誤的日誌輸出,像是 commons-logging 的 warninfo 等 方法接受的是 Object,開發者可能隨意傳 入物件,因而容易忘了思考與定義該輸出的日誌訊息為何,SLF4J 的 warninfo 等方法強制要傳入 String 物件來避免這類問題。

在〈LOGBack: Evolving Java Logging〉中 Ceki Gülcü 也談到,有些日誌程式庫在使用上,可能會使用字串串接方式組成訊息,例如:

if(logger.isDebugEnabled()) {
    logger.debug("User with account " + user.getAccount() +
    " failed authentication; supplied crypted password " + user.crypt(password) +
    " does not match." );
}


可以看出字串串接上極為麻煩,而為了避免字串串接上的開銷,必須使用 isDebugEnabled() 來判斷,SLF4J 可以採用字串模版的方式來解 決:

logger.debug(
    "User with account {} failed authentication; supplied crypted password {} does not match.",
    user.getAccount(), user.crypt(password));


這樣的設計也被 Log4j2 吸收了;至於 JDK 本身的 JUL,儘管常被建議不要使用,實際上還是有在進行改善,如果日誌動作是比較消耗資源的話,在 JDK8 中可以撰寫:

logger.debug(() -> expensive());

這帶入了 Lambda 語法,讓語法簡潔了許多,而實際上 expensive() 只會在層級為 Debug 時才會執行,不必特別使用 isLoggable() 判斷,也避免了不必要的開銷。

認識日誌程式庫歴史

從歷史的發展時間來看,這邊談到的日誌程式庫大致上的順序為 Log4j -> JUL -> [commons-logging] -> [SLF4J]/LogBack -> [Log4j2 API]/Log4j2 Core,[] 部份表示介面定義,可以的話,使用日誌時應該透過這些介面,而 SLF4J 看來可以替代 commons-logging,它的橋接設計,可以靈活地替換各種實 作品,也可以橋接至Log4j2,無論是直接使用,用來整合多個日誌方案,或者用來逐步改造既有程式的日誌,會是個不錯選擇。

然而,實際的情況可能更複雜,好好檢視日誌程式庫的發展歷史是必要的,網路上雖然也有許多的比較文章,然而,試著自行疏理、動手實作或者改造幾個簡單 專案的日誌,會更能瞭解這些程式庫之間錯綜複雜的關係,以便從中做出更好的選擇。