自從七月份去走川藏后,已經(jīng)好幾個(gè)月沒(méi)有更新博客了。其實(shí)八月底從拉薩回來(lái)后一直在Spring的代碼,也想寫(xiě)幾篇關(guān)于Spring源碼的解讀,可惜Spring實(shí)在是太復(fù)雜了,花了我一個(gè)多月的時(shí)間,框架大體流程是有頭緒了,但是具體實(shí)現(xiàn)和各個(gè)模塊的具體細(xì)節(jié)還都不是很清楚,遲遲不敢動(dòng)筆。其實(shí)原本我不想回到Logging這一塊,我知道光看完Log4J的代碼還不夠,也感覺(jué)Log在系統(tǒng)中其實(shí)占據(jù)了蠻重要的位置(雖然很多人都沒(méi)有意識(shí)到),不過(guò)一般Log框架使用簡(jiǎn)單,遇到問(wèn)題也比較少,即使看完源碼對(duì)實(shí)際工作也幫助不大。可惜我前段時(shí)間答應(yīng)要在公司講一個(gè)關(guān)于Logging相關(guān)的Session了,木有辦法,只能重拾Logging的內(nèi)容。也趁這個(gè)機(jī)會(huì)把Java的Logging相關(guān)框架做一個(gè)全面的了解,包括Commons Logging、SLF4J、JDK Logging、LogBack。首先從Commons Logging和SLF4J的比較開(kāi)始。
先來(lái)隨便扯點(diǎn)吧,貌似所有這些流行的Logging框架都和Log4J多少有點(diǎn)關(guān)系(不太確定Commons Logging有多大關(guān)系,不過(guò)至少也都是Apache下的項(xiàng)目吧)。JDK Logging據(jù)說(shuō)當(dāng)初是想用Log4J的,但是當(dāng)時(shí)兩家好像談判談崩了,然后JDK自己實(shí)現(xiàn)了一個(gè),貌似結(jié)構(gòu)和Log4J差不多,只是實(shí)現(xiàn)的比較爛,基本上也只能在做測(cè)試的時(shí)候用,而SLF4J和LogBack都是出自Log4J的創(chuàng)始人Ceki Gülcü之手。這家伙也算是閑的蛋疼,光整Logging這些框架貌似就花了不少時(shí)間吧。
言歸正傳,在Logging系統(tǒng)中,目前框架都是基于相同的設(shè)計(jì),即從一個(gè)LogFactory中取得一個(gè)命名的Log(Logger)實(shí)例,然后使用這個(gè)Log(Logger)實(shí)例打印debug、info、warn、error等不同級(jí)別的日志。作為兩個(gè)門(mén)面日志系統(tǒng),Commons Logging和SLF4J也同樣采用這樣的設(shè)計(jì)。所謂門(mén)面日志系統(tǒng),是指它們本身并不實(shí)現(xiàn)具體的日志打印邏輯,它們只是作為一個(gè)代理系統(tǒng),接收應(yīng)用程序的日志打印請(qǐng)求,然后根據(jù)當(dāng)前環(huán)境和配置,選取一個(gè)具體的日志實(shí)現(xiàn)系統(tǒng),將真正的打印邏輯交給具體的日志實(shí)現(xiàn)系統(tǒng),從而實(shí)現(xiàn)應(yīng)用程序日志系統(tǒng)的“可插拔”,即可以通過(guò)配置或更換jar包來(lái)方便的更換底層日志實(shí)現(xiàn)系統(tǒng),而不需要改變?nèi)魏未a。個(gè)人感覺(jué)SLF4J的實(shí)現(xiàn)更加靈活,并且它還提供了Maker和MDC的接口。這個(gè)將在接下的小節(jié)中具體介紹。
Commons Logging的設(shè)計(jì)比較簡(jiǎn)單,它定義了一個(gè)Log接口,所有它支持的日志系統(tǒng)都有相應(yīng)的Log實(shí)現(xiàn)類(lèi),如Log4JLogger、Jdk14Logger、Jdk13LumberjackLogger、SimpleLog、NoOpLog、AvalonLogger、LogKitLogger等類(lèi),在LogFactory中定義了一定的規(guī)則,從而根據(jù)當(dāng)前的環(huán)境和配置取得特定的Log子類(lèi)實(shí)例。
Commons Logging中默認(rèn)實(shí)現(xiàn)的LogFactory(LogFactoryImpl類(lèi))查找具體Log實(shí)現(xiàn)類(lèi)的邏輯如下:
1. 查找在commons-logging.properties文件中是否定存在以org.apache.commons.logging.Log或org.apache.commons.logging.log(舊版本,不建議使用)為key定義的Log實(shí)現(xiàn)類(lèi),如果是,則使用該類(lèi)。
2. 否則,查找在系統(tǒng)屬性中(-D方式啟動(dòng)參數(shù))是否存在以org.apache.commons.logging.Log或org.apache.commons.logging.log(舊版本,不建議使用)為key定義的Log實(shí)現(xiàn)類(lèi),如果是,則使用該類(lèi)。
3. 否則,如果在classpath中存在Log4J的jar包,則使用Log4JLogger類(lèi)。
4. 否則,如果當(dāng)前使用的JDK版本或等于1.4,則使用Jdk14Logger類(lèi)。
5. 否則,如果存在Lumberjack版本的Logging系統(tǒng),則使用Jdk13LumberjackLogger類(lèi)。
6. 否則,如果可以正常初始化Commons Logging自身實(shí)現(xiàn)的SimpleLog實(shí)例,則使用該類(lèi)
7. 最后,以上步驟都失敗,則拋出LogConfigurationException。
其實(shí),Commons Logging還支持用戶(hù)自定義的LogFactory實(shí)現(xiàn)類(lèi)。對(duì)LogFactory類(lèi)的查找邏輯為:
1. 查看系統(tǒng)屬性中是否存在以org.apache.commons.logging.LogFactory為key的LogFactory實(shí)現(xiàn)類(lèi),若有,則使用該類(lèi)實(shí)例化一個(gè)LogFactory實(shí)例。
2. 否則,嘗試使用service provider的方式查找LogFactory實(shí)現(xiàn)類(lèi),即查看classpath或jar包中是否存在META-INF/services/org.apache.commons.logging.LogFactory文件,如果存在,則使用該文件內(nèi)定義的LogFactory類(lèi)實(shí)例化一個(gè)LogFactory實(shí)例。
3. 否則,查找commons-logging.properties文件是否存在,并且其中存在以org.apache.commons.logging.LogFactory為key的LogFactory實(shí)現(xiàn)類(lèi),若有,則使用該類(lèi)實(shí)例化一個(gè)LogFactory實(shí)例。
4. 否則,使用默認(rèn)的LogFactoryImpl實(shí)現(xiàn)類(lèi)實(shí)例化一個(gè)LogFactory實(shí)例。
Commons Logging的類(lèi)設(shè)計(jì)圖如下:

在使用Commons Logging時(shí),經(jīng)常在服務(wù)器部署中會(huì)遇到ClassLoader的問(wèn)題,這也是經(jīng)常被很多人所詬病的地方,特別是在和Log4J一起使用的時(shí)候。常見(jiàn)的如,由于Common Logging使用非常廣泛,因而很多Web容器(WebSphere)在內(nèi)也會(huì)使用它作為日志處理系統(tǒng)而將其jar包引入到容器本身中,此時(shí)LogFactory是使用Web容器本身的ClassLoader裝載的,即使Log4J中使用了ContextClassLoader來(lái)查找配置文件,此時(shí)的Thread依然在容器中,因而它使用的ClassLoader還是容器本身的ClassLoader實(shí)例,此時(shí)需要把Log4J的配置文件放到共享目錄下,該配置文件才能被正常識(shí)別(以我的理解,容器在啟動(dòng)的時(shí)候,它根本無(wú)法獲得Web應(yīng)用程序中的jar包,所以也需要將Log4J的jar包放到共享目錄中才可以,不過(guò)我木有用過(guò)WebSphere,也沒(méi)法測(cè)試,所以只能猜測(cè)~)。在WebSphere還可以通過(guò)設(shè)置類(lèi)的加載順序?yàn)?/span>PARENT_LAST的方法來(lái)解決。而在Jboss中則只能將自己的配置加到其conf下的Log4J配置文件中,因?yàn)?/span>Jboss默認(rèn)導(dǎo)入Log4J包。具體可以參考我轉(zhuǎn)載的一篇文章,Log4j/common log和各種服務(wù)器集成的問(wèn)題(木有經(jīng)驗(yàn),只能用別人的文章了。。。還很可惜的木有機(jī)會(huì)測(cè)試。。。):http://www.aygfsteel.com/DLevin/archive/2012/11/02/390639.html,另外還找到一篇更加詳細(xì)的描述Commons Logging中存在的ClassLoader問(wèn)題的文章:http://articles.qos.ch/classloader.html
Commons Logging的具體實(shí)現(xiàn):
在使用Commons Logging時(shí),一般是通過(guò)LogFactory獲取Log實(shí)例,然后調(diào)用Log接口中相應(yīng)的方法。因而Commons Logging的實(shí)現(xiàn)可以分成以下幾個(gè)步驟:
1. LogFactory類(lèi)初始化
a. 緩存加載LogFactory的ClassLoader(thisClassLoader字段),出于性能考慮。因?yàn)?/span>getClassLoader()方法可能會(huì)使用AccessController(雖然目前并沒(méi)有使用),因而緩存起來(lái)以提升性能。
b. 初始化診斷流。讀取系統(tǒng)屬性org.apache.commons.logging.diagnostics.dest,若該屬性的值為STDOUT、STDERR、文件名。則初始化診斷流字段(diagnosticStream),并初始化診斷消息的前綴(diagnosticPrefix),其格式為:”[LogFactory from <ClassLoaderName@HashCode>] “, 該前綴用于處理在同一個(gè)應(yīng)用程序中可能會(huì)有多個(gè)ClassLoader加載LogFactory實(shí)例的問(wèn)題。
c. 如果配置了診斷流,則打印當(dāng)前環(huán)境信息:java.ext.dir、java.class.path、ClassLoader以及ClassLoader層級(jí)關(guān)系信息。
d. 初始化factories實(shí)例(Hashtable),用于緩存LogFactory(context-classloader –-> LogFactory instance)。如果系統(tǒng)屬性org.apache.commons.logging.LogFactory.HashtableImpl存在,則使用該屬性定義的Class作為factories Hashtable的實(shí)現(xiàn)類(lèi),否則,使用Common Logging實(shí)現(xiàn)的WeakHashtable。若初始化沒(méi)有成功,則使用Hashtable類(lèi)本身。使用WeakHashtable是為了處理在webapp中,當(dāng)webapp被卸載是引起的內(nèi)存泄露問(wèn)題,即當(dāng)webapp被卸載時(shí),其ClassLoader的引用還存在,該ClassLoader不會(huì)被回收而引起內(nèi)存泄露。因而當(dāng)不支持WeakHashtable時(shí),需要卸載webapp時(shí),調(diào)用LogFactory.relase()方法。
e. 最后,如果需要打印診斷信息,則打印“BOOTSTRAP COMPLETED”信息
2. 查找LogFactory類(lèi)實(shí)現(xiàn),并實(shí)例化。
當(dāng)調(diào)用LogFactory.getLog()方法時(shí),它首先會(huì)創(chuàng)建LogFactory實(shí)例(getFactory()),然后創(chuàng)建相應(yīng)的Log實(shí)例。getFactory()方法不支持線程同步,因而多個(gè)線程可能會(huì)創(chuàng)建多個(gè)相同的LogFactory實(shí)例,由于創(chuàng)建多個(gè)LogFactory實(shí)例對(duì)系統(tǒng)并沒(méi)有影響,因而可以不用實(shí)現(xiàn)同步機(jī)制。
a. 獲取context-classloader實(shí)例。
b. 從factories Hashtable(緩存)中獲取LogFactory實(shí)例。
c. 讀取commons-logging.properties配置文件(如果存在的話,如果存在多個(gè),則可以定義priority屬性值,取所有commons-logging.properties文件中priority數(shù)值最大的文件),如果設(shè)置use_tccl屬性為false,則在類(lèi)的加載過(guò)程中使用初始化cache的thisClassLoader字段,而不用context ClassLoader。
d. 查找系統(tǒng)屬性中是否存在org.apache.commons.logging.LogFactory值,若有,則使用該值作為LogFactory的實(shí)現(xiàn)類(lèi),并實(shí)例化該LogFactory實(shí)例。
e. 使用service provider方法查找LogFactory的實(shí)現(xiàn)類(lèi),并實(shí)例化。對(duì)應(yīng)Service ID是:META-INF/services/org.apache.commons.logging.LogFactory
f. 查找commons-logging.properties文件中是否定義了LogFactory的實(shí)現(xiàn)類(lèi):org.apache.commons.logging.LogFactory,是則用該類(lèi)實(shí)例化一個(gè)出LogFactory
g. 否則,使用默認(rèn)的LogFactory實(shí)現(xiàn):LogFactoryImpl類(lèi)。
h. 緩存新創(chuàng)建的LogFactory實(shí)例,并將commons-logging.properties配置文件中所有的鍵值對(duì)加到LogFactory的屬性集合中。
3. 通過(guò)LogFactory實(shí)例查找Log實(shí)例(LogFactoryImpl實(shí)現(xiàn))
使用LogFactory實(shí)例調(diào)用getInstance()方法取得Log實(shí)例。
a. 如果緩存(instances字段,Hashtable)存在,則使用緩存中的值。
b. 查找用戶(hù)自定義的Log實(shí)例,即從先從commons-logging.properties配置文件中配置的org.apache.commons.logging.Log(org.apache.commons.logging.log,舊版本)類(lèi),若不存在,查找系統(tǒng)屬性中配置的org.apache.commons.logging.Log(org.apache.commons.logging.log,舊版本)類(lèi)。如果找到,實(shí)例化Log實(shí)例
c. 遍歷classesToDiscover數(shù)組,嘗試創(chuàng)建該數(shù)組中定義的Log實(shí)例,并緩存Log類(lèi)的Constructor實(shí)例,在下次創(chuàng)建Log實(shí)例是就不需要重新計(jì)算。在創(chuàng)建Log實(shí)例時(shí),如果use_tccl屬性設(shè)為false,則使用當(dāng)前ClassLoader(加載當(dāng)前LogFactory類(lèi)的ClassLoader),否則盡量使用Context ClassLoader,一般來(lái)說(shuō)Context ClassLoader和當(dāng)前ClassLoader相同或者是當(dāng)前ClassLoader的下層ClassLoader,然而在很多自定義ClassLoader系統(tǒng)中并沒(méi)有設(shè)置正確的Context ClassLoader導(dǎo)致當(dāng)前ClassLoader成了Context ClassLoader的下層,LogFactoryImpl默認(rèn)處理這種情況,即使用當(dāng)前ClassLoader。用戶(hù)可以通過(guò)設(shè)置org.apache.commons.logging.Log.allowFlawedContext配置作為這個(gè)特性的開(kāi)關(guān)。
d. 如果Log類(lèi)定義setLogFactory()方法,則調(diào)用該方法,將當(dāng)前LogFactory實(shí)例傳入。
e. 將新創(chuàng)建的Log實(shí)例存入緩存中。
4. 調(diào)用Log實(shí)例中相應(yīng)的方法
Log接口比較簡(jiǎn)單,并且Log4J、JDK相應(yīng)的實(shí)現(xiàn)類(lèi)也都直接代理給各自框架,因而實(shí)現(xiàn)比較簡(jiǎn)單,不在詳述。關(guān)于SimpleLog,可以類(lèi)似的參考http://www.aygfsteel.com/DLevin/archive/2012/06/12/380647.html。 不過(guò)還是有必要對(duì)Jdk14Logger的實(shí)現(xiàn)吐槽一下,每次調(diào)用log方法時(shí)都會(huì)創(chuàng)建新的Throwable實(shí)例,然后去計(jì)算ClassName和Method,這會(huì)引起嚴(yán)重的性能問(wèn)題,因?yàn)閯?chuàng)建一個(gè)Throwable實(shí)例,意味著需要停止當(dāng)前的運(yùn)行,dump出一個(gè)調(diào)用棧快照。它為什么不像Jdk13LumberjackLogger的實(shí)現(xiàn)一樣,把這兩個(gè)字段緩存起來(lái)呢??