Java调试的变迁:从System.out.println到log4j
用惯了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的方式,譬如:
classAlertLevelextendsjava.util.logging.Level
{
publicAlertLevel()
{
super("ALERT",950);
}
}
...