转帖|其它|编辑:郝浩|2009-01-12 13:16:53.000|阅读 946 次
概述:日志记录对于软件的维护特别是对于已部署到运行环境之后的软件调试都有着重要的意义。本文介绍了 JDK 的日志框架,以及如何根据不同需求自定义日志处理、消息格式化、消息级别等组件。最后阐述了如何利用 JDK 日志框架的扩展能力将 Java 程序能够通过 STAF(Software Testing Automation Framework,一种自动化测试框架)日志服务来进行监视。
# 慧都年终大促·界面/图表报表/文档/IDE等千款热门软控件火热促销中 >>
文章关键字:|JDK|日志|框架|Java|JDK|XML|HTML|测试|
自 Java 1.4 开始,JDK 包括了一个崭新的日志框架包 java.util.logging,该日志框架设计精良,和 JDK 紧密结合,控制灵活,使用简单。日志记录对于软件的维护特别是对于已部署到运行环境之后的软件调试都有着重要的意义。在实际的项目中,往往还需要比该框架所提供的更为复杂的日志功能。对于这种需求,JDK 日志框架具有足够的可扩展能力,可以自定义不同需求的日志处理、消息格式化、日志消息级别等组件。在下面的内容中,本文将介绍了如何扩展 JDK 日志框架,自定义日志处理方式。并就一个实际的例子来介绍如何结合 JDK 日志框架和 STAF(Software Testing Automation Framework,一种自动化测试框架)日志服务来对 Java 程序进行监视。
JDK 日志框架介绍
JDK 的日志框架即 java.util.logging 包。对于一个软件的日志系统而言,首先必须得有一个日志对象,该对象负责记录日志信息。同时该信息可以输出到不同的位置,例如控制台,文件甚至网络中。对于信息的格式,则可以根据不同的需求,可以输出成普通文本,XML 或者 HTML 的格式。同时还需要对日志信息进行不同级别的分类,这样的好处是可以过滤冗余信息,只保留关键的日志。对于一个日志框架而言,日志对象必须是可配置的,它可以按照配置来输出到指定的目标,同时按照配置来决定输出的格式和决定何种级别以上的日志才能输出。配置的形式还可以是多种多样的,既能是代码的形式,也能是配置文件的形式。尤其是配置文件的形式,对于一个已经部署到运行环境中的软件而言,可以非常方便的改变日志配置而无需改变其源代码。
JDK 日志框架提供了上述的所有功能。它主要包括如下几个部件:
对于程序而言,它的 Logger 对象首先会判断日志的级别是否满足输出级别的要求,然后将满足级别要求的日志消息交给所配置的 Handler 对象来处理,如果日志对象配置了一个 Filter 对象,那么 Filter 对象将会对日志信息做一次过滤。 Handler 对象接受到日志消息后,根据其所配置的格式化类 Formatter 来改变日志的格式,根据所配置的 Filter 对象和 Level 对象来再次过滤日志信息,最后输出到该种 Handler 对象所指定的输出位置中,该输出位置可以是控制台,文件,网络 socket 甚至是内存缓冲区。其架构模型如图 1 所示。
JDK 提供了如下几种默认支持的 Handler 类:
同时 JDK 日志框架也不失其灵活性,你可以定制自己所需要的 Handler,将日志按照自定义的需求输出到不同的位置,同时 Formatter,Level 类都可以自定义扩展,下面就详细叙述如何自定义扩展这些组件。
自定义日志 Handler
所有的 Handler 类都是继承自 java.util.logging.Handler 抽象类,该类结构图如 图 2 所示。
由该类图可见,Handler 抽象类提供了抽象接口:publish, flush 和 close 。这些接口提供了日志输出的基本功能。同时 Handler 类保存了 Formatter,Filter 和 Level 对象用来控制日志输出。因此,编写自定义的 Handler 类需要如下步骤:
[SPAN] 一个典型的自定义 Handler 类实现如清单 1 所示。
清单 1 自定义 Handler 类
public class MyHandler extends Handler { private boolean doneHeader = false; public MyHandler() { setLevel(Level.INFO); setFilter(null); setFormatter(new SimpleFormatter()); } _cnnew1@Override public void close() throws SecurityException { if (!doneHeader) { output(getFormatter().getHead(this)); doneHeader = true; } output(getFormatter().getTail(this)); flush(); } @Override public void flush() { // 清空缓冲区 } @Override public void publish(LogRecord record) { if (!isLoggable(record)) { return; } String msg = getFormatter().format(record); try { if (!doneHeader ) { output(getFormatter().getHead(this)); doneHeader = true; } output(msg); } catch (Exception ex) { reportError(null, ex, ErrorManager.WRITE_FAILURE); } } private void output(String message) { // 实现日志输出 } }
这里 reportError 方法是将日志类中的错误信息输出到外界,这个是由 ErrorManager 类实现的,ErrorManager 类负责记录日志框架中 Handler 的错误,一般情况下是将该错误打印到控制台中。具体的每条日志消息被 JDK 日志框架封装成 LogRecord 对象,该类部分定义如 清单 2所示。
清单 2 LogRecord 类定义
public class LogRecord implements java.io.Serializable { public String getLoggerName(); public void setLoggerName(String name); public ResourceBundle getResourceBundle(); public void setResourceBundle(ResourceBundle bundle); public Level getLevel(); public void setLevel(Level level); public String getMessage(); public void setMessage(String message); public Object[] getParameters(); public void setParameters(Object parameters[]); public int getThreadID(); public void setThreadID(int threadID); public long getMillis(); public void setMillis(long millis); public Throwable getThrown(); public void setThrown(Throwable thrown); ... }
由清单 2 可见,LogRecord 类包含了一个日志消息的级别、消息文本、时间、参数、线程等等所有的信息,这些都交给 Handler,Formatter 和 Filter 这些对象来处理。同时该类也是可序列化的,可以序列化到网络和文件中。该类还可以和一个 ResourceBundle 对象绑定,实现消息字符串的本地化处理。
本节描述了一个典型的自定义的 Handler 类的实现。在本文后面部分将会有一个实际的例子来介绍如何实现一个 STAF 日志处理类。[SPAN]
自定义日志 Formatter
日志可以被格式化为一定格式的文本,也可以成为 XML 或者 HTML 这样标准的格式。这取决于 Formatter 类的具体实现。 Formatter 抽象类提供了 format 成员函数用于扩展。一个典型的自定义 Formatter 类实现如清单 3 所示:
清单 3 LogRecord 类定义
public class MyFormatter extends Formatter { private final String lineSeparator =
System.getProperty("line.separator"); @Override public String format(LogRecord record) { StringBuffer sb = new StringBuffer(); String message = formatMessage(record); sb.append(record.getLevel().getLocalizedName()); sb.append(message); sb.append(lineSeparator); if (record.getThrown() != null) { try { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); record.getThrown().printStackTrace(pw); pw.close(); sb.append(sw.toString()); } catch (Exception ex) { } } return sb.toString(); } }
其中 formatMessage 方法提供了默认的将日志记录本地化和格式化的方法。它还能支持 java.text 风格的文本格式化,这只需要在调用 Logger 对象的 setMessage 方法设定 java.text 风格的格式字符串,同时通过 setParameters 方法设置参数,这样 formatMessage 将会根据所设置的 java.text 风格的格式字符串来格式化日志消息。总之,formatMessage 方法方便了子类格式化字符串。使子类只需要定义输出文本的格式而无需考虑本地化等问题。
自定义日志消息级别
JDK 日志框架默认提供了 SEVERE,WARNING,INFO,CONFIG,FINE,FINER,FINEST 这几种日志级别。如果我们需要定义更多的日志级别,只需要继承 java.util.logging.Level 类,然后将自定义的级别作为静态成员变量声明即可。一个典型的自定义的消息类如清单 4 所示。
清单 4 自定义 Level 类
public class MyLevel extends Level { protected MyLevel(String name, int value) { super(name, value); } public static final Level Level1 = new MyLevel("Level1", 123); ... // 其他自定义级别 }
权重值 value 是一个整型数。在默认的 JDK 日志级别中,SEVERE 的权重是 1000,FINEST 是 300,可以根据具体的需求来定义每个自定义级别的权重。例如在 WARNING 和 INFO 级别中加入一个新的级别,该级别的权重必须介于 800 到 900 之间。[SPAN]
自由的日志配置
和其他日志框架一样,JDK 日志框架同样提供了强大的日志配置功能。你既可以通过代码进行动态配置,也可以通过配置文件来实现自由灵活的配置。通过代码动态配置,应用程序可以实现在运行过程中改变日志类的配置,动态地改变不同的配置组合。一个简单的动态配置代码如清单 5 所示。
清单 5 动态配置 Logger 对象
public static void main(String[] args){ Handler fh = new FileHandler("%t/wombat.log"); Logger.getLogger("logname").addHandler(fh); Logger.getLogger("com.wombat").setLevel("com.wombat",Level.FINEST); ... }
配置文件的配置方法则同样灵活多变。它主要是在应用程序启动时根据一个指定的配置文件来设置日志对象。在配置文件中,日志对象是由其名称来标识的。一个典型的日志配置文件如清单 6 所示。
清单 6 JDK Logger 配置文件
# 设置日志对象的 Handler,日志对象的名称是com.xyz.foo com.xyz.foo.handlers= java.util.logging.FileHandler,
java.util.logging.ConsoleHandler # 设置日志对象的基本输出级别 com.xyz.foo.level = INFO #FileHandler 只允许输出 SEVERE 以上级别的日志 java.util.logging.ConsoleHandler.level = SEVERE #ConsoleHandler 允许输出 INFO 以上级别的日志 java.util.logging.ConsoleHandler.level = INFO
当设置好一个日志配置文件后,在 java 程序的启动参数中,我们可以通过添加 -Djava.util.logging.config.file 参数来定义配置文件路径,一个典型的 java 命令行如下:
java -Djava.util.logging.config.file=logger.properties -cp . Mainclass
我们也可以在应用程序中声明自定义的 Handler,Formatter,Level 等组件,这只需要这些自定义组件能够在 classpath 中找到即可。
实例——结合 STAF 日志服务
STAF(Software Testing Automation Framework)是一个自动化软件测试框架,它可以实现分布式的自动化软件测试管理。我们可以应用 STAF 库的 Java API 来做基于 STAF 框架的应用,同时 STAF 同时也提供了日志服务。其日志服务是用来记录自动化测试流程中的信息,方便在 24x7 的自动化测试中记录自动化测试的操作,便于发现潜在的自动化测试管理脚本的问题。
既然我们可以用 STAF 的 Java API 来做基于 STAF 的应用,我们也可以将 JDK 的日志框架同 STAF 的日志服务接口结合起来。 STAF 的日志服务的 Java 接口定义如清单 7 所示:
清单 7 STAFLog 类定义
public class STAFLog { public STAFLog(String logType, String logName, STAFHandle handle); public STAFResult log(int level, String msg) // Log type constants public static STAFResult log(STAFHandle theHandle, String logType, String logName, int level, String msg) public String getName(); public String getLogType(); public int getMonitorMask(); ... //other methods }
从清单 7 我们可以看出,STAFLog 类提供了方法可以将日志信息存储到 STAF 的日志库中, 这个日志库既可以是本地的文件,也可以是另一个 STAF 服务器上的日志库。这是通过本地 STAF 服务器的配置来决定的。而 STAFLog.log() 方法只用于记录日志信息。[SPAN]
将 STAF 日志服务的 Java API 同 JDK 日志框架结合起来需要做如下步骤:
创建 STAF 日志 Handler 类
该类封装了 STAF 日志服务 API 的接口。同时 STAF 的 Java API 需要一个全局的 STAFHandle 对象,用来表示本地的 STAF 服务句柄。这个可以通过建立一个静态的 STAFHandle 对象即可。其代码如下所示,我们定义了一个 STAFHandler 类如清单 8 所示。
清单 8 STAFHandler 类实现
import java.util.logging.*; import com.ibm.staf.wrapper.STAFLog; public class STAFHandler extends Handler { private String logName; private static STAFHandle stafHandle = null; public STAFHandler(String name) { configure(); logName = name; } public STAFHandler() { configure(); } @Override public void close() throws SecurityException { if (stafHandle != null){ try { stafHandle.unRegister(); } catch (STAFException e) { //ignore } } } @Override public void flush() { //nothing } @Override public void publish(LogRecord record) { if (!isLoggable(record)) { return; } String msg; try { msg = getFormatter().format(record);
} catch (Exception ex) { reportError(null, ex, ErrorManager.FORMAT_FAILURE); return; } try { STAFLog.log(stafHandle, STAFLog.MACHINE,
logName, record.getLevel().getName(), msg); } catch (Exception ex) { reportError(null, ex, ErrorManager.WRITE_FAILURE); } ...
在实现 STAFHandler 类时有以下几个要点:
但到目前为止,我们还没有给 STAFHandler 类添加一个配置的代码,使之可以支持配置文件。下面我们定义了一个函数 configure,其代码如清单 9 所示。
清单 9 配置函数实现
private void configure() { if (stafHandle == null) { try { stafHandle = new STAFHandle("my application"); } catch (STAFException e) { reportError("registe staf handle error",
e, ErrorManager.OPEN_FAILURE); } } LogManager manager = LogManager.getLogManager(); String cname = getClass().getName(); //set staf log name logName = manager.getProperty(cname + ".name"); if (logName == null) logName = "demo.staflog"; //set formatter String sformatter = manager.getProperty(cname + ".formatter"); Formatter formatter = null; if (sformatter != null) { try { formatter = (Formatter)Class.forName(sformatter).newInstance(); } catch (Exception e) { //ignore } } setFormatter(formatter == null? new STAFFormatter() : formatter); //set level String sLevel = manager.getProperty(cname + ".level"); Level level = null; if (sLevel != null) { try { level = STAFLevel.parse(sLevel); } catch (Exception e) { //ignore } } setLevel(level == null? STAFLevel.DEBUG : level); }
在实现配置文件支持的代码中,有以下几个要点:
创建一个适合 STAF 日志的 Formatter 类
由于 STAF 日志服务无需特殊的格式,我们只需要定义一个普通文本格式的 Formatter 即可。其代码如清单 10 所示,注意这里考虑了如果记录了一个异常对象的情况,将异常对象的 stack 打印到字符串中添加到消息文本中。
清单 10. STAFFormatter 实现
import java.io.*; import java.util.logging.*; public class STAFFormatter extends Formatter { private final String lineSeparator = System.getProperty("line.separator"); @Override public String format(LogRecord record) { StringBuffer sb = new StringBuffer(); String message = formatMessage(record); sb.append(message); sb.append(lineSeparator); if (record.getThrown() != null) { try { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); record.getThrown().printStackTrace(pw); pw.close(); sb.append(sw.toString()); } catch (Exception ex) { } } return sb.toString(); } }
创建对应于 STAF 日志级别的 Level 对象
这是由于 STAFLog 有着不同的日志消息级别,它包括 Fatal, Error, Warning, Info, Tracer, Debug 等级别,有些是 JDK 日志框架已有的级别,有些则不是。我们需要增加新的 Level 对象来满足 STAFLog 的需求。一个新的 Level 类:STAFLevel 定义如清单 11 所示。
清单 11 自定义 STAFLevel
import java.util.logging.Level; public class STAFLevel extends Level { protected STAFLevel(String name, int value) { super(name, value); } protected STAFLevel(String name, int value, String resourceBundleName) { super(name, value, resourceBundleName); } public static final Level FATAL = new STAFLevel("FATAL",980); public static final Level ERROR = new STAFLevel("ERROR",980); public static final Level TRACE = new STAFLevel("TRACE", 790); public static final Level DEBUG = new STAFLevel("DEBUG", 690); }
清单 11 定义了 FATAL,ERROR,TRACE 和 DEBUG 级别。这就和 STAFLog 中的部分级别一一对应起来了。
将一切组合起
清单 12 描述了如何在一段实际的代码中将 STAF 日志处理类和 JDK 日志类结合起来。 从清单 12 可以看出,该实例默认指定输出到 STAF 日志服务的日志名称为“ staflogger ”。然后通过动态配置的方法来设定 Handler,Level 和 Formatter 。最后在调用 JDK 的日志对象的 log 方法记录了 4 种自定义级别的日志。
清单 12 一个完整的例子
package demo.staflog; import java.util.logging.Logger; public class STAFLoggerTest { public static void main(String[] args) { Logger logger = Logger.getLogger(STAFLoggerTest.class.getName()); logger.setUseParentHandlers(false); logger.setLevel(STAFLevel.DEBUG); STAFHandler stafHandler = new STAFHandler("staflogger"); stafHandler.setLevel(STAFLevel.DEBUG); stafHandler.setFormatter(new STAFFormatter()); logger.addHandler(stafHandler); //log logger.log(STAFLevel.DEBUG, "debug log"); logger.log(STAFLevel.FATAL, "fatal log"); logger.log(STAFLevel.ERROR, "error log"); logger.log(STAFLevel.TRACE, "trace log"); } }
但我们也可以将这些代码改为配置文件的方式,其配置文件如清单 13 所示:
清单 13 STAFLog 类定义
# 设置日志对象的 Handler demo.staflog.STAFLoggerTest.handlers= demo.staflog.STAFHandler demo.staflog.STAFLoggerTest.level = DEBUG # 取消发送日志到父 Logger 对象 demo.staflog.STAFLoggerTest.useParentHandlers = FALSE # 设置 Handler 的名称,输出级别和格式化对象 demo.staflog.STAFHandler.name= staflogger demo.staflog.STAFHandler.level = DEBUG demo.staflog.STAFHandler.formatter = demo.staflog.STAFFormatter
这样代码可以简化为清单 14 。
清单 14 STAFLog 类定义
public class STAFLoggerTest { private static Level defaultLevel = STAFLevel.DEBUG; public static void main(String[] args) { //log logger.log(STAFLevel.DEBUG, "debug log"); logger.log(STAFLevel.FATAL, "fatal log");
logger.log(STAFLevel.ERROR, "error log"); logger.log(STAFLevel.TRACE, "trace log");
} }
配置文件的方式相对于动态配置的方式更加灵活,因为这无需改变和重新编译代码,只需要修改配置文件,就能修改日志中 Handler,Level 和 Formatter 的组合配置,这对于已经部署发布的软件而言,有着更为实际的意义。
当运行代码后,在命令行中输入 STAF 命令来显示 STAF 日志 staflogger:
mymachine:~ myname$ staf local log query machine mymachine logname staflogger Response -------- Date-Time Level Message ----------------- ----- ---------- 20081111-16:15:21 Debug debug log 20081111-16:15:21 Fatal fatal log 20081111-16:15:21 Error error log 20081111-16:15:21 Trace trace log
这显示了我们刚才在 Java 代码中记录的信息,它们已经被输出到 STAF 的日志服务中了。
结束语
JDK 日志框架简单灵活,它虽然比 log4j 出现的时期晚,但其功能并不比 log4j 少。而且 JDK 日志框架直接隶属于 JDK,被 Java 标准所支持而无需安装第三方库文件。本文介绍了 JDK 日志框架的结构,如何扩展 JDK 日志框架使之满足实际的项目需求。并以如何在 Java 程序中将日志输出到 STAF 的日志服务中为例,一步步描述了如何实现扩展 JDK 日志组件,使之和 STAF 日志服务结合到一起,同时如何创建灵活的配置文件来组合日志框架组件。希望本文可以给其他需要扩展 JDK 日志组件的开发者提供帮助。
本站文章除注明转载外,均为本站原创或翻译。欢迎任何形式的转载,但请务必注明出处、不得修改原文相关链接,如果存在内容上的异议请邮件反馈至chenjj@pclwef.cn
文章转载自:IBM