
用慣了VC的人剛接觸Java大概很不習(xí)慣代碼的調(diào)試,的確,在M$的大部分IDE都做得相當(dāng)出色,包括像VJ++這樣一直被Java程序員稱為是“垃圾”的類庫(記得以前在瀚海星云的Java版提有關(guān)VJ問題的人是有可能被封的,^_^),它的開發(fā)工具在調(diào)試上都相當(dāng)容易。Java也有命令行方式的調(diào)試和IDE的調(diào)試,但現(xiàn)在的像JB這樣的玩意又是個龐然大物,低配置的機(jī)器可能就是個奢望,不像VC那樣。怎么辦呢,高手們說,“我的jdb用得賊熟練”,那我會報以景仰的目光,像我這樣的菜鳥基本上就沒使過jdb,還是老老實實在代碼里面System.out.println(...)。直到1996年一個叫做“歐洲安全電子市場”(E.U. SEMPER)的項目啟動,“調(diào)試”不再是一件“體力活”,而是一種軟件設(shè)計的藝術(shù),這個項目組開發(fā)的日志管理接口后來成為Apache Jakarta項目中的一員,它就是現(xiàn)在我們所熟悉的log4j。下面的文字將概要介紹與Java日志記錄相關(guān)的一些技術(shù),目的不是讓您放棄老土的System.out.println(...),而是說,在Java的世界里可以有許多種選擇,你今天覺得掌握了一件高級武器,明天可能就是“過時”的了,呵呵。
始祖:System.out.println(...)
為什么還是要一再提到它?畢竟我們的習(xí)慣不是那么容易改變的,而且System.out(別忘了還有System.err)是一個直接和控制臺打交道的PrintStream對象,是終端顯示的基礎(chǔ),高級的Logger要在終端顯示日志內(nèi)容,就必然會用到這個。一個小規(guī)模的程序調(diào)試,恰當(dāng)?shù)厥褂肧ystem.out.println(...)我認(rèn)為仍然是一種最方便最有效的方法,所以我們?nèi)园阉旁谧铋_始,以示不能“數(shù)典忘祖” :)
不常用的關(guān)鍵字:assert
assert對多數(shù)人來講可能還比較陌生,它也是一個調(diào)試工具,好像是J2SE 1.4才加進(jìn)來的東東,一種常見的用法是:
assert (布爾表達(dá)式);
當(dāng)表達(dá)式為true時沒有任何反映,如果為false系統(tǒng)將會拋出一個AssertionError。如果你要使用assert,在編譯時必須加上“-source 1.4”的選項,在運(yùn)行時則要加上“-ea”選項。
后生可畏:Java Logging API一瞥
System.out.println(...)對于較高要求的用戶是遠(yuǎn)遠(yuǎn)不夠的,它還不是一個日志系統(tǒng),一個比較完善的日志系統(tǒng)應(yīng)當(dāng)有輸出媒介、優(yōu)先級、格式化、日志過濾、日志管理、參數(shù)配置等功能。伴隨J2SE 1.4一起發(fā)布的Java日志包java.util.logging適時地滿足了我們的初步需求,在程序中按一定格式顯示和記錄豐富的調(diào)試信息已經(jīng)是一件相當(dāng)easy的事情。
1. 日志記錄器:Logger
Logger是一個直接面向用戶的日志功能調(diào)用接口,從用戶的角度上看,它完成大部分日志記錄工作,通常你得到一個Logger對象,只需要使用一些簡單方法,譬如info,warning,log,logp,logrb等就能完成任務(wù),簡單到和System.out.println(...)一樣只用一條語句,但后臺可能在向控制臺,向文件,向數(shù)據(jù)庫,甚至向網(wǎng)絡(luò)同時輸出該信息,而這個過程對用戶是完全透明的。
在使用Logger之前,首先需要通過getLogger()或getAnonymousLogger()靜態(tài)方法得到一個Logger對象(想想看,這里是不是設(shè)計模式當(dāng)中的“工廠方法”的一個實實在在的應(yīng)用?可以參考一下Logger的源代碼,你就明白LogManager是“工廠類”而Logger是“產(chǎn)品類”,凡事都要學(xué)以致用嘛,呵呵)。這里我們需要了解的是Logger的“名字空間”(namespace)的概念:通常我們調(diào)試時需要清楚地知道某個變量是出現(xiàn)在什么位置,精確到哪個類的哪個方法,namespace就是這么個用處。我們用getLogger()得到Logger時需要指定這個Logger的名字空間,通常是一個包名,譬如“com.jungleford.test”等,如果是指定了namespace,那么將在一個全局對象LogManager中注冊這個namespace,Logger會基于namespace形成層次關(guān)系,譬如namespace為“com.jungleford”的Logger就是namespace為“com.jungleford.test”的Logger的父,后者調(diào)用getParent()方法將返回前者,如果當(dāng)前沒有namespace為“com.jungleford”的Logger,則查找namespace為“com”的Logger,要是按照這個鏈找不到就返回根Logger,其namespace為"",根Logger的父是null。從理論上說,這個namespace可以是任意的,通常我們是按所調(diào)試的對象來定,但如果你是使用getAnonymousLogger()方法產(chǎn)生的Logger,那它就沒有namespace,這個“匿名Logger”的父是根Logger。
得到一個Logger對象后就可以記錄日志了,下面是一些常用的方法:
finest、finer、fine、info、config、warning、severe:簡潔的方法,輸出的日志為指定的級別。關(guān)于日志級別我們在后面將會詳細(xì)談到。
log:不僅可以指定消息和級別,還可以帶一些參數(shù),甚至可以直接是一個LogRecord對象(這些參數(shù)是LogRecord對象的重要組成部分)。
logp:更加精細(xì)了,不但具有l(wèi)og方法的功能,還可以不使用當(dāng)前的namespace,定義新的類名和方法名。
entering、exiting:這兩個方法在調(diào)試的時候特別管用,用來觀察一個變量變化的情況,就如同我們在VC的調(diào)試狀態(tài)下watch一個變量,然后按F10,呵呵。
2. 輸出媒介控制:Handler
日志的意義在于它可以以多種形式輸出,尤其是像文件這樣可以長久保存的媒介,這是System.out.println(...)所無法辦到的。Logging API的Handler類提供了一個處理日志記錄(LogRecord,它是對一條日志消息的封裝對象)的接口,包括幾個已實現(xiàn)的API:
ConsoleHandler:向控制臺輸出。
FileHandler:向文件輸出。
SocketHandler:向網(wǎng)絡(luò)輸出。
這三個輸出控制器都是StreamHandler的子類,另外Handler還有一個MemoryHandler的子類,它有特殊的用處,我們在后面將會看到。在程序啟動時默認(rèn)的Handler是ConsoleHandler,不過這個是可以配置的,下面會談到logging配置文件的問題。
此外用戶還可以定制自己輸出控制器,繼承Handler即可,通常只需要實現(xiàn)Handler中三個未定義的抽象方法:
publish:主要方法,把日志記錄寫入你需要的媒介。
flush:清除緩沖區(qū)并保存數(shù)據(jù)。
close:關(guān)閉控制器。
flush:清除緩沖區(qū)并保存數(shù)據(jù)。
close:關(guān)閉控制器。
通過重寫以上三個方法我們可以很容易就實現(xiàn)一個把日志寫入數(shù)據(jù)庫的控制器。
3. 自定義輸出格式:Formatter
除了可以指定輸出媒介之外,我們可能還希望有多種輸出格式,譬如可以是普通文本、HTML表格、XML等等,以滿足不同的查看需求。Logging API中的Formatter就是這樣一個提供日志記錄格式化方法接口的類。默認(rèn)提供了兩種Formatter:
SimpleFormatter:標(biāo)準(zhǔn)日志格式,就是我們通常在啟動一些諸如Tomcat、JBoss之類的服務(wù)器的時候經(jīng)常能在控制臺下看到的那種形式,就像這樣:
2004-12-20 23:08:52 org.apache.coyote.http11.Http11Protocol init
信息: Initializing Coyote HTTP/1.1 on http-8080
2004-12-20 23:08:56 org.apache.coyote.http11.Http11Protocol init
信息: Initializing Coyote HTTP/1.1 on http-8443
XMLFormatter:XML形式的日志格式,你的Logger如果add了一個new XMLFormatter(),那么在控制臺下就會看到下面這樣的形式,不過更常用的是使用上面介紹的FileHandler輸出到XML文件中:
<?xml version="1.0" encoding="GBK" standalone="no"?>
<!DOCTYPE log SYSTEM "logger.dtd">
<log>
<record>
<date>2004-12-20T23:47:56</date>
<millis>1103557676224</millis>
<sequence>0</sequence>
<logger>Test</logger>
<level>WARNING</level>
<class>Test</class>
<method>main</method>
<thread>10</thread>
<message>warning message</message>
</record>
<!DOCTYPE log SYSTEM "logger.dtd">
<log>
<record>
<date>2004-12-20T23:47:56</date>
<millis>1103557676224</millis>
<sequence>0</sequence>
<logger>Test</logger>
<level>WARNING</level>
<class>Test</class>
<method>main</method>
<thread>10</thread>
<message>warning message</message>
</record>
與Handler類似,我們也可以編寫自己的格式化處理器,譬如API里沒有將日志輸出為我們可通過瀏覽器查看的HTML表格形式的Formatter,我們只需要重寫3個方法:
format:格式化LogRecord中包含的信息。
getHead:輸出信息的頭部。
getTail:輸出信息的尾部。
getHead:輸出信息的頭部。
getTail:輸出信息的尾部。
4. 定義日志級別:Level
大家可能都知道Windows的“事件查看器”,里面有三種事件類型:“信息”、“警告”、“錯誤”。這其實就是日志級別的一種描述。Java日志級別用Level類表示,一個日志級別對應(yīng)的是一個整數(shù)值,范圍和整型值的范圍是一致的,該整數(shù)值愈大,說明警戒級別愈高。Level有9個內(nèi)置的級別,分別是:
類型 對應(yīng)的整數(shù)
OFF 最大整數(shù)(Integer.MAX_VALUE)
SEVERE 1000
WARNING 900
INFO 800
CONFIG 700
FINE 500
FINER 400
FINEST 300
ALL 最小整數(shù)(Integer.MIN_VALUE)
你也可以定義自己的日志級別,但要注意的是,不是直接創(chuàng)建Level的對象(因為它的構(gòu)造函數(shù)是protected的),而是通過繼承Level的方式,譬如:
class AlertLevel extends java.util.logging.Level
{
public AlertLevel()
{
super("ALERT", 950);
}
}
...
Logger logger = Logger.getAnonymousLogger();
logger.log(new AlertLevel(), "A dangerous action!");
{
public AlertLevel()
{
super("ALERT", 950);
}
}
...
Logger logger = Logger.getAnonymousLogger();
logger.log(new AlertLevel(), "A dangerous action!");
上面定義了一個高于WARNING但低于SEVERE的日志級別。
于是可能有朋友會興沖沖地用以下的語句來記錄一個事件:
Logger logger = Logger.getAnonymousLogger();
logger.fine("Everything seems ok.");
//或者是
//logger.log(Level.FINE, "Everything seems ok.");
logger.fine("Everything seems ok.");
//或者是
//logger.log(Level.FINE, "Everything seems ok.");
但是一程序運(yùn)行,奇怪了,怎么沒有打印出任何消息呢?下一小節(jié)我們就來談這個問題。
5. 日志過濾器:Filter
所謂過濾器是控制哪些日志該輸出哪些不該輸出的一種組件。上面你寫的那條日志沒有能在控制臺顯示出來,是因為logging API預(yù)先設(shè)定的缺省級別是INFO,也就是說只有級別不低于INFO(即其整數(shù)值不小于800)的日志才會被輸出,這個就是Filter的功能。所以我們可以看到SEVERE、WARNING、INFO以及上面我們定義的ALERT消息,但看不到FINE、FINER和FINEST消息。當(dāng)然,你盡可以用Logger的setLevel方法或者修改配置文件的方法(什么是配置文件,我們后面將會看到)來重新定義Logger輸出的最低級別。
Filter不僅僅可以按日志級別過濾,你也可以定義自己的Filter,實現(xiàn)其中的isLoggable方法,隨便按照LogRecord攜帶的任何信息進(jìn)行過濾,譬如(順便復(fù)習(xí)一下匿名類,呵呵):
Logger logger = Logger.getAnonymousLogger();
logger.setFilter(new Filter()
{
public boolean isLoggable(LogRecord rec)
{
//從LogRecord里得到過濾信息
}
});
6. 預(yù)定義參數(shù)
LogManager是一個實現(xiàn)了Singleton模式的全局對象(由于是一個唯一的對象,LogManager需要是線程安全的),它管理著程序啟動以后所有已注冊(包層次)或匿名的Logger,以及相關(guān)配置信息。這里的配置信息通常是從<JAVA_HOME>\jre\lib\logging.properties文件得到的。logging.properties對于logging API來說是一個很重要的文件,它的內(nèi)容一般是:
############################################################
# Default Logging Configuration File
#
# You can use a different file by specifying a filename
# with the java.util.logging.config.file system property.
# For example java -Djava.util.logging.config.file=myfile
############################################################
############################################################
# Global properties
############################################################
# "handlers" specifies a comma separated list of log Handler
# classes. These handlers will be installed during VM startup.
# Note that these classes must be on the system classpath.
# By default we only configure a ConsoleHandler, which will only
# show messages at the INFO and above levels.
handlers= java.util.logging.ConsoleHandler
# To also add the FileHandler, use the following line instead.
#handlers= java.util.logging.FileHandler, java.util.logging.ConsoleHandler
# Default global logging level.
# This specifies which kinds of events are logged across
# all loggers. For any given facility this global level
# can be overriden by a facility specific level
# Note that the ConsoleHandler also has a separate level
# setting to limit messages printed to the console.
.level= INFO
############################################################
# Handler specific properties.
# Describes specific configuration info for Handlers.
############################################################
# default file output is in user's home directory.
java.util.logging.FileHandler.pattern = %h/java%u.log
java.util.logging.FileHandler.limit = 50000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter
# Limit the message that are printed on the console to INFO and above.
java.util.logging.ConsoleHandler.level = INFO
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
############################################################
# Facility specific properties.
# Provides extra control for each logger.
############################################################
# For example, set the com.xyz.foo logger to only log SEVERE
# messages:
com.xyz.foo.level = SEVERE
你可以通過修改這個配置文件來改變運(yùn)行時Logger的行為,譬如:.level定義的是上面所說的默認(rèn)輸出的最低日志級別;XXXHandler相關(guān)屬性定義了各種輸出媒介等等。
這里比較有意思的是關(guān)于日志文件,也就是FileHandler,當(dāng)然,你可以在程序中創(chuàng)建一個FileHandler,然后添加到logger中:
fhd.setLevel(Level.ALL);
fhd.setFormatter(new XMLFormatter());
logger.addHandler(fhd);
fhd.setLevel(Level.ALL);
fhd.setFormatter(new XMLFormatter());
logger.addHandler(fhd);
這段代碼等價于上面logging.properties中的文字段:
java.util.logging.FileHandler.limit = 50000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter
java.util.logging.FileHandler.limit = 50000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter
這里的pattern代表用轉(zhuǎn)義字符定義的一個日志文件名: 轉(zhuǎn)義字符串 含義 %t 臨時目錄 %h 用戶目錄,即系統(tǒng)屬性“user.home”對應(yīng)的值 %g 一個隨機(jī)生成的數(shù)字,可以重復(fù) %u 一個隨機(jī)生成的非重復(fù)數(shù)字
以上面的“%h/java%u.log”為例,在Windows 2000下代表日志文件可能就是:C:\Documents and Settings\Administrator\javax.log。這里x代表一個不重復(fù)的數(shù)字,如果是第一次,那么就是java0.log;如果在該目錄下已經(jīng)存在了一個java0.log的文件,那么logger就產(chǎn)生一個java1.log的新的日志文件。
當(dāng)然,你可以在別的地方使用自己寫的配置文件,不過在啟動程序時候需要指定java.logging.config.file屬性:
以上面的“%h/java%u.log”為例,在Windows 2000下代表日志文件可能就是:C:\Documents and Settings\Administrator\javax.log。這里x代表一個不重復(fù)的數(shù)字,如果是第一次,那么就是java0.log;如果在該目錄下已經(jīng)存在了一個java0.log的文件,那么logger就產(chǎn)生一個java1.log的新的日志文件。
當(dāng)然,你可以在別的地方使用自己寫的配置文件,不過在啟動程序時候需要指定java.logging.config.file屬性:
7. 資源與本地化
Logger里還有個方法叫logrb,可能初學(xué)者不太會用到。如果你安裝的JDK是國際版的,那么你將會看到在中文Windows平臺下日志輸出的INFO、WARNING顯示的是“信息”、“警告”等中文字樣。因為logrb是一個和Java i18n/l10n相關(guān)的方法,你可以定義自己的“資源包”(Resource Bundle),然后在logrb方法中指定相應(yīng)的資源名稱,那么在輸出日志中你就能看到用自己定義的本地語言、時間等顯示的信息。如果你對i18n/l10n感興趣,可以參考Java Localization文檔。
了解以上組件后,我們回顧一個完整的日志處理的工作過程:
程序啟動日志服務(wù),創(chuàng)建Logger對象,LogManager按照namespace的層次結(jié)構(gòu)組織Logger,在同一個namespace里子Logger將繼承父Logger的屬性;同時,LogManager從logging.properties中讀取相應(yīng)的屬性對Logger進(jìn)行初始化,如果在程序中設(shè)置了屬性則使用新的配置。當(dāng)應(yīng)用程序產(chǎn)生一條日志,Logger將創(chuàng)建一個LogRecord對象,該對象封裝了一條日志的全部信息。Logger需要根據(jù)當(dāng)前設(shè)置的Filter來判斷這條日志是否需要輸出,并將有用的日志傳給相應(yīng)的Handler處理,而Handler根據(jù)當(dāng)前設(shè)置的Formatter和Resource Bundle將日志消息轉(zhuǎn)換成一定的顯示格式,然后輸出到預(yù)定的媒介(控制臺、文件等)中去。整個過程大致如圖1所示:
圖1
前面我們在介紹Handler的時候提到過一個特殊的類叫MemoryHandler,這里我們要了解一下“Handler鏈”的概念,日志在輸出之前可能經(jīng)過多個Handler的處理,MemoryHandler在這種情況下就是一個中間角色,它維持一個內(nèi)存中的日志緩沖區(qū),當(dāng)日志沒有填滿緩沖區(qū)時就將全部日志送到下一個Handler,否則新進(jìn)來的日志將會覆蓋最老的那些日志,因此,使用MemoryHandler可以維護(hù)一定容量的日志,另外,MemoryHandler也可以不需要使用Formatter來進(jìn)行格式化,從而具有較高的效率。一個使用Handler鏈的例子如圖2所示:

圖2
青出于藍(lán):Apache Jakarta log4j日志工具包
應(yīng)付日常的日志需求,J2SE的Logging API可以說已經(jīng)做得相當(dāng)出色了,但追求完美的開發(fā)人員可能需要可擴(kuò)展性更好的專業(yè)日志處理工具,log4j正是當(dāng)前比較流行的一個工具包,它提供更多的輸出媒介、輸出格式和配置選擇,你會發(fā)現(xiàn)原來在J2SE里一些仍需要自己手工構(gòu)建的功能在log4j當(dāng)中都已經(jīng)為你實現(xiàn)了。關(guān)于log4j我可能談得不會太多,可以看看文后所附的“參考資料”,網(wǎng)上也有很詳細(xì)的介紹,我在這里做的是一個對比,因為log4j和J2SE 1.4 Logging API的用法是很相似的,一些名稱不同的組件你會發(fā)現(xiàn)他們所處的地位其實是一樣的:
J2SE 1.4中的類 log4j中的類
日志記錄器 Logger Logger
日志管理器 LogManager LogManager
日志對象 LogRecord LoggingEvent
輸出媒介控制 Handler Appender
格式化 Formatter Layout
級別 Level Level
過濾器 Filter Filter
log4j可以做到更精細(xì)更完善的控制,譬如J2SE里沒有現(xiàn)成向數(shù)據(jù)庫里寫日志的方法,但log4j卻有JDBCAppender,它甚至還能向GUI圖形界面(LF5Appender,一種以JTree方式顯示的層次結(jié)構(gòu))、Windows NT事件查看器(NTEventLogAppender)、UNIX的syslogd服務(wù)(SyslogAppender)、電子郵箱(SMTPAppender)、Telnet終端(TelnetAppender)、JMS消息(JMSAppender)輸出日志,牛吧;J2SE里默認(rèn)只能用%JAVA_HOME%\jre\lib\logging.properties做配置文件,但log4j卻可以在代碼中設(shè)置其它路徑下的properties文件或XML格式的配置文件。log4j的其它方面同樣很豐富,總之,log4j的最大的特點就是“靈活”,無論是Appender、Layout還是Configurator,你可以把日志輕松地弄成幾乎任何你想要的形式。
框架與標(biāo)準(zhǔn):JSR議案
從時間順序上講,log4j要比J2SE Logging API來得早,很多概念都是log4j先有的,但成為一個標(biāo)準(zhǔn),則是在JSR 47的形成。可能有人還不太了解JSR,這還要談到JCP,即“Java Community Process”,它是一個于1998年成立的旨在為Java技術(shù)制定民間標(biāo)準(zhǔn)的開放組織,你可以通過http://www.jcp.org/en/participation/membership申請成為它的付費或免費會員,JCP的主要工作就是制定和發(fā)布JSR(Java Specification Requests),JSR對于Java的意義就相當(dāng)于RFC對于網(wǎng)絡(luò)技術(shù)的意義,由于JCP會員們的集思廣益,使得JSR成為Java界的一個重要標(biāo)準(zhǔn)。JSR 47即“Logging API Specification”,制定了調(diào)試和日志框架,J2SE Logging API正是該框架的一個實現(xiàn)。由于種種原因,在JSR 47出來以前,log4j就已經(jīng)成為一項成熟的技術(shù),使得log4j在選擇上占據(jù)了一定的優(yōu)勢,但不能因此就說JSR 47是過時的規(guī)范,標(biāo)準(zhǔn)總是在發(fā)展的嘛!
并不是全部:其它日志處理工具
除了J2SE Logging API和log4j,日志處理方面還有別的技術(shù):Jakarta的commons組件項目中的JCL(Jakarta Commons Logging)是一個不錯的選擇,它有點類似于GSS-API(通用安全服務(wù)接口)中的思想,其日志服務(wù)機(jī)制是可以替換的,也就是說既可以用J2SE Logging API也可以用log4j,但JCL對開發(fā)人員提供一致的接口,這一點相當(dāng)重要,組件可重用正是Jakarta Commons項目追求的一個目標(biāo);IBM的JLog也是在J2SE Logging API之前推出的一個工具包,但JLog是一個商業(yè)產(chǎn)品。
至于日志API的應(yīng)用那可就多了,現(xiàn)在哪個大一點的工具或平臺不用到日志模塊呢?Tomcat、JBoss……
說了這么多,我們無非需要知道的一件事就是,“調(diào)試”也是一門學(xué)問。在我們一個勁地用System.out.println(...)而且用得很爽的時候,也應(yīng)該想想看,如何讓這樣一條菜鳥語句也能變得人性化和豐富多彩。
- Java Logging Documentation
- Java Logging APIs
- J2SE進(jìn)階, by www.javaresearch.org
- Short introduction to log4j, by Ceki Gülcü
- log4j APIs
- FAQ about log4j