天天看點

深入源碼之JDK Logging

jdk從1.4開始提供logging實作,據說當初jdk打算采用log4j的,後來因為某些原因談判沒談攏,然後就自己開發了一套,不知道是為了報複而故意不沿用log4j的命名方式和抽象方式,還是開發這個子產品的人水準不夠,或沒用心,亦或是我用commons logging和log4j習慣了,看jdk的logging實作怎麼看怎麼不爽~~~吐個槽額~~~~

jdk logging将日志列印抽象成以下幾個類之間的互動:

1.    level,定義日志的級别,類似log4j中的level類。

jdk logging采用了完全不同于log4j中對級别的抽象。在jdk logging中,預設定義了以下幾個級别:severe(對應log4j中的error或fatal)、warning(對應log4j中的warn)、info(對應log4j中的info)、config(對應log4j中的debug)、fine(對應log4j中的trace)、finer(對應log4j中的trace)、finest(對應log4j中的trace)。另外,類似log4j,jdk logging也定義了兩個特殊的級别:all和off,分别對應列印所有級别的日志和關閉日志列印。

level中包含三個字段:name、value、resourcebundlename。其中name指定級别名稱,value指定該級别對應的一個int值,其值從severe開始依次遞減,resourcebundlename定義本地化後的級别名稱,預設是sun.util.logging.resources.logging,即我們在日志中看到警告、資訊等級别字段就是通過調用level的getlocalizedname()方法,讀取resourcebundlename對應的resource值來獲得的,這也是log4j中沒有聽過的。

level還定義了一個parse()方法,它可以支援解析name字元串、代表級别的int值(以字元串的形式)、以及對應的localized名稱,如果所有的都不滿足需求,則抛出illegalargumentexception。

最後,level還實作了readresolve()方法,進而確定反序列化後的level隻是level類中定義的幾個執行個體(出了自定義的level執行個體)。

2.    logrecord,封裝了列印一條一直所包含的所有資料,類似log4j中的loggingevent。

它包含了以下資訊:

a.    日志級别(level)

b.    全局辨別号(sequencenumber,即沒建立一個logrecord,sequencenumber都會自增1)

c.    列印這條日志語句所在的類的名稱(sourceclassname),可以通過調用infercaller()方法解析出來。解析實作則是通過執行個體化一個throwable執行個體,通過解析該執行個體的call stack即可得出列印這條日志所在的類的名稱和方法名稱,這裡貌似不支援行号、檔案名等資訊。并且通過設定needtoinfercaller字段(不可序列化)來判斷sourceclassname和sourcemethodname是否已經取得,進而不用每次調用的時候都去解析而提升性能。

d.    列印這條日志語句所在調用方法的名稱(sourcemethodname),它也是通過調用infercaller()方法解析出來。

e.    列印消息(message)

f.     線程号(threadid),對logrecord本身而言,從0開始對每個線程自增1

g.    建立logrecord的時間(millis)

h.    列印日志中的異常(thrown)

i.      日志名稱(loggername)

j.     資源名稱(resourcebundlename)

k.    參數清單(parameters數組),在formatter中通過messageformat使用這些參數清單,并在序列化是手動調用其tostring()方法序列化,而不是采用預設的序列化方式。

l.      resourcebundle執行個體,擷取本地消息,不可序列化。

3.    formatter,根據配置格式化一條日志記錄,類似log4j中的layout。

jdk logging支援兩種類型的formatter:simpleformatter和xmlformatter,預設采用simpleformatter,它先列印日期和時間、loggername或source classname、方法名稱,然後換行,在列印日志級别、本地化後的消息,然後換行,列印異常資訊。而xmlformatter實作gethead()、gettail()方法,并且将每條記錄寫成一條<record></record>記錄。吐槽一下,它的實作是在是太不專業了~~。

4.    handler,實作将日志寫入指定目的地,如consolehandler、filehandler、sockethandler即對應将日志寫入控制台、檔案、socket端口。它類似log4j中的appender。

handler包含對formatter、level、filter的引用,其中formatter将logrecord格式化成string字元串;level定義目前handler支援的日志級别;而filter則提供一個使用者自定義的過濾一些日志的擴充點。

public interface filter {

        public boolean isloggable(logrecord record);

}

使用者可以定義自己的filter類以實作使用者自定義的過濾邏輯。handler還定義了encoding屬性,以配置底層日志的輸出格式。

handler中最終要的方法是publish(),它實作了真正列印日志消息的邏輯。

public abstract void close() throws securityexception;

預設jdk logging實作了streamhandler,而streamhandler有三個子類:consolehandler、filehandler、sockethandler。streamhandler支援的配置有:

java.util.logging.streamhandler.level 設定目前handler支援的級别,預設為fine

java.util.logging.streamhandler.filter 設定目前handler的filter,預設為null

java.util.logging.streamhandler.formatter 設定目前handler的formatter類,預設為simpleformatter

java.util.logging.streamhandler.encoding 設定目前handler的編碼方式,預設為null

consolehandler隻是将outputstream設定為system.err,其他實作和streamhandler類似。

而sockethandler将outputstream綁定到對應的端口号中,其他也和streamhandler類似。另外它還增加了兩個配置:java.util.logging.sockethandler.port和java.util.logging.sockethandler.host分别對應端口号和主機。

filehandler支援指定檔案名模闆(java.util.logging.filehandler.pattern),檔案最大支援大小(java.util.logging.filehandler.limit,位元組為機關,0為沒有限制),循環日志檔案數(java.util.logging.filehandler.count)、對已存在的日志檔案是否往後添加(java.util.logging.filehandler.append)。

filehandler支援的檔案模闆參數有:

/     目錄分隔符

%t   系統臨時目錄

%h 系統目前使用者目錄

%g 生成的以差別循環日志檔案名

%u 一個唯一的數字以處理沖突問題

%% 一個%

5.    logmanager類,讀取配置檔案和管理logger執行個體,類似log4j的logrepository。

在logmanager類初始化時,使用者可以通過指定java.util.logging.manager系統屬性以自定義logmanager,預設使用logmanager類本身。初始化完成後建立rootlogger,并将新建立的rootlogger執行個體加入到logmanager中。logmanager中将所有建立的logger緩存在loggers字段中(hashtable,name作為key,weakreference<logger>作為value)。rootlogger的name為空,因而所有對rootlogger的配置都從”.”開始。

對logger的配置支援一下幾種方式:

<name>.level=info|config….

handers=<handlername1>,<handlername2>…. (以”,”分隔或以空格、tab等字元分隔,全局handler)

config=<configclassname1>,<configclassname2>….(自定義類,實作在其構造函數中實作自定義配置)

<name>.userparenthandlers=true|false|1|0

<name>.handlers=<handlername1>,<handlername2>…(以”,”分隔或以空格、tab等字元分隔)

<handlername>.level=info|config….

以及各自handler本身支援的配置,具體各自的handler。

使用者可以通過以下方式自定義配置檔案:

a.    設定系統屬性java.util.logging.config.class,由自定義類的構造函數實作自定義配置,如調用logmanager、logger中的一些靜态方法。

b.    設定系統屬性java.util.logging.config.file,自定義配置檔案路徑。讀取該檔案中的内容作為配置資訊。

c.    預設使用${java.home}/lib/logging.properties檔案作為配置檔案(jdk已經提供了一些預設配置,一般是${jre_home}/lib/logging.properties檔案)

類似log4j,logmanager也将logger建構成樹狀結構,并且對那些暫時沒有真正logger執行個體的節點,使用lognode,同樣建構成樹,這個實作也類似log4j的provisionnode。

6.    logger類,使用者列印log接口,類似log4j中的logger。

logger包含name、handlers、resourcebundlename、useparenthandlers、filter、anonymous、levelobject、parent、kids等字段。其中其他字段都比較容易了解,anonymous比較難了解,按注釋,它是用來表達目前logger是一個匿名logger,即不會被加入到logmanager中,因而也不需要安全檢查,匿名logger一般在applet中使用,對applet不了解,因而也無法做更詳細的解釋。在建構樹時,該logger同時保持了父節點和子節點的所有引用,對jdk這個logging的實作一直無力吐槽。

logger提供getlogger()接口,這個也是一般使用者直接打交道的接口,傳入name,傳回緩存的logger執行個體或新建立一個logger執行個體。

對匿名logger,logger提供getanonymouslogger()接口。log(logrecord)方法是對列印日志的真正實作:

public void log(logrecord record) {

    if (record.getlevel().intvalue() < levelvalue || levelvalue == offvalue) {

        return;

    }

    synchronized (this) {

        if (filter != null && !filter.isloggable(record)) {

            return;

        }

    logger logger = this;

    while (logger != null) {

        handler targets[] = logger.gethandlers();

        if (targets != null) {

            for (int i = 0; i < targets.length; i++) {

                targets[i].publish(record);

            }

        if (!logger.getuseparenthandlers()) {

            break;

        logger = logger.getparent();

其他log方法隻是對該方法中logrecord中不同字段參數的組合。其他logp()系類方法隻是提供給使用者自定log所在的方法名和類名;而entering、existing、thrown等幾個方法隻是幾個傻逼的命名而已。

最後給張jdk logging的類圖吧:

深入源碼之JDK Logging