始祖: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:关闭控制器。
通过重写以上三个方法我们可以很容易就实现一个把日志写入数据库的控制器。
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>
与Handler类似,我们也可以编写自己的格式化处理器,譬如API里没有将日志输出为我们可通过浏览器查看的HTML表格形式的Formatter,我们只需要重写3个方法:
format:格式化LogRecord中包含的信息。
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!");
上面定义了一个高于WARNING但低于SEVERE的日志级别。
于是可能有朋友会兴冲冲地用以下的语句来记录一个事件:
Logger logger = Logger.getAnonymousLogger();
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(new Filter()
{
public boolean 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
图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(...)而且用得很爽的时候,也应该想想看,如何让这样一条菜鸟语句也能变得人性化和丰富多彩。
参考资料
Java Logging Documentation Java Logging APIs J2SE进阶, by www.javaresearch.org Short introduction to log4j, by Ceki Gülcü log4j APIs FAQ about log4j本文地址:http://com.8s8s.com/it/it12358.htm