Java日志指南
作者:网络转载 发布时间:[ 2015/7/20 10:19:19 ] 推荐标签:测试开发技术 编程语言
转换模式
Log4j和Logback中的PatternLayout类都支持转换模式,它决定了我们如何从每一条日志事件中提取信息以及如何对信息进行格式化。下面显示了这些模式的一个子集,对于Log4j和Logback来说,虽然这些特定的字段都是一样的,但是并不是所有的字段都会使用相同的模式。想要了解更多信息,可以查看Log4j和Logback的PatternLayout文档。

例如,下面的PatternLayout会在中括号内x显示日志级别,后面是线程名字和日志事件的消息:
[%p] %t: %m
下面是使用了上述转换模式后的日志输出示例:
[INFO] main: initializing worker threads
[DEBUG] worker: listening on port 12222[INFO] worker: received request from 192.168.1.200[ERROR] worker: unknown request ID from 192.168.1.200
记录栈跟踪信息
如果你在Java程序中使用过异常,那么很有可能已经看到过栈跟踪信息。它提供了一个程序中方法调用的快照,让你准确定位程序执行的位置。例如,下面的栈跟踪信息是程序试图打开一个不存在的文件后生成的:
[ERROR] main: Unable to open file! java.io.FileNotFoundException: foo.file (No such file or directory)
at java.io.FileInputStream.open(Native Method) ~[?:1.7.0_79]
at java.io.FileInputStream.<init>(FileInputStream.java:146) ~[?:1.7.0_79]
at java.io.FileInputStream.<init>(FileInputStream.java:101) ~[?:1.7.0_79]
at java.io.FileReader.<init>(FileReader.java:58) ~[?:1.7.0_79]
at FooClass.main(FooClass.java:47)
这个示例使用了一个名为FooClass的类,它包含一个main方法。在程序第47行,FileReader独享试图打开一个名为foo.file的文件,由于在程序目录下没有名字是foo.file的文件,因此Java虚拟机抛出了一个FileNotFoundException。因为这个方法调用被放到了try-catch语块中,所以我们能够捕获这个异常并记录它,或者至少可以阻止程序崩溃。
使用PatternLayout记录栈跟踪信息
在写本篇文章时新版本的Log4j和Logback中,如果在Layout中没有和可抛异常相关的信息,那么都会自动将%xEx(这种栈跟踪信息包含了每次方法调用的包信息)添加到PatternLayout中。如果对于普通的日志信息的模式如下:
[%p] %t: %m
它会变为:
[%p] %t: %m%xEx
这样不仅仅错误信息会被记录下来,完整的栈跟踪信息也会被记录:
[ERROR] main: Unable to open file! java.io.FileNotFoundException: foo.file (No such file or directory)
at java.io.FileInputStream.open(Native Method) ~[?:1.7.0_79]
at java.io.FileInputStream.<init>(FileInputStream.java:146) ~[?:1.7.0_79]
at java.io.FileInputStream.<init>(FileInputStream.java:101) ~[?:1.7.0_79]
at java.io.FileReader.<init>(FileReader.java:58) ~[?:1.7.0_79]
at FooClass.main(FooClass.java:47)
%xEx中的包查询是一个代价昂贵的操作,如果你频繁的记录异常信息,那么可能会碰到性能问题,例如:
// ...
} catch (FileNotFoundException ex) {
logger.error(“Unable to open file!”, ex);
}
一种解决方法是在模式中显式的包含%ex,这样只会请求异常的栈跟踪信息:
[%p] %t: %m%ex
另外一种方法是通过追加%xEx(none)的方法排除(在Log4j)中所有的异常信息:
[%p] %t: %m%xEx{none}
或者在Logback中使用%nopex:
[%p] %t: %m%nopex
使用结构化布局输出栈跟踪信息
如你在“解析多行栈跟踪信息”一节中所见,对于站跟踪信息来说,使用结构化布局来记录是合适的方式,例如JSON和XML。 这些布局会自动将栈跟踪信息按照核心组件进行分解,这样我们可以很容易将其导出到其他程序或者日志服务中。对于上述站跟踪信息,如果使用JSON格式,部分信息显示如下:
...
"loggerName" : "FooClass",
"message" : "Foo, oh no! ",
"thrown" : {
"commonElementCount" : 0,
"localizedMessage" : "foo.file (No such file or directory)",
"message" : "foo.file (No such file or directory)",
"name" : "java.io.FileNotFoundException",
"extendedStackTrace" : [ {
"class" : "java.io.FileInputStream",
"method" : "open",
"file" : "FileInputStream.java",
...
记录未捕获异常
通常情况下,我们通过捕获的方式来处理异常。如果一个异常没有被捕获,那么它可能会导致程序终止。如果能够留存任何日志,那么这是一个可以帮助我们调试为什么会发生异常的好办法,这样你可以找到发生异常的根本原因并解决它。下面来说明我们如何建立一个默认的异常处理器来记录这些错误。
Thread类中有两个方法,我们可以用它来为未捕获的异常指定一个ExceptionHandler:
setDefaultUncaughtExceptionHandler 可以让你在任何线程上处理任何异常。setUncaughtExceptionHandler可以让你针对一个指定的线程设定一个不同的处理方法。而ThreadGroup则允许你设定一个处理方法。大部分人会使用默认的异常处理方法。
下面是一个示例,它设定了一个默认的异常处理方法,来创建一个日志事件。它要求你传入一个UncaughtExceptionHandler:
import java.util.logging.*;
public class ExceptionDemo {
private static final Logger logger = Logger.getLogger(ExceptionDemo.class);
public static void main(String[] args) {
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
public void uncaughtException(Thread t, Throwable e) {
logger.log(Level.SEVERE, t + " ExceptionDemo threw an exception: ", e);
};
});
class adminThread implements Runnable {
public void run() {
throw new RuntimeException();
}
}
Thread t = new Thread(new adminThread());
t.start();
}
}
下面是一个未处理异常的输出示例:
May 29, 2015 2:21:15 PM ExceptionDemo$1 uncaughtException
SEVERE: Thread[Thread-1,5,main] ExceptionDemo threw an exception:
java.lang.RuntimeException
at ExceptionDemo$1adminThread.run(ExceptionDemo.java:15)
at java.lang.Thread.run(Thread.java:745)
JSON
JSON(JavaScript Object Notation)是一种用来存储结构化数据的格式,它将数据存储成键值对的集合,类似于HashMap或者Hashtable。JSON具有的可移植性和通用性,大部分现代语言都内置支持它或者通过已经准备好的第三方类库来支持它。
JSON支持许多基本数据类型,包括字符串、数字、布尔、数组和null。例如,你可以使用下面的JSON格式来表示一个电脑:
{
"manufacturer": "Dell",
"model": "Inspiron",
"hardware": {
"cpu": "Intel Core i7",
"ram": 16384,
“cdrom”: null
},
"peripherals": [
{
"type": "monitor",
"manufacturer": "Acer",
"model": "S231HL"
}
]
}
JSON的可移植性使得它非常适合存储日志记录,使用JSON后,Java日志可以被任何数目的JSON解释器所读取。因为数据已经是结构化的,所以解析JSON日志要远比解析纯文本日志容易。
Java中的JSON
对于Java来说,有大量的JSON实现,其中一个是JSON.simple。JSON.simple是轻量级的、易于使用,并且全部符合JSON标准。
如果想将上面的computer对象转换成可用的Java对象,我们可以从文件中读取JSON内容,将其传递给JSON.simple,然后返回一个Object,接着我们可以将Object转换成JSONObject:
Object computer = JSONValue.parse(new FileReader("computer.json"));
JSONObject computerJSON = (JSONObject)computer;
另外,为了取得键值对的信息,你可以使用任何日志框架来记录一个JSONObject,JSONObject对象包含一个toString()方法, 它可以将JSON转换成文本:
2015-05-06 14:54:32,878 INFO JSONTest main {"peripherals":[{"model":"S231HL","manufacturer":"Acer","type":"monitor"}],"model":"Inspiron","hardware":{"cdrom":null,"ram":16384,"cpu":"Intel Core i7"},"manufacturer":"Dell"}
虽然这样做可以很容易的打印JSONObject,但如果你使用结构化的Layouts,例如JSONLayout或者XMLLayout,可能会导致意想不到的结果:
...
"message" : "{"peripherals":[{"model":"S231HL","manufacturer":"Acer","type":"monitor"}],"model":"Inspiron","hardware":{"cdrom":null,"ram":16384,"cpu":"Intel Core i7"},"manufacturer":"Dell"}",
...
Log4j中的JSONLayout并没有内置支持内嵌JSON对象,但你可以通过创建自定义Layout的方式来添加一个JSONObject字段,这个Layout会继承或者替换JSONLayout。然而,如果你使用一个日志管理系统,需要记住许多日志管理系统会针对某些字段使用预定义的数据类型。如果你创建一个Layout并将JSONObject存储到message字段中,那么它可能会和日志系统中使用的String数据类型相冲突。一种解决办法是将JSON数据存储到一个字段中,然后将字符串类型的日志消息存储到另外一个字段中。
其它JSON库
除了JSON.simple,Java中还有很多其它JSON库。JSON-java是由JSON创建者开发的一个参考实现,它包含了额外的一些功能,可以转换其它数据类型,包括web元素。但是目前JSON-java已经没有人来维护和提供支持了。
如果想将JSON对象转换成Java对象或者逆向转换,Google提供了一个Gson库。使用Gson时,可以很简单使用 toJson() 和 fromJson() 方法来解析JSON,这两个方法分别用来将Java对象转换成JSON字符串以及将JSON字符串转换成Java对象。Gson甚至可以应用在内存对象中,允许你映射到那些没有源代码的对象上。
Jackson
Jackson是一个强大的、流行的、功能丰富的库,它可以在Java中管理JSON对象。有一些框架甚至使用Jackson作为它们的JSONLayouts。尽管它很大并且复杂,但Jackson对于初学者和高级用户来说,是很容易使用的。
Logback通过logback-jackson和logback-json-classic库继承了Jackson,这两个库也是logback-contrib项目的一部分。在集成了Jackson后,你可以将日志以JSON的格式导出到任何Appender中。
Logback Wiki详细解释了如何将JSON添加到logback中,在Wiki页面中的示例使用了LogglyAppender,这里的配置也可以应用到其他Appender上。下面的示例说明了如何将JSON格式化的日志记录写入到名为myLog.json的文件中:
...
<appender name="file" class="ch.qos.Logback.core.FileAppender">
<file>myLog.json</file>
<encoder class="ch.qos.Logback.core.encoder.LayoutWrappingEncoder">
<layout class="ch.qos.Logback.contrib.json.classic.JsonLayout">
<jsonFormatter class="ch.qos.Logback.contrib.jackson.JacksonJsonFormatter"/>
</layout>
</encoder>
</appender>
...

sales@spasvo.com