jungleford如是說(shuō)
用慣了VC的人剛接觸Java大概很不習(xí)慣代碼的調(diào)試,的確,在M$的大部分IDE都做得相當(dāng)出色,包括像VJ++這樣一直被Java程序員稱為是“垃圾”的類庫(kù)(記得以前在瀚海星云的Java版提有關(guān)VJ問(wèn)題的人是有可能被封的,^_^),它的開發(fā)工具在調(diào)試上都相當(dāng)容易。Java也有命令行方式的調(diào)試和IDE的調(diào)試,但現(xiàn)在的像JB這
樣的玩意又是個(gè)龐然大物,低配置的機(jī)器可能就是個(gè)奢望,不像VC那樣。怎么辦呢,高手們說(shuō),“我的jdb用得賊熟練”,那我會(huì)報(bào)以景仰的目光,像我這樣的
菜鳥基本上就沒(méi)使過(guò)jdb,還是老老實(shí)實(shí)在代碼里面System.out.println(...)。直到1996年一個(gè)叫做“歐洲安全電子市場(chǎng)”
(E.U. SEMPER)的項(xiàng)目啟動(dòng),“調(diào)試”不再是一件“體力活”,而是一種軟件設(shè)計(jì)的藝術(shù),這個(gè)項(xiàng)目組開發(fā)的日志管理接口后來(lái)成為ApacheJakarta項(xiàng)目中的一員,它就是現(xiàn)在我們所熟悉的log4j。下面的文字將概要介紹與Java日志記錄相關(guān)的一些技術(shù),目的不是讓您放棄老土的System.out.println(...),而是說(shuō),在Java的世界里可以有許多種選擇,你今天覺(jué)得掌握了一件高級(jí)武器,明天可能就是“過(guò)時(shí)”的了,呵呵。
始祖:System.out.println(...)
為什么還是要一再提到它?畢竟我們的習(xí)慣不是那么容易改變的,而且System.out(別忘了還有System.err)是一個(gè)直接和控制臺(tái)打交道的PrintStream對(duì)象,是終端顯示的基礎(chǔ),高級(jí)的Logger要在終端顯示日志內(nèi)容,就必然會(huì)用到這個(gè)。一個(gè)小規(guī)模的程序調(diào)試,恰當(dāng)?shù)厥褂肧ystem.out.println(...)我認(rèn)為仍然是一種最方便最有效的方法,所以我們?nèi)园阉旁谧铋_始,以示不能“數(shù)典忘祖” :)
不常用的關(guān)鍵字:assert
assert對(duì)多數(shù)人來(lái)講可能還比較陌生,它也是一個(gè)調(diào)試工具,好像是J2SE 1.4才加進(jìn)來(lái)的東東,一種常見(jiàn)的用法是:
當(dāng)表達(dá)式為true時(shí)沒(méi)有任何反映,如果為false系統(tǒng)將會(huì)拋出一個(gè)AssertionError。如果你要使用assert,在編譯時(shí)必須加上“-source 1.4”的選項(xiàng),在運(yùn)行時(shí)則要加上“-ea”選項(xiàng)。
后生可畏:Java Logging API一瞥
System.out.println(...)對(duì)于較高要求的用戶是遠(yuǎn)遠(yuǎn)不夠的,它還不是一個(gè)日志系統(tǒng),一個(gè)比較完善的日志系統(tǒng)應(yīng)當(dāng)有輸出媒介、優(yōu)先級(jí)、格式化、日志過(guò)濾、日志管理、參數(shù)配置等功能。伴隨J2SE 1.4一起發(fā)布的Java日志包java.util.logging適時(shí)地滿足了我們的初步需求,在程序中按一定格式顯示和記錄豐富的調(diào)試信息已經(jīng)是一件相當(dāng)easy的事情。
1. 日志記錄器:Logger
Logger是
一個(gè)直接面向用戶的日志功能調(diào)用接口,從用戶的角度上看,它完成大部分日志記錄工作,通常你得到一個(gè)Logger對(duì)象,只需要使用一些簡(jiǎn)單方法,譬如
info,warning,log,logp,logrb等就能完成任務(wù),簡(jiǎn)單到和System.out.println(...)一樣只用一條語(yǔ)句,但
后臺(tái)可能在向控制臺(tái),向文件,向數(shù)據(jù)庫(kù),甚至向網(wǎng)絡(luò)同時(shí)輸出該信息,而這個(gè)過(guò)程對(duì)用戶是完全透明的。
在使用Logger之前,首先需要通過(guò)getLogger()或getAnonymousLogger()靜態(tài)方法得到一個(gè)Logger對(duì)象(想想看,這里是不是設(shè)計(jì)模式當(dāng)中的“工廠方法”的一個(gè)實(shí)實(shí)在在的應(yīng)用?可以參考一下Logger的源代碼,你就明白LogManager是“工廠類”而Logger是“產(chǎn)品類”,凡事都要學(xué)以致用嘛,呵呵)。這里我們需要了解的是Logger的“名字空間”(namespace)
的概念:通常我們調(diào)試時(shí)需要清楚地知道某個(gè)變量是出現(xiàn)在什么位置,精確到哪個(gè)類的哪個(gè)方法,namespace就是這么個(gè)用處。我們用getLogger
()得到Logger時(shí)需要指定這個(gè)Logger的名字空間,通常是一個(gè)包名,譬如“com.jungleford.test”等,如果是指定了
namespace,那么將在一個(gè)全局對(duì)象LogManager中注冊(cè)這個(gè)namespace,Logger會(huì)基于namespace形成層次關(guān)系,譬如
namespace為“com.jungleford”的Logger就是namespace為“com.jungleford.test”的
Logger的父,后者調(diào)用getParent()
方法將返回前者,如果當(dāng)前沒(méi)有namespace為“com.jungleford”的Logger,則查找namespace為“com”的
Logger,要是按照這個(gè)鏈找不到就返回根Logger,其namespace為"",根Logger的父是null。從理論上說(shuō),這個(gè)
namespace可以是任意的,通常我們是按所調(diào)試的對(duì)象來(lái)定,但如果你是使用getAnonymousLogger()方法產(chǎn)生的Logger,那它
就沒(méi)有namespace,這個(gè)“匿名Logger”的父是根Logger。
得到一個(gè)Logger對(duì)象后就可以記錄日志了,下面是一些常用的方法:
finest、finer、fine、info、config、warning、severe:簡(jiǎn)潔的方法,輸出的日志為指定的級(jí)別。關(guān)于日志級(jí)別我們?cè)诤竺鎸?huì)詳細(xì)談到。
log:不僅可以指定消息和級(jí)別,還可以帶一些參數(shù),甚至可以直接是一個(gè)LogRecord對(duì)象(這些參數(shù)是LogRecord對(duì)象的重要組成部分)。
logp:更加精細(xì)了,不但具有l(wèi)og方法的功能,還可以不使用當(dāng)前的namespace,定義新的類名和方法名。
entering、exiting:這兩個(gè)方法在調(diào)試的時(shí)候特別管用,用來(lái)觀察一個(gè)變量變化的情況,就如同我們?cè)赩C的調(diào)試狀態(tài)下watch一個(gè)變量,然后按F10,呵呵。 |
2. 輸出媒介控制:Handler
日志的意義在于它可以以多種形式輸出,尤其是像文件這樣可以長(zhǎng)久保存的媒介,這是System.out.println(...)所無(wú)法辦到的。Logging API的Handler類提供了一個(gè)處理日志記錄(LogRecord,它是對(duì)一條日志消息的封裝對(duì)象)的接口,包括幾個(gè)已實(shí)現(xiàn)的API:
這三個(gè)輸出控制器都是StreamHandler的子類,另外Handler還有一個(gè)MemoryHandler的子類,它有特殊的用處,我們?cè)诤竺鎸?huì)看到。在程序啟動(dòng)時(shí)默認(rèn)的Handler是ConsoleHandler,不過(guò)這個(gè)是可以配置的,下面會(huì)談到logging配置文件的問(wèn)題。
此外用戶還可以定制自己輸出控制器,繼承Handler即可,通常只需要實(shí)現(xiàn)Handler中三個(gè)未定義的抽象方法:
通過(guò)重寫以上三個(gè)方法我們可以很容易就實(shí)現(xiàn)一個(gè)把日志寫入數(shù)據(jù)庫(kù)的控制器。
3. 自定義輸出格式:Formatter
除了可以指定輸出媒介之外,我們可能還希望有多種輸出格式,譬如可以是普通文本、HTML表格、XML等等,以滿足不同的查看需求。Logging API中的Formatter就是這樣一個(gè)提供日志記錄格式化方法接口的類。默認(rèn)提供了兩種Formatter:
SimpleFormatter:標(biāo)準(zhǔn)日志格式,就是我們通常在啟動(dòng)一些諸如Tomcat、JBoss之類的服務(wù)器的時(shí)候經(jīng)常能在控制臺(tái)下看到的那種形式,就像這樣:
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了一個(gè)new XMLFormatter(),那么在控制臺(tái)下就會(huì)看到下面這樣的形式,不過(guò)更常用的是使用上面介紹的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> |
與Handler類似,我們也可以編寫自己的格式化處理器,譬如API里沒(méi)有將日志輸出為我們可通過(guò)瀏覽器查看的HTML表格形式的Formatter,我們只需要重寫3個(gè)方法:
4. 定義日志級(jí)別:Level
大家可能都知道Windows的“事件查看器”,里面有三種事件類型:“信息”、“警告”、“錯(cuò)誤”。這其實(shí)就是日志級(jí)別的一種描述。Java日志級(jí)別用Level類表示,一個(gè)日志級(jí)別對(duì)應(yīng)的是一個(gè)整數(shù)值,范圍和整型值的范圍是一致的,該整數(shù)值愈大,說(shuō)明警戒級(jí)別愈高。Level有9個(gè)內(nèi)置的級(jí)別,分別是:
你也可以定義自己的日志級(jí)別,但要注意的是,不是直接創(chuàng)建Level的對(duì)象(因?yàn)樗臉?gòu)造函數(shù)是protected的),而是通過(guò)繼承Level的方式,譬如:
class
AlertLevel extends java.util.logging.Level { public AlertLevel() { super("ALERT", 950); } } ... Logger logger = Logger.getAnonymousLogger(); logger.log(new AlertLevel(), "A dangerous action!");
|
上面定義了一個(gè)高于WARNING但低于SEVERE的日志級(jí)別。
于是可能有朋友會(huì)興沖沖地用以下的語(yǔ)句來(lái)記錄一個(gè)事件:
Logger
logger = Logger.getAnonymousLogger(); logger.fine("Everything seems ok."); //或者是 //logger.log(Level.FINE, "Everything seems ok.");
|
但是一程序運(yùn)行,奇怪了,怎么沒(méi)有打印出任何消息呢?下一小節(jié)我們就來(lái)談這個(gè)問(wèn)題。
5. 日志過(guò)濾器:Filter
所謂過(guò)濾器是控制哪些日志該輸出哪些不該輸出的一種組件。上面你寫的那條日志沒(méi)有能在控制臺(tái)顯示出來(lái),是因?yàn)閘ogging
API預(yù)先設(shè)定的缺省級(jí)別是INFO,也就是說(shuō)只有級(jí)別不低于INFO(即其整數(shù)值不小于800)的日志才會(huì)被輸出,這個(gè)就是Filter的功能。所以我
們可以看到SEVERE、WARNING、INFO以及上面我們定義的ALERT消息,但看不到FINE、FINER和FINEST消息。當(dāng)然,你盡可以
用Logger的setLevel方法或者修改配置文件的方法(什么是配置文件,我們后面將會(huì)看到)來(lái)重新定義Logger輸出的最低級(jí)別。
Filter不僅僅可以按日志級(jí)別過(guò)濾,你也可以定義自己的Filter,實(shí)現(xiàn)其中的isLoggable方法,隨便按照LogRecord攜帶的任何信息進(jìn)行過(guò)濾,譬如(順便復(fù)習(xí)一下匿名類,呵呵):
Logger
logger = Logger.getAnonymousLogger(); logger.setFilter(newFilter() { publicboolean isLoggable(LogRecord rec) { //從LogRecord里得到過(guò)濾信息 } });
|
6. 預(yù)定義參數(shù)
LogManager是一個(gè)實(shí)現(xiàn)了Singleton模式的全局對(duì)象(由于是一個(gè)唯一的對(duì)象,LogManager需要是線程安全的),它管理著程序啟動(dòng)以后所有已注冊(cè)(包層次)或匿名的Logger,以及相關(guān)配置信息。這里的配置信息通常是從<JAVA_HOME>\jre\lib\logging.properties文件得到的。logging.properties對(duì)于logging API來(lái)說(shuō)是一個(gè)很重要的文件,它的內(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
|
你可以通過(guò)修改這個(gè)配置文件來(lái)改變運(yùn)行時(shí)Logger的行為,譬如:.level定義的是上面所說(shuō)的默認(rèn)輸出的最低日志級(jí)別;XXXHandler相關(guān)屬性定義了各種輸出媒介等等。
這里比較有意思的是關(guān)于日志文件,也就是FileHandler,當(dāng)然,你可以在程序中創(chuàng)建一個(gè)FileHandler,然后添加到logger中:
FileHandler fhd = newFileHandler("%h/java%u.log", 5000, 1, true); fhd.setLevel(Level.ALL); fhd.setFormatter(newXMLFormatter()); logger.addHandler(fhd); |
這段代碼等價(jià)于上面logging.properties中的文字段:
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 |
這里的pattern代表用轉(zhuǎn)義字符定義的一個(gè)日志文件名:
轉(zhuǎn)義字符串 | 含義 |
%t |
臨時(shí)目錄 |
%h |
用戶目錄,即系統(tǒng)屬性“user.home”對(duì)應(yīng)的值 |
%g |
一個(gè)隨機(jī)生成的數(shù)字,可以重復(fù) |
%u |
一個(gè)隨機(jī)生成的非重復(fù)數(shù)字 |
以上面的“%h/java%u.log”為例,在Windows 2000下代表日志文件可能就是:C:\Documents and Settings\Administrator\javax.log。這里x代表一個(gè)不重復(fù)的數(shù)字,如果是第一次,那么就是java0.log;如果在該目錄下已經(jīng)存在了一個(gè)java0.log的文件,那么logger就產(chǎn)生一個(gè)java1.log的新的日志文件。
當(dāng)然,你可以在別的地方使用自己寫的配置文件,不過(guò)在啟動(dòng)程序時(shí)候需要指定java.logging.config.file屬性:
java -Djava.logging.config.file=... |
7. 資源與本地化
Logger里還有個(gè)方法叫logrb,
可能初學(xué)者不太會(huì)用到。如果你安裝的JDK是國(guó)際版的,那么你將會(huì)看到在中文Windows平臺(tái)下日志輸出的INFO、WARNING顯示的是“信息”、
“警告”等中文字樣。因?yàn)閘ogrb是一個(gè)和Java i18n/l10n相關(guān)的方法,你可以定義自己的“資源包”(Resource
Bundle),然后在logrb方法中指定相應(yīng)的資源名稱,那么在輸出日志中你就能看到用自己定義的本地語(yǔ)言、時(shí)間等顯示的信息。如果你對(duì)
i18n/l10n感興趣,可以參考Java Localization文檔。
了解以上組件后,我們回顧一個(gè)完整的日志處理的工作過(guò)程:
程序啟動(dòng)日志服務(wù),創(chuàng)建Logger對(duì)象,LogManager按照namespace的層次結(jié)構(gòu)組織Logger,在同一個(gè)namespace里子
Logger將繼承父Logger的屬性;同時(shí),LogManager從logging.properties中讀取相應(yīng)的屬性對(duì)Logger進(jìn)行初始
化,如果在程序中設(shè)置了屬性則使用新的配置。當(dāng)應(yīng)用程序產(chǎn)生一條日志,Logger將創(chuàng)建一個(gè)LogRecord對(duì)象,該對(duì)象封裝了一條日志的全部信息。
Logger需要根據(jù)當(dāng)前設(shè)置的Filter來(lái)判斷這條日志是否需要輸出,并將有用的日志傳給相應(yīng)的Handler處理,而Handler根據(jù)當(dāng)前設(shè)置的
Formatter和Resource
Bundle將日志消息轉(zhuǎn)換成一定的顯示格式,然后輸出到預(yù)定的媒介(控制臺(tái)、文件等)中去。整個(gè)過(guò)程大致如圖1所示:
圖1
前面我們?cè)诮榻BHandler的時(shí)候提到過(guò)一個(gè)特殊的類叫MemoryHandler,
這里我們要了解一下“Handler鏈”的概念,日志在輸出之前可能經(jīng)過(guò)多個(gè)Handler的處理,MemoryHandler在這種情況下就是一個(gè)中間
角色,它維持一個(gè)內(nèi)存中的日志緩沖區(qū),當(dāng)日志沒(méi)有填滿緩沖區(qū)時(shí)就將全部日志送到下一個(gè)Handler,否則新進(jìn)來(lái)的日志將會(huì)覆蓋最老的那些日志,因此,使
用MemoryHandler可以維護(hù)一定容量的日志,另外,MemoryHandler也可以不需要使用Formatter來(lái)進(jìn)行格式化,從而具有較高
的效率。一個(gè)使用Handler鏈的例子如圖2所示:
圖2
青出于藍(lán):Apache Jakarta log4j日志工具包
應(yīng)付日常的日志需求,J2SE的Logging
API可以說(shuō)已經(jīng)做得相當(dāng)出色了,但追求完美的開發(fā)人員可能需要可擴(kuò)展性更好的專業(yè)日志處理工具,log4j正是當(dāng)前比較流行的一個(gè)工具包,它提供更多的
輸出媒介、輸出格式和配置選擇,你會(huì)發(fā)現(xiàn)原來(lái)在J2SE里一些仍需要自己手工構(gòu)建的功能在log4j當(dāng)中都已經(jīng)為你實(shí)現(xiàn)了。關(guān)于log4j我可能談得不會(huì)
太多,可以看看文后所附的“參考資料”,網(wǎng)上也有很詳細(xì)的介紹,我在這里做的是一個(gè)對(duì)比,因?yàn)閘og4j和J2SE 1.4 Logging API的用法是很相似的,一些名稱不同的組件你會(huì)發(fā)現(xiàn)他們所處的地位其實(shí)是一樣的:
log4j可以做到更精細(xì)更完善的控制,譬如J2SE里沒(méi)有現(xiàn)成向數(shù)據(jù)庫(kù)里寫日志的方法,但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的最大的特點(diǎn)就是“靈活”,
無(wú)論是Appender、Layout還是Configurator,你可以把日志輕松地弄成幾乎任何你想要的形式。
框架與標(biāo)準(zhǔn):JSR議案
從時(shí)間順序上講,log4j要比J2SE Logging API來(lái)得早,很多概念都是log4j先有的,但成為一個(gè)標(biāo)準(zhǔn),則是在JSR 47的形成。可能有人還不太了解JSR,這還要談到JCP,即“Java Community Process”,它是一個(gè)于1998年成立的旨在為Java技術(shù)制定民間標(biāo)準(zhǔn)的開放組織,你可以通過(guò)http://www.jcp.org/en/participation/membership申
請(qǐng)成為它的付費(fèi)或免費(fèi)會(huì)員,JCP的主要工作就是制定和發(fā)布JSR(Java Specification
Requests),JSR對(duì)于Java的意義就相當(dāng)于RFC對(duì)于網(wǎng)絡(luò)技術(shù)的意義,由于JCP會(huì)員們的集思廣益,使得JSR成為Java界的一個(gè)重要標(biāo)
準(zhǔn)。JSR 47即“Logging API Specification”,制定了調(diào)試和日志框架,J2SE Logging
API正是該框架的一個(gè)實(shí)現(xiàn)。由于種種原因,在JSR
47出來(lái)以前,log4j就已經(jīng)成為一項(xiàng)成熟的技術(shù),使得log4j在選擇上占據(jù)了一定的優(yōu)勢(shì),但不能因此就說(shuō)JSR
47是過(guò)時(shí)的規(guī)范,標(biāo)準(zhǔn)總是在發(fā)展的嘛!
并不是全部:其它日志處理工具
除了J2SE Logging API和log4j,日志處理方面還有別的技術(shù):Jakarta的commons組件項(xiàng)目中的JCL(Jakarta
Commons
Logging)是一個(gè)不錯(cuò)的選擇,它有點(diǎn)類似于GSS-API(通用安全服務(wù)接口)中的思想,其日志服務(wù)機(jī)制是可以替換的,也就是說(shuō)既可以用J2SE
Logging API也可以用log4j,但JCL對(duì)開發(fā)人員提供一致的接口,這一點(diǎn)相當(dāng)重要,組件可重用正是Jakarta
Commons項(xiàng)目追求的一個(gè)目標(biāo);IBM的JLog也是在J2SE Logging API之前推出的一個(gè)工具包,但JLog是一個(gè)商業(yè)產(chǎn)品。
至于日志API的應(yīng)用那可就多了,現(xiàn)在哪個(gè)大一點(diǎn)的工具或平臺(tái)不用到日志模塊呢?Tomcat、JBoss……
說(shuō)了這么多,我們無(wú)非需要知道的一件事就是,“調(diào)試”也是一門學(xué)問(wèn)。在我們一個(gè)勁地用System.out.println(...)而且用得很爽的時(shí)候,也應(yīng)該想想看,如何讓這樣一條菜鳥語(yǔ)句也能變得人性化和豐富多彩。
參考資料
posted on 2007-01-19 00:45
苦笑枯 閱讀(345)
評(píng)論(0) 編輯 收藏 所屬分類:
Java