剛寫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中的Logger、Appender、Layout等概念,在JUL中換成了Logger、Handler、Formatter之類的角色,這部份可參考先前專欄〈從日誌API認識日誌需求〉;認識Logger、Appender等概念,以及它們之間的組合關係,主要是知道如何在日誌設定檔中,組合出應用程式的日誌需求,而不是在程式碼中調整,也因此,認識日誌實作品的重要任務之一,就是知道如何撰寫設定檔,以及是否可彈性地選擇設定檔。

不過,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ü的文件:https://goo.gl/e2CTRN)。

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替換掉(以及額外的三點要求,詳見https://goo.gl/CG5cjb)。

API上的著墨

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

在〈LOGBack: Evolving Java Logging〉(https://goo.gl/CqtbJj)中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,無論是直接使用,用來整合多個日誌方案,或者用來逐步改造既有程式的日誌,會是個不錯選擇。

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

專欄作者

熱門新聞

Advertisement