發(fā)布日期:2006年07月30日,更新日期:2006年07月30日
1996年初,歐洲安全電子市場(EU SEMPER)項(xiàng)目組決定編寫自己的日志記錄API,后來這個(gè)API演變成了Log4j。Log4j是一個(gè)開放源碼項(xiàng)目,一個(gè)非常流行的Java日志記錄包。它允許開發(fā)者向代碼中插入日志記錄語句,還允許在不修改應(yīng)用程序源碼的情況下修改記錄日志的行為。
1996年初,歐洲安全電子市場(EU SEMPER)項(xiàng)目組決定編寫自己的日志記錄API,后來這個(gè)API演變成了Log4j。Log4j是一個(gè)開放源碼項(xiàng)目,一個(gè)非常流行的Java日志記錄包。它允許開發(fā)者向代碼中插入日志記錄語句,還允許在不修改應(yīng)用程序源碼的情況下修改日志記錄的行為。
幾乎每一個(gè)項(xiàng)目都會(huì)使用日志記錄,但是由于日志記錄不是項(xiàng)目的核心,因此受重視的程度一般不是很高。我們認(rèn)為使用日志記錄是一件非常嚴(yán)肅的事情,而且做好使用日志記錄的規(guī)劃比單純記錄日志本身更加重要。
本文將比較全面的闡述Log4j的設(shè)計(jì)原理和使用方法。
日志記錄記錄的是應(yīng)用程序運(yùn)行的軌跡。我們可以通過查看這些軌跡來調(diào)試應(yīng)用程序,這可能也是日志記錄最為流行的用法了。但是我們必須意識到規(guī)劃良好的日志記錄中還含有豐富的信息,通過手工的方式或借助一些工具(大多數(shù)時(shí)候需要自己來書寫這些工具)來分析挖掘這些信息。
例如,如果我們在規(guī)劃中指出必須記錄用戶的每一次操作,記錄的樣式為 [日志信息]-[操作開始的時(shí)間]-[日志級別]-[日志類別]-[用戶名]-[操作名]-[消息],這只是我們假設(shè)的一種樣式,實(shí)際的日志中一般會(huì)含有比這更加豐富的信息。為了更好的理解,我們根據(jù)該樣式構(gòu)造了一些日志記錄(其中日志類別org.solol.Main、org.solol.Parser和org.solol.UserOperator使用了不同的樣式):
[日志信息]-[2006-07-30 08:54:20]-[INFO]-[org.solol.Main]-[具體的消息]
[日志信息]-[2006-07-30 08:55:20]-[INFO]-[org.solol.UserOperator]-[User1]-[查詢報(bào)表1]-[具體的消息]
[日志信息]-[2006-07-30 08:55:30]-[INFO]-[org.solol.UserOperator]-[User1]-[查詢報(bào)表2]-[具體的消息]
[日志信息]-[2006-07-30 08:56:01]-[INFO]-[org.solol.Parser]-[具體的消息]
[日志信息]-[2006-07-30 08:57:26]-[INFO]-[org.solol.UserOperator]-[User2]-[添加用戶User3]-[具體的消息]
[日志信息]-[2006-07-30 08:58:20]-[INFO]-[org.solol.UserOperator]-[User1]-[查詢報(bào)表3]-[具體的消息]
[日志信息]-[2006-07-30 08:59:38]-[INFO]-[org.solol.UserOperator]-[User3]-[查詢報(bào)表1]-[具體的消息]
[日志信息]-[2006-07-30 08:59:39]-[INFO]-[org.solol.UserOperator]-[User2]-[退出系統(tǒng)]-[具體的消息]
從上面的日志記錄中我們很容易抽取出某一用戶的操作列表,如對于用戶User1我們的結(jié)果為:
[日志信息]-[2006-07-30 08:55:20]-[INFO]-[org.solol.UserOperator]-[User1]-[查詢報(bào)表1]-[具體的消息]
[日志信息]-[2006-07-30 08:55:30]-[INFO]-[org.solol.UserOperator]-[User1]-[查詢報(bào)表2]-[具體的消息]
[日志信息]-[2006-07-30 08:58:20]-[INFO]-[org.solol.UserOperator]-[User1]-[查詢報(bào)表3]-[具體的消息]
這樣我們就得到了某一時(shí)間段中User1的操作列表,可以利用這一列表來進(jìn)行安全分析。
我們還可以從另外的角度來分析上面的日志記錄,如我們很容易統(tǒng)計(jì)出操作(日志類別為org.solol.UserOperator)發(fā)生的總次數(shù)(6次),其中操作[查詢報(bào)表1]為2次,[查詢報(bào)表2]為1次,[查詢報(bào)表3]為1次,[添加用戶User3]為1次,[退出系統(tǒng)]為1次。這樣我們就可以得出系統(tǒng)中的那些操作用戶使用的比較頻繁。
以上我們從兩個(gè)角度對日記記錄中的信息進(jìn)行了簡單的挖掘,實(shí)際中待挖掘的方面要豐富的多,這取決于您的意圖和您的想象力。
這里我們還要特別強(qiáng)調(diào)一下:所有這一切都需要有使用日志記錄的良好規(guī)劃。如果規(guī)劃不好(即日志記錄沒有規(guī)律性),那么我們挖掘時(shí)的任務(wù)就會(huì)非常繁重或者使挖掘成為一個(gè)不可能的任務(wù)。
文章到了這里我們要來描述日志記錄的最為流行的用法了,即調(diào)試應(yīng)用程序。我們在調(diào)試應(yīng)用程序時(shí)一般會(huì)使用兩種方法,除了日志記錄之外,還有debugger調(diào)試器。
我們不想把他們放到一起來描述,因?yàn)檫@是兩個(gè)完全不同的問題,雖然他們都用來調(diào)試應(yīng)用程序。使用debugger調(diào)試器我們可以清楚的知道引發(fā)錯(cuò)誤的上下文及其相關(guān)信息,也可以使用單步執(zhí)行、設(shè)置斷點(diǎn)、檢查變量以及暫掛和恢復(fù)線程等等比較高級的能力,但是盡管這樣它也不能替代日志記錄,同樣日志記錄也不能替代debugger調(diào)試器。我們要結(jié)合使用這兩種方法,不同的場景使用不同的方法會(huì)有更好的效果。
我們認(rèn)為使用日志記錄來調(diào)試應(yīng)用程也應(yīng)該充分考慮軟件的開發(fā)周期。這里我們只考慮軟件開發(fā)周期中的與日志記錄有關(guān)的兩個(gè)階段:
- 開發(fā)階段,用來記錄應(yīng)用程序的方方面面和各種細(xì)節(jié),非常詳細(xì),使得一看到它就知道那里出了問題,出了什么樣的問題。
- 出品階段,要能夠記錄各種級別的錯(cuò)誤和警告,同時(shí)也要適度記錄應(yīng)用程序正常運(yùn)行的關(guān)鍵信息,這些信息可以給相關(guān)人員(開發(fā)人員、測試人員、用戶等)極大的信心,使他們可以毫不猶豫的告訴您--瞧我們的軟件在正常的運(yùn)行。如一個(gè)好的web服務(wù)器的啟動(dòng)日志記錄不僅要包含錯(cuò)誤和警告,還要包含服務(wù)器正在啟動(dòng),正在加載某某組件等等,最后還要提示啟動(dòng)是成功還是失敗。
閱讀到這里我們就應(yīng)該著手實(shí)現(xiàn)我們的日志記錄了。比較幸運(yùn)的是我們有好多日志記錄軟件包可選,這就使我們不必關(guān)心日志記錄的細(xì)節(jié),只要把主要的精力放到日志記錄的規(guī)劃上就好了。我們選擇的是Log4j,文章的余下部分將主要介紹這個(gè)Java日志記錄軟件包。
log4j的特性列表:
- 在運(yùn)行速度方面進(jìn)行了優(yōu)化
- 使用基于名稱的日志(logger)層次結(jié)構(gòu)
- 是fail-stop的
- 是線程安全的
- 不受限于預(yù)定義的實(shí)用工具集
- 可以在運(yùn)行時(shí)使用property和xml兩種格式的文件來配置日志記錄的行為
- 在一開始就設(shè)計(jì)為能夠處理Java異常
- 能夠定向輸出到文件(file)、控制臺(console)、java.io.OutputStream、java.io.Writer、遠(yuǎn)程服務(wù)器、遠(yuǎn)程Unix Syslog守護(hù)者、遠(yuǎn)程JMS監(jiān)聽者、NT EventLog或者發(fā)送e-mail
- 使用DEBUG、INFO、WARN、ERROR和FATAL五5個(gè)級別
- 可以容易的改變?nèi)罩居涗浀牟季?Layout)
- 輸出日志記錄的目的地和寫策略可以通過實(shí)現(xiàn)Appender接口來改變
- 支持為每個(gè)日志(logger)附加多個(gè)目的地(appender)
- 提供國際化支持
Log4j有三個(gè)主要的組件:Logger、Appender和Layout。這三個(gè)組件相互配合使得我們可以獲得非常強(qiáng)大的日志記錄的能力。
Logger的名稱是區(qū)分大小寫的,依據(jù)名稱可以確定其層次結(jié)構(gòu)(即父子關(guān)系),規(guī)則如下:
- 如果Logger A的名稱后跟一個(gè)點(diǎn)(.)是Logger B的名稱的前綴就認(rèn)為Logger A是Logger B的祖先。
- 如果在Logger A和Logger B之間,Logger B沒有任何其它的祖先就認(rèn)為Logger A是Logger B的父親。
在Logger的層次結(jié)構(gòu)的最頂層是root logger,它會(huì)永遠(yuǎn)存在,而且不能通過名字取到。
上面文字的描述可能不好的理解,為此我們給出了一張圖,Logger的層次結(jié)構(gòu)圖,從中可以非常直觀的看出三種主要組件的關(guān)系和各自所起的作用。
圖示 1. Logger的層次結(jié)構(gòu)圖
Loger x.y是Logger x.y.z的祖先,因?yàn)閤.y.是x.y.z的前綴,這符合規(guī)則的前一條。另外在Logger x.y和Logger x.y.z之間,Logger x.y.z沒有其它的祖先,因此Logger x.y是Logger x.y.z的父親,這符合規(guī)則的后一條。這樣我們依據(jù)上面的規(guī)則就可以構(gòu)造出如圖1所示的Logger的層次結(jié)構(gòu)。
從圖1中我們還可以看到每一個(gè)Logger都有一個(gè)Level,根據(jù)該Level的值Logger決定是否處理對應(yīng)的日志請求。如果Level沒有被設(shè)置,就象圖1中的Logger x.y一樣,又該怎么辦呢?答案是可以從祖先那里繼承。
如果Logger C沒有被設(shè)置Level,那么它將沿著它的層次結(jié)構(gòu)向上查找,如果找到就繼承并結(jié)束,否則會(huì)一直查找到root logger結(jié)束。因?yàn)閘og4j在設(shè)計(jì)時(shí)保證root logger會(huì)被設(shè)置一個(gè)默認(rèn)的Level,所以任何logger都可以繼承到Level。
圖1中的Logger x.y沒有被設(shè)置Level,但是根據(jù)上面的繼承規(guī)則,Logger x.y繼承了root logger的Level。
我們在來看看Logger選擇日志記錄請求(log request)的規(guī)則:
假設(shè)Logger M具有q級的Level,這個(gè)Level可能是設(shè)置的也可能是繼承到的。
如果向Logger M發(fā)出一個(gè)Level為p的日志記錄請求,那么只有滿足p>=q時(shí)這個(gè)日志記錄請求才會(huì)被處理。
org.apache.log4j.Logger中的不同方法發(fā)出不同Level的日志記錄請求,如下:
- public void debug(Object message),發(fā)出Level為DEBUG的日志記錄請求
- public void info(Object message),發(fā)出Level為INFO的日志記錄請求
- public void warn(Object message),發(fā)出Level為WARN的日志記錄請求
- public void error(Object message),發(fā)出Level為ERROR日志記錄請求
- public void fatal(Object message),發(fā)出Level為FATAL的日志請求
- public void log(Level l, Object message),發(fā)出指定Level的日志記錄請求
其中的靜態(tài)常量DEBUG、INFO、WARN、ERROR、FATAL是在org.apache.log4j.Level中定義的,除了使用這些預(yù)定義的Level之外,Log4j還支持自定義Level。
注:org.apache.log4j.Level中還預(yù)定義了一些其它的Level。
在Log4j中,Appender指的是日志記錄輸出的目的地。當(dāng)前支持的Appender(目的地)有文件(file)、控制臺(console)、java.io.OutputStream、java.io.Writer、遠(yuǎn)程服務(wù)器、遠(yuǎn)程Unix Syslog守護(hù)者、遠(yuǎn)程JMS監(jiān)聽者、NT EventLog或者發(fā)送e-mail。如果您在上面沒有找到適合的Appender,那就需要考慮實(shí)現(xiàn)自己的自定義Appender了。
每個(gè)Logger可以有多個(gè)Appender,但是相同的Appender只會(huì)被添加一次。
Appender的附加性意味著Logger C會(huì)將日志記錄發(fā)給它的和它祖先的所有Appender。在圖1中Logger a會(huì)將日志記錄發(fā)給它自己的JDBCAppender和它的祖先root logger的ConsoleAppender和FileAppender。Logger x.y.z自己沒有Appender,它將把日志記錄發(fā)給它的祖先root logger的ConsoleAppender和FileAppender,如果Logger x.y也含有Appender,那么它們也會(huì)包括在內(nèi)。
Appender的附加性是可以被中斷的。假設(shè)Logger C的一個(gè)祖先為Logger P,如果Logger P的附加性標(biāo)志(additivity flag)設(shè)置為假,那么Logger C會(huì)將日志記錄只發(fā)給它的和在它和Logger P之間的祖先(包括Logger P)的Appender,而不會(huì)發(fā)給Logger P的祖先的Appender。Logger的附加性標(biāo)志(additivity flag)默認(rèn)值為ture。
在圖1中如果沒有設(shè)置Logger a的附加性標(biāo)志(additivity flag),而是使用默認(rèn)值true,那么Logger a會(huì)將日志記錄發(fā)給它自己的JDBCAppender和它祖先root logger的ConsoleAppender和FileAppender,這和上面的描述相同。如果設(shè)置Logger a的附加性標(biāo)志(additivity flag)的值false,那么Logger a會(huì)將日志記錄發(fā)給它自己的JDBCAppender而不會(huì)在發(fā)給它祖先root logger的ConsoleAppender和FileAppender了。
Appender定制了輸出目的地,通常我們還需要定制日志記錄的輸出格式,在Log4j中是通過將Layout和Appender關(guān)聯(lián)到一起來實(shí)現(xiàn)的。Layout依據(jù)用戶的要求來格式化日志記錄。PatternLayout(標(biāo)準(zhǔn)Log4j組件)讓用戶依據(jù)類似于C語言printf函數(shù)的轉(zhuǎn)換模式來指定輸出格式。
例如,轉(zhuǎn)換模式(conversion pattern)為"%r [%t] %-5p %c - %m%n"的PatternLayout將生成類似于以下內(nèi)容的輸出:
176 [main] INFO org.foo.Bar - Located nearest gas station.
在上面的輸出中:
- 第一個(gè)字段表示自程序開始到發(fā)出日志記錄請求時(shí)所消耗的毫秒數(shù)
- 第二個(gè)字段表示發(fā)出日志記錄請求的線程
- 第三個(gè)字段表示日志記錄請求的Level
- 第四個(gè)字段表示發(fā)出日志記錄請求的Logger的名稱
- 第五個(gè)字段(-后的文本)表示日志記錄請求的消息
Log4j中還提到了一些其它的Layout,包括HTMLLayout、SimpleLayout、XMLLayout、TTCCLayout和DateLayout。如果這些不能滿足您的要求,還可以自定義自己的Layout。
依據(jù)既有的經(jīng)驗(yàn)顯示用于日志記錄的代碼大約是全部代碼量的4%。如果應(yīng)用程序具有一定的規(guī)模,日志記錄語句的數(shù)量還是比較巨大的,因此必須有效的管理這些語句。
在Log4j中我們可以通過配置Log4j環(huán)境來有效的管理日志記錄。配置的方式有三種:
- 通過程序配置
- 通過Property文件配置
- 通過XML文件配置
通過程序配置Log4j環(huán)境實(shí)際上就是在應(yīng)用程序的代碼中改變Logger的Level或增加減少Appender等等。
Log4j提供了BasicConfigurator,它只是為root logger添加Appender。其中,
- BasicConfigurator.configure()為root logger添加一個(gè)關(guān)聯(lián)著PatternLayout.TTCC_CONVERSION_PATTERN的ConsoleAppender
- BasicConfigurator.configure(Appender appender)為root logger添加指定的Appender
我們可以把BasicConfigurator看成是一個(gè)簡單的使用程序配置Log4j環(huán)境的示例。例如,要給root logger添加兩個(gè)Appender(A和B),下面的代碼分別完成了這個(gè)要求。
不使用BasicConfigurator:
//示例代碼,不能直接使用
Logger root = Logger.getRootLogger();
root.addAppender(A);
root.addAppender(B);
使用BasicConfigurator:
//示例代碼,不能直接使用
BasicConfigurator.configure(A);
BasicConfigurator.configure(B);
這里要使用PropertyConfigurator來分析配置文件并設(shè)置日志記錄,但是要注意日志記錄先前的配置不會(huì)被清除和重設(shè)。
Property文件是由key=value這樣的鍵值對所組成的,可以使用#或!作為注釋行的開始。下面給出了兩個(gè)簡單的示例:
非常簡單的示例1:
log4j.rootLogger=DEBUG, A1
log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%-4r %-5p [%t] %37c %3x - %m%n
稍顯復(fù)雜的示例2:
log4j.rootLogger=, A1, A2
log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%d %-5p [%t] %-17c{2} (%13F:%L) %3x - %m%n
log4j.appender.A2=org.apache.log4j.FileAppender
log4j.appender.A2.File=filename.log
log4j.appender.A2.Append=false
log4j.appender.A2.layout=org.apache.log4j.PatternLayout
log4j.appender.A2.layout.ConversionPattern=%-5r %-5p [%t] %c{2} - %m%n
上面的兩個(gè)示例只是讓您對配置文件的格式有一個(gè)大體的認(rèn)識,我們將在后面詳細(xì)的描述各個(gè)配置元素的語法。
Repository-wide threshold:
Repository-wide threshold指定的Level的優(yōu)先級高于Logger本身的Level。語法為log4j.threshold=[level],level可以為OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL。也可以使用自定義Level,這時(shí)的語法為log4j.threshold=[level#classname]。默認(rèn)為ALL。
依據(jù)上面的規(guī)則,我們有這樣的結(jié)論:如果log4j.threshold=ERROR,Logger C的Level=DEBUG,這時(shí)只有高于等于ERROR的日志記錄請求會(huì)被Logger C處理。
Appender的配置:
Appender的配置語法為
# For appender named appenderName, set its class.
# Note: The appender name can contain dots.
log4j.appender.appenderName=fully.qualified.name.of.appender.class
# Set appender specific options.
log4j.appender.appenderName.option1=value1
...
log4j.appender.appenderName.optionN=valueN
#For each named appender you can configure its Layout.
#The syntax for configuring an appender's layout is:
log4j.appender.appenderName.layout=fully.qualified.name.of.layout.class
log4j.appender.appenderName.layout.option1=value1
....
log4j.appender.appenderName.layout.optionN=valueN
Logger的配置:
root logger的配置語法:
log4j.rootLogger=[level], appenderName, appenderName, ...,其中l(wèi)evel可以為OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL。也可以使用自定義Level,這時(shí)的語法為[level#classname]。
如果Level被指定那么root logger的Level將被配置為指定值。如果Level沒有被指定那么root logger的Level不會(huì)被修改。從上面的語法中我們可以看出通過用,分隔的列表可以為root logger指定多個(gè)Appender。
對于root logger之外的logger語法是相似的,為log4j.logger.logger_name=[level|INHERITED|NULL], appenderName, appenderName, ...
上面只有INHERITED和NULL需要說明一下,其它部分和root logger相同。INHERITED和NULL的意義是相同的。如果我們使用了它們,意味著這個(gè)logger將不在使用自己的Level而是從它的祖先那里繼承。
Logger的附加性標(biāo)志(additivity flag)可以使用log4j.additivity.logger_name=[false|true]來配置。
ObjectRenderer配置:
我們可以通過ObjectRenderer來定義將消息對象轉(zhuǎn)換成字符串的方式。語法為log4j.renderer.fully.qualified.name.of.rendered.class=fully.qualified.name.of.rendering.class。如:
//my.Fruit類型的消息對象將由my.FruitRenderer轉(zhuǎn)換成字符串
log4j.renderer.my.Fruit=my.FruitRenderer
對上面的各個(gè)配置元素的語法理解之后,在來看示例1和2就很容易了。
PropertyConfigurator不支持Filter的配置。如果要支持Filter您可以使用DOMConfigurator,即使用XML文件的方式配置。
要使用DOMConfigurator.configure()來讀取XML格式的配置文件。XML文件格式的定義是通過org/apache/log4j/xml/log4j.dtd來完成的,各個(gè)配置元素的嵌套關(guān)系如下:
<!ELEMENT log4j:configuration (renderer*, appender*,(category|logger)*,root?,categoryFactory?)>
這里沒有給出更為詳細(xì)的內(nèi)容,要了解詳細(xì)的內(nèi)容需要查閱log4j.dtd。
下面這個(gè)簡單的示例可以使您對XML配置文件的格式有一個(gè)基本的認(rèn)識:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j SYSTEM "log4j.dtd">
<log4j>
<appender name="A1" class="org.apache.log4j.FileAppender">
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%-5p %c{2} - %m\n"/>
</layout>
</appender>
<appender name="A2" class="org.apache.log4j.FileAppender">
<layout class="org.apache.log4j.TTCCLayout">
<param name="DateFormat" value="ISO8601" />
</layout>
<param name="File" value="warning.log" />
<param name="Append" value="false" />
</appender>
<category name="org.apache.log4j.xml" priority="debug">
<appender-ref ref="A1" />
</category>
<root priority="debug">
<appender-ref ref="A1" />
<appender-ref ref="A2" />
</root>
</log4j>
默認(rèn)初始化過程在LogManager類的靜態(tài)初始化器中完成。具體步驟如下:
- 檢查系統(tǒng)屬性log4j.defaultInitOverride,如果值為false則執(zhí)行初始化過程,否則跳過初始化過程。
- 將系統(tǒng)屬性log4j.configuration的值賦給變量resource。如果log4j.configuration沒有被定義則使用默認(rèn)值log4j.properties。
- 試圖轉(zhuǎn)換變量resource到一個(gè)url。
- 如果變量resource不能轉(zhuǎn)換成一個(gè)url,那么將使用org.apache.log4j.helpers.Loader.getResource(resource, Logger.class)得到一個(gè)url。
- 如果還是得不到url,將忽略默認(rèn)初始化過程。如果得到url將使用PropertyConfigurator或DOMConfigurator來配置,也可以使用自定義的XXXConfigurator。
posted on 2008-05-21 10:20
xzc 閱讀(1970)
評論(0) 編輯 收藏 所屬分類:
Log4j