自 Java 1.4 开始,JDK 包括了一个崭新的日志框架包 java.util.logging,该日志框架设计精良,和 JDK 紧密结合,控制灵活,使用简单。日志记录对于软件的维护特别是对于已部署到运行环境之后的软件调试都有着重要的意义。在实际的项目中,往往还需要比该框架所提供的更为复杂的日志功能。对于这种需求,JDK 日志框架具有足够的可扩展能力,可以自定义不同需求的日志处理、消息格式化、日志消息级别等组件。在下面的内容中,本文将介绍了如何扩展 JDK 日志框架,自定义日志处理方式。并就一个实际的例子来介绍如何结合 JDK 日志框架和 STAF(Software Testing Automation Framework,一种自动化测试框架)日志服务来对 Java 程序进行监视。
JDK 的日志框架即 java.util.logging 包。对于一个软件的日志系统而言,首先必须得有一个日志对象,该对象负责记录日志信息。同时该信息可以输出到不同的位置,例如控制台,文件甚至网络中。对于信息的格式,则可以根据不同的需求,可以输出成普通文本,XML 或者 HTML 的格式。同时还需要对日志信息进行不同级别的分类,这样的好处是可以过滤冗余信息,只保留关键的日志。对于一个日志框架而言,日志对象必须是可配置的,它可以按照配置来输出到指定的目标,同时按照配置来决定输出的格式和决定何种级别以上的日志才能输出。配置的形式还可以是多种多样的,既能是代码的形式,也能是配置文件的形式。尤其是配置文件的形式,对于一个已经部署到运行环境中的软件而言,可以非常方便的改变日志配置而无需改变其源代码。
JDK 日志框架提供了上述的所有功能。它主要包括如下几个部件:
- Logger:日志记录对象。用于记录日志信息。
- Handler:用于处理日志信息的输出。在 Handler 类中,可以决定日志是输出到文件中还是控制台中。
- Filter: 用于过滤日志。在 Filter 类中,可以根据日志级别或者某种条件来决定是否输出该日志。这样达到去除冗余信息的目的。
- Formatter:用于格式化日志信息。该类可以将日志文本格式化成 XML 或者 HTML 的格式,这完全依赖于具体的实现。
- Level:用于表示日志的级别。 JDK 日志框架默认有如下级别 : SEVERE,WARNING,INFO,CONFIG,FINE,FINER,FINEST 。
对于程序而言,它的 Logger 对象首先会判断日志的级别是否满足输出级别的要求,然后将满足级别要求的日志消息交给所配置的 Handler 对象来处理,如果日志对象配置了一个 Filter 对象,那么 Filter 对象将会对日志信息做一次过滤。 Handler 对象接受到日志消息后,根据其所配置的格式化类 Formatter 来改变日志的格式,根据所配置的 Filter 对象和 Level 对象来再次过滤日志信息,最后输出到该种 Handler 对象所指定的输出位置中,该输出位置可以是控制台,文件,网络 socket 甚至是内存缓冲区。其架构模型如 图 1 所示。
JDK 提供了如下几种默认支持的 Handler 类:
- ConsoleHandler: 输出日志到控制台中
- FileHandler:输出日志到指定文件中
- MemoryHandler:输出日志到内存缓冲区中,当一定的条件满足的时候(如某种关键字的日志信息)再将缓冲区中的日志输出
- SocketHandler: 输出日志到网络 socket 中
- StreamHandler: 输出日志到输入输出流对象中
同时 JDK 日志框架也不失其灵活性,你可以定制自己所需要的 Handler,将日志按照自定义的需求输出到不同的位置,同时 Formatter,Level 类都可以自定义扩展,下面就详细叙述如何自定义扩展这些组件。
所有的 Handler 类都是继承自 java.util.logging.Handler 抽象类,该类结构图如 图 2 所示。
由该类图可见,Handler 抽象类提供了抽象接口:publish, flush 和 close 。这些接口提供了日志输出的基本功能。同时 Handler 类保存了 Formatter,Filter 和 Level 对象用来控制日志输出。因此,编写自定义的 Handler 类需要如下步骤:
- 继承 Handler 抽象类
- 实现 publish,flush 和 close 方法。其中 publish 方法是用于发布一条日志记录。 flush 方法是清空内存缓冲区。 close 方法是当应用程序关闭的时候,释放该 Handler 类所申请的资源(如文件,socket 等)
- 设置默认的 Formatter,Filter 和 Level 对象。必要的时候,可以在类的初始化时候读取配置文件来设置这些参数。
一个典型的自定义 Handler 类实现如清单 1 所示。
public class MyHandler extends Handler { private boolean doneHeader = false; public MyHandler() { setLevel(Level.INFO); setFilter(null); setFormatter(new SimpleFormatter()); } [email protected] 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 日志处理类。
日志可以被格式化为一定格式的文本,也可以成为 XML 或者 HTML 这样标准的格式。这取决于 Formatter 类的具体实现。 Formatter 抽象类提供了 format 成员函数用于扩展。一个典型的自定义 Formatter 类实现如清单 3 所示:
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 所示。
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 之间。
和其他日志框架一样,JDK 日志框架同样提供了强大的日志配置功能。你既可以通过代码进行动态配置,也可以通过配置文件来实现自由灵活的配置。通过代码动态配置,应用程序可以实现在运行过程中改变日志类的配置,动态地改变不同的配置组合。一个简单的动态配置代码如清单 5 所示。
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 所示。
# 设置日志对象的 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(Software Testing Automation Framework)是一个自动化软件测试框架,它可以实现分布式的自动化软件测试管理。我们可以应用 STAF 库的 Java API 来做基于 STAF 框架的应用,同时 STAF 同时也提供了日志服务。其日志服务是用来记录自动化测试流程中的信息,方便在 24x7 的自动化测试中记录自动化测试的操作,便于发现潜在的自动化测试管理脚本的问题。
既然我们可以用 STAF 的 Java API 来做基于 STAF 的应用,我们也可以将 JDK 的日志框架同 STAF 的日志服务接口结合起来。 STAF 的日志服务的 Java 接口定义如清单 7 所示:
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() 方法只用于记录日志信息。
将 STAF 日志服务的 Java API 同 JDK 日志框架结合起来需要做如下步骤:
该类封装了 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 类时有以下几个要点:
- 由于 STAF API 的调用时需要一个 STAFHandle 的对象来代表本地的 STAF 服务,在该类中声明了一个全局变量用来存储 STAFHandle 。
- close 方法是用来清理系统资源的,上述代码的 close 方法中释放了全局变量 STAFHandle 对象。
- publish 方法就是获得格式化后的消息后,直接调用 STAF 的日志 API 将日志发送到 STAF 服务中。
但到目前为止,我们还没有给 STAFHandler 类添加一个配置的代码,使之可以支持配置文件。下面我们定义了一个函数 configure,其代码如清单 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 API 的初始化需要注册 STAFHandle 对象。而且该注册只能执行一次。我们根据全局变量 stafHandle 的值来决定是否注册该对象。
- JDK 的日志框架有一个全局的 singleton 管理类 STAFManager,该类用于管理日志类,并提供了读取日志配置文件的成员函数 getProperty 。在上述的代码中,我们通过 STAFManager.getProperty 方法,从日志配置文件中读取 STAFHandler 对象所设置的 Formatter 类名,然后通过反射生成一个新的 Formatter 对象,设置到 Handler 对象中。
- 对于日志级别也是通过 STAFManager.getProperty 方法。需要注意的是由于我们的日志级别是自定义的级别,所以 Level 对象是由我们自定义的 Level 类 STAFLevel 来生成的。
- 我们也能定义自己需要的属性。比如清单 9 中我们定义了一个 .name 属性,用来存储 STAF 日志名称,通过 getProperty 函数从配置文件中读取 .name 属性。
由于 STAF 日志服务无需特殊的格式,我们只需要定义一个普通文本格式的 Formatter 即可。其代码如清单 10 所示,注意这里考虑了如果记录了一个异常对象的情况,将异常对象的 stack 打印到字符串中添加到消息文本中。
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(); } }
这是由于 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 所示:
# 设置日志对象的 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 。
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 日志组件的开发者提供帮助。