Java调试的变迁:从System.out.println到log4j

类别:Java 点击:0 评论:0 推荐:
 jungleford如是说

    用惯了VC的人刚接触Java大概很不习惯代码的调试,的确,在M$的大部分IDE都做得相当出色,包括像VJ++这样一直被Java程序员称为是“垃圾”的类库(记得以前在瀚海星云的Java版提有关VJ问题的人是有可能被封的,^_^),它的开发工具在调试上都相当容易。Java也有命令行方式的调试和IDE的调试,但现在的像JB这样的玩意又是个庞然大物,低配置的机器可能就是个奢望,不像VC那样。怎么办呢,高手们说,“我的jdb用得贼熟练”,那我会报以景仰的目光,像我这样的菜鸟基本上就没使过jdb,还是老老实实在代码里面System.out.println(...)。直到1996年一个叫做“欧洲安全电子市场”(E.U. SEMPER)的项目启动,“调试”不再是一件“体力活”,而是一种软件设计的艺术,这个项目组开发的日志管理接口后来成为Apache Jakarta项目中的一员,它就是现在我们所熟悉的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:关闭控制器。
    通过重写以上三个方法我们可以很容易就实现一个把日志写入数据库的控制器。

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


    你可以通过修改这个配置文件来改变运行时Logger的行为,譬如:.level定义的是上面所说的默认输出的最低日志级别;XXXHandler相关属性定义了各种输出媒介等等。
    这里比较有意思的是关于日志文件,也就是FileHandler,当然,你可以在程序中创建一个FileHandler,然后添加到logger中: FileHandler fhd = new FileHandler("%h/java%u.log", 5000, 1, true);
fhd.setLevel(Level.ALL);
fhd.setFormatter(new XMLFormatter());
logger.addHandler(fhd);
    这段代码等价于上面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代表用转义字符定义的一个日志文件名: 转义字符串 含义 %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属性: java -Djava.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(...)而且用得很爽的时候,也应该想想看,如何让这样一条菜鸟语句也能变得人性化和丰富多彩。

参考资料

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