
????用慣了VC的人剛接觸Java大概很不習慣代碼的調試,的確,在M$的大部分IDE都做得相當出色,包括像VJ++這樣一直被Java程序員稱為是“垃圾”的類庫(記得以前在瀚海星云的Java版提有關VJ問題的人是有可能被封的,^_^),它的開發工具在調試上都相當容易。Java也有命令行方式的調試和IDE的調試,但現在的像JB這樣的玩意又是個龐然大物,低配置的機器可能就是個奢望,不像VC那樣。怎么辦呢,高手們說,“我的jdb用得賊熟練”,那我會報以景仰的目光,像我這樣的菜鳥基本上就沒使過jdb,還是老老實實在代碼里面System.out.println(...)。直到1996年一個叫做“歐洲安全電子市場”(E.U. SEMPER)的項目啟動,“調試”不再是一件“體力活”,而是一種軟件設計的藝術,這個項目組開發的日志管理接口后來成為ApacheJakarta項目中的一員,它就是現在我們所熟悉的log4j。下面的文字將概要介紹與Java日志記錄相關的一些技術,目的不是讓您放棄老土的System.out.println(...),而是說,在Java的世界里可以有許多種選擇,你今天覺得掌握了一件高級武器,明天可能就是“過時”的了,呵呵。
始祖:System.out.println(...)
????為什么還是要一再提到它?畢竟我們的習慣不是那么容易改變的,而且System.out(別忘了還有System.err)是一個直接和控制臺打交道的PrintStream對象,是終端顯示的基礎,高級的Logger要在終端顯示日志內容,就必然會用到這個。一個小規模的程序調試,恰當地使用System.out.println(...)我認為仍然是一種最方便最有效的方法,所以我們仍把它放在最開始,以示不能“數典忘祖” :)
不常用的關鍵字:assert
????assert對多數人來講可能還比較陌生,它也是一個調試工具,好像是J2SE 1.4才加進來的東東,一種常見的用法是:
?assert (布爾表達式);
????當表達式為true時沒有任何反映,如果為false系統將會拋出一個AssertionError。如果你要使用assert,在編譯時必須加上“-source 1.4”的選項,在運行時則要加上“-ea”選項。
后生可畏:Java Logging API一瞥
????System.out.println(...)對于較高要求的用戶是遠遠不夠的,它還不是一個日志系統,一個比較完善的日志系統應當有輸出媒介、優先級、格式化、日志過濾、日志管理、參數配置等功能。伴隨J2SE 1.4一起發布的Java日志包java.util.logging適時地滿足了我們的初步需求,在程序中按一定格式顯示和記錄豐富的調試信息已經是一件相當easy的事情。
1. 日志記錄器:Logger
????Logger是一個直接面向用戶的日志功能調用接口,從用戶的角度上看,它完成大部分日志記錄工作,通常你得到一個Logger對象,只需要使用一些簡單方法,譬如info,warning,log,logp,logrb等就能完成任務,簡單到和System.out.println(...)一樣只用一條語句,但后臺可能在向控制臺,向文件,向數據庫,甚至向網絡同時輸出該信息,而這個過程對用戶是完全透明的。
????在使用Logger之前,首先需要通過getLogger()或getAnonymousLogger()靜態方法得到一個Logger對象(想想看,這里是不是設計模式當中的“工廠方法”的一個實實在在的應用?可以參考一下Logger的源代碼,你就明白LogManager是“工廠類”而Logger是“產品類”,凡事都要學以致用嘛,呵呵)。這里我們需要了解的是Logger的“名字空間”(namespace)的概念:通常我們調試時需要清楚地知道某個變量是出現在什么位置,精確到哪個類的哪個方法,namespace就是這么個用處。我們用getLogger()得到Logger時需要指定這個Logger的名字空間,通常是一個包名,譬如“com.jungleford.test”等,如果是指定了namespace,那么將在一個全局對象LogManager中注冊這個namespace,Logger會基于namespace形成層次關系,譬如namespace為“com.jungleford”的Logger就是namespace為“com.jungleford.test”的Logger的父,后者調用getParent()方法將返回前者,如果當前沒有namespace為“com.jungleford”的Logger,則查找namespace為“com”的Logger,要是按照這個鏈找不到就返回根Logger,其namespace為"",根Logger的父是null。從理論上說,這個namespace可以是任意的,通常我們是按所調試的對象來定,但如果你是使用getAnonymousLogger()方法產生的Logger,那它就沒有namespace,這個“匿名Logger”的父是根Logger。
????得到一個Logger對象后就可以記錄日志了,下面是一些常用的方法:
finest、finer、fine、info、config、warning、severe:簡潔的方法,輸出的日志為指定的級別。關于日志級別我們在后面將會詳細談到。
log:不僅可以指定消息和級別,還可以帶一些參數,甚至可以直接是一個LogRecord對象(這些參數是LogRecord對象的重要組成部分)。
logp:更加精細了,不但具有log方法的功能,還可以不使用當前的namespace,定義新的類名和方法名。
entering、exiting:這兩個方法在調試的時候特別管用,用來觀察一個變量變化的情況,就如同我們在VC的調試狀態下watch一個變量,然后按F10,呵呵。
2. 輸出媒介控制:Handler
????日志的意義在于它可以以多種形式輸出,尤其是像文件這樣可以長久保存的媒介,這是System.out.println(...)所無法辦到的。Logging API的Handler類提供了一個處理日志記錄(LogRecord,它是對一條日志消息的封裝對象)的接口,包括幾個已實現的API:
ConsoleHandler:向控制臺輸出。
FileHandler:向文件輸出。
SocketHandler:向網絡輸出。
????這三個輸出控制器都是StreamHandler的子類,另外Handler還有一個MemoryHandler的子類,它有特殊的用處,我們在后面將會看到。在程序啟動時默認的Handler是ConsoleHandler,不過這個是可以配置的,下面會談到logging配置文件的問題。
????此外用戶還可以定制自己輸出控制器,繼承Handler即可,通常只需要實現Handler中三個未定義的抽象方法:
publish:主要方法,把日志記錄寫入你需要的媒介。
flush:清除緩沖區并保存數據。
close:關閉控制器。
flush:清除緩沖區并保存數據。
close:關閉控制器。
????通過重寫以上三個方法我們可以很容易就實現一個把日志寫入數據庫的控制器。
3. 自定義輸出格式:Formatter
????除了可以指定輸出媒介之外,我們可能還希望有多種輸出格式,譬如可以是普通文本、HTML表格、XML等等,以滿足不同的查看需求。Logging API中的Formatter就是這樣一個提供日志記錄格式化方法接口的類。默認提供了兩種Formatter:
SimpleFormatter:標準日志格式,就是我們通常在啟動一些諸如Tomcat、JBoss之類的服務器的時候經常能在控制臺下看到的那種形式,就像這樣:
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類表示,一個日志級別對應的是一個整數值,范圍和整型值的范圍是一致的,該整數值愈大,說明警戒級別愈高。Level有9個內置的級別,分別是:
類型?? ????? ?對應的整數
OFF????? ??? 最大整數(Integer.MAX_VALUE)
SEVERE??? ?1000
WARNING?? 900
INFO???????? 800
CONFIG????? 700
FINE???????? ?500
FINER????????400
FINEST????? 300
ALL????????? ?最小整數(Integer.MIN_VALUE)
????你也可以定義自己的日志級別,但要注意的是,不是直接創建Level的對象(因為它的構造函數是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.");
????但是一程序運行,奇怪了,怎么沒有打印出任何消息呢?下一小節我們就來談這個問題。
5. 日志過濾器:Filter
????所謂過濾器是控制哪些日志該輸出哪些不該輸出的一種組件。上面你寫的那條日志沒有能在控制臺顯示出來,是因為logging API預先設定的缺省級別是INFO,也就是說只有級別不低于INFO(即其整數值不小于800)的日志才會被輸出,這個就是Filter的功能。所以我們可以看到SEVERE、WARNING、INFO以及上面我們定義的ALERT消息,但看不到FINE、FINER和FINEST消息。當然,你盡可以用Logger的setLevel方法或者修改配置文件的方法(什么是配置文件,我們后面將會看到)來重新定義Logger輸出的最低級別。
????Filter不僅僅可以按日志級別過濾,你也可以定義自己的Filter,實現其中的isLoggable方法,隨便按照LogRecord攜帶的任何信息進行過濾,譬如(順便復習一下匿名類,呵呵):
Logger logger = Logger.getAnonymousLogger();
logger.setFilter(newFilter()
{
??publicboolean isLoggable(LogRecord rec)
??{
????//從LogRecord里得到過濾信息
??}
});
6. 預定義參數
????LogManager是一個實現了Singleton模式的全局對象(由于是一個唯一的對象,LogManager需要是線程安全的),它管理著程序啟動以后所有已注冊(包層次)或匿名的Logger,以及相關配置信息。這里的配置信息通常是從<JAVA_HOME>\jre\lib\logging.properties文件得到的。logging.properties對于logging API來說是一個很重要的文件,它的內容一般是:
############################################################
# 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
????你可以通過修改這個配置文件來改變運行時Logger的行為,譬如:.level定義的是上面所說的默認輸出的最低日志級別;XXXHandler相關屬性定義了各種輸出媒介等等。
????這里比較有意思的是關于日志文件,也就是FileHandler,當然,你可以在程序中創建一個FileHandler,然后添加到logger中:
fhd.setLevel(Level.ALL);
fhd.setFormatter(newXMLFormatter());
logger.addHandler(fhd);
fhd.setLevel(Level.ALL);
fhd.setFormatter(newXMLFormatter());
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代表用轉義字符定義的一個日志文件名: 轉義字符串含義%t臨時目錄%h用戶目錄,即系統屬性“user.home”對應的值%g一個隨機生成的數字,可以重復%u一個隨機生成的非重復數字
????以上面的“%h/java%u.log”為例,在Windows 2000下代表日志文件可能就是:C:\Documents and Settings\Administrator\javax.log。這里x代表一個不重復的數字,如果是第一次,那么就是java0.log;如果在該目錄下已經存在了一個java0.log的文件,那么logger就產生一個java1.log的新的日志文件。
????當然,你可以在別的地方使用自己寫的配置文件,不過在啟動程序時候需要指定java.logging.config.file屬性:
????以上面的“%h/java%u.log”為例,在Windows 2000下代表日志文件可能就是:C:\Documents and Settings\Administrator\javax.log。這里x代表一個不重復的數字,如果是第一次,那么就是java0.log;如果在該目錄下已經存在了一個java0.log的文件,那么logger就產生一個java1.log的新的日志文件。
????當然,你可以在別的地方使用自己寫的配置文件,不過在啟動程序時候需要指定java.logging.config.file屬性:
7. 資源與本地化
????Logger里還有個方法叫logrb,可能初學者不太會用到。如果你安裝的JDK是國際版的,那么你將會看到在中文Windows平臺下日志輸出的INFO、WARNING顯示的是“信息”、“警告”等中文字樣。因為logrb是一個和Java i18n/l10n相關的方法,你可以定義自己的“資源包”(Resource Bundle),然后在logrb方法中指定相應的資源名稱,那么在輸出日志中你就能看到用自己定義的本地語言、時間等顯示的信息。如果你對i18n/l10n感興趣,可以參考Java Localization文檔。
????了解以上組件后,我們回顧一個完整的日志處理的工作過程:
????程序啟動日志服務,創建Logger對象,LogManager按照namespace的層次結構組織Logger,在同一個namespace里子Logger將繼承父Logger的屬性;同時,LogManager從logging.properties中讀取相應的屬性對Logger進行初始化,如果在程序中設置了屬性則使用新的配置。當應用程序產生一條日志,Logger將創建一個LogRecord對象,該對象封裝了一條日志的全部信息。Logger需要根據當前設置的Filter來判斷這條日志是否需要輸出,并將有用的日志傳給相應的Handler處理,而Handler根據當前設置的Formatter和Resource Bundle將日志消息轉換成一定的顯示格式,然后輸出到預定的媒介(控制臺、文件等)中去。整個過程大致如圖1所示:
????????????????????????????????????????????????? 圖1
????前面我們在介紹Handler的時候提到過一個特殊的類叫MemoryHandler,這里我們要了解一下“Handler鏈”的概念,日志在輸出之前可能經過多個Handler的處理,MemoryHandler在這種情況下就是一個中間角色,它維持一個內存中的日志緩沖區,當日志沒有填滿緩沖區時就將全部日志送到下一個Handler,否則新進來的日志將會覆蓋最老的那些日志,因此,使用MemoryHandler可以維護一定容量的日志,另外,MemoryHandler也可以不需要使用Formatter來進行格式化,從而具有較高的效率。一個使用Handler鏈的例子如圖2所示:

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