[size=xx-large][b]log4j日志现场背后的秘密[/b][/size]
[size=x-large]1、什么是日志现场[/size]
调用Logger打印日志的地方,称为日志现场。日志现场的属性有:类名、方法名、java文件名、和行数。
下面是一个例子。
LoggerTest.java
[size=x-large]
2、让log4j在打印日志消息时,也打印出日志现场[/size]
在classpath下,添加一个log4j.xml的文件。内容如下:
log4j.xml
再运行上面的文件,
在控制台中,可以看到如下信息:日志现场(类名、方法名、java文件名、和行数)和消息。
consoleView
[size=x-large]3、封装log4j[/size]
在打印需要格式化的字符串时,使用Message.format()会出现如下代码。
当日志级别大于debug时,不需要打印该消息。但是format方法还是会被调用。这造成了浪费。早期log4j官网推荐如下使用方式。
这样就有了早期对log4j的封装版本。
CLogger.java
DLogger
[size=x-large]4、使用封装的Logger出现的问题[/size]
一切安好,使用之。
LoggerTestThree.java
使用上例的log4j.xml
在控制台看到如下信息:
console view
打印的日志现场是,调用Logger的debug的地方。经过我们的封装,我们希望日志现场为,调用DLogger的debug方法的地方。于是问题出来了,我们封装了日志类,也转移了日志现场。怎样找到日志现场呢?
[size=x-large]5、如果是你,会如何定位日志现场的[/size]
首先得理解,java函数调用堆栈。从应用的入口函数(main函数)出发,对其他函数的调用,形成了函数调用堆栈。在java方法中,新建一个Throwable对象,该对象的StackTraceElement[]属性,就包括函数调用堆栈。
看如下代码:
LoggerTestTwo.java
MyLogger.java
运行上面代码,在控制台中可以看到如下信息:创建Throwable时的函数调用堆栈。
consoleView
设定我们的MyLogger.debug()方法对应,log4j中的Logger.debug()方法。那么日志现场LoggerTestTwo.test(LoggerTestTwo.java:10)就在其中。我们发现,在被MyLogger.debug() 调用的所有 (包括直接和间接调用) 方法中,新建一个Throwable,其函数调用堆栈都包括这些信息。
[size=x-large]
6、log4j是如何定位日志现场的[/size]
在被Logger的debug方法调用的方法中,创建一个Throwable对象,解析其函数调用堆栈,确认日志现场。
秘密1: fqcn( fqnOfCallingClass,fqnOfCategoryClass) class name of first class considered part of the logging framework. Location will be site that calls a method on this class.
fqcn是Logger的全类名。在与函数调用堆栈比对中,离开Logger类的下一个地址,就是日志现场。
在logger的debug方法,创建LoggingEvent时,传入一个名为fqcn值为Logger的全类名的字符串。这样在LoggingEven的 getLocationInformation()方法中,创建LocationInfo时,传入一个新建的Throwable和fqcn。
从LocationInfo中,既可得到日志现场。
[size=x-large]7、改进封装log4j的技术[/size]
在log4j_1.2.16中,logMF和logSF提供了参考意见。
于是就有了上一章的封装log4j的实现。
[size=x-large]1、什么是日志现场[/size]
调用Logger打印日志的地方,称为日志现场。日志现场的属性有:类名、方法名、java文件名、和行数。
下面是一个例子。
LoggerTest.java
import org.apache.log4j.Logger;
public class LoggerTest {
private static final Logger logger = Logger.getLogger(LoggerTest.class);
public static void main(String[] args) {
new LoggerTest().test();
}
private void test() {
// 下面一行就是日志现场 LoggerTest类,test方法,LoggerTest.java文件,第12行。
logger.debug("Hello, log4j!");
}
}
[size=x-large]
2、让log4j在打印日志消息时,也打印出日志现场[/size]
在classpath下,添加一个log4j.xml的文件。内容如下:
log4j.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/" >
<appender name="SYSTEM_OUT" class="org.apache.log4j.ConsoleAppender">
<layout class="org.apache.log4j.PatternLayout">
<!-- %l表示日志现场,%m表示日志消息,%n表示换行。-->
<param name="ConversionPattern" value="%l: %m%n"/>
</layout>
</appender>
<root>
<!-- root 引用SYSTEM_OUT appender-->
<appender-ref ref="SYSTEM_OUT"/>
</root>
</log4j:configuration>
再运行上面的文件,
在控制台中,可以看到如下信息:日志现场(类名、方法名、java文件名、和行数)和消息。
consoleView
LoggerTest.test(LoggerTest.java:12): Hello, log4j!
[size=x-large]3、封装log4j[/size]
在打印需要格式化的字符串时,使用Message.format()会出现如下代码。
logger.debug(MessageFormat.format(“Hello, {0}!”, “log4j”));
当日志级别大于debug时,不需要打印该消息。但是format方法还是会被调用。这造成了浪费。早期log4j官网推荐如下使用方式。
if(logger.isDebugEnabled()){
logger.debug(MessageFormat.format(“Hello, {0}!”, “log4j”));
}
这样就有了早期对log4j的封装版本。
CLogger.java
public class CLogger {
public static void debug(Logger logger, String pattern, Object[] arguments) {
if (logger.isDebugEnabled()) {
logger.debug(MessageFormat.format(pattern, arguments));
}
}
}
DLogger
import java.text.MessageFormat;
import org.apache.log4j.Logger;
public class DLogger {
public static DLogger getLogger(Class<?> clazz) {
return new DLogger(clazz);
}
private final Logger logger;
private DLogger(Class<?> clazz) {
logger = Logger.getLogger(clazz);
}
public void debug(String pattern, Object... arguments) {
if (logger.isDebugEnabled()) {
logger.debug(MessageFormat.format(pattern, arguments));
}
}
}
[size=x-large]4、使用封装的Logger出现的问题[/size]
一切安好,使用之。
LoggerTestThree.java
public class LoggerTestThree {
private static final DLogger logger = DLogger
.getLogger(LoggerTestThree.class);
public static void main(String[] args) {
LoggerTestThree loggerTestTwo = new LoggerTestThree();
loggerTestTwo.test();
}
private void test() {
logger.debug("Hello,{0}!", "log4j"); //我们想将日志现场定在这里。
}
}
使用上例的log4j.xml
在控制台看到如下信息:
console view
DLogger.debug(DLogger.java:19): Hello,log4j!
打印的日志现场是,调用Logger的debug的地方。经过我们的封装,我们希望日志现场为,调用DLogger的debug方法的地方。于是问题出来了,我们封装了日志类,也转移了日志现场。怎样找到日志现场呢?
[size=x-large]5、如果是你,会如何定位日志现场的[/size]
首先得理解,java函数调用堆栈。从应用的入口函数(main函数)出发,对其他函数的调用,形成了函数调用堆栈。在java方法中,新建一个Throwable对象,该对象的StackTraceElement[]属性,就包括函数调用堆栈。
看如下代码:
LoggerTestTwo.java
public class LoggerTestTwo {
private static final MyLogger logger = new MyLogger();
public static void main(String[] args) {
LoggerTestTwo loggerTestTwo = new LoggerTestTwo();
loggerTestTwo.test();
}
private void test() {
logger.debug();
}
}
MyLogger.java
public class MyLogger {
public void debug() {
Throwable throwable = new Throwable();
StackTraceElement[] stackTraceElements = throwable.getStackTrace();
for (StackTraceElement element : stackTraceElements) {
System.out.println(element);
}
}
}
运行上面代码,在控制台中可以看到如下信息:创建Throwable时的函数调用堆栈。
consoleView
MyLogger.debug(MyLogger.java:3)
LoggerTestTwo.test(LoggerTestTwo.java:10)
LoggerTestTwo.main(LoggerTestTwo.java:6)
设定我们的MyLogger.debug()方法对应,log4j中的Logger.debug()方法。那么日志现场LoggerTestTwo.test(LoggerTestTwo.java:10)就在其中。我们发现,在被MyLogger.debug() 调用的所有 (包括直接和间接调用) 方法中,新建一个Throwable,其函数调用堆栈都包括这些信息。
[size=x-large]
6、log4j是如何定位日志现场的[/size]
在被Logger的debug方法调用的方法中,创建一个Throwable对象,解析其函数调用堆栈,确认日志现场。
秘密1: fqcn( fqnOfCallingClass,fqnOfCategoryClass) class name of first class considered part of the logging framework. Location will be site that calls a method on this class.
fqcn是Logger的全类名。在与函数调用堆栈比对中,离开Logger类的下一个地址,就是日志现场。
在logger的debug方法,创建LoggingEvent时,传入一个名为fqcn值为Logger的全类名的字符串。这样在LoggingEven的 getLocationInformation()方法中,创建LocationInfo时,传入一个新建的Throwable和fqcn。
new LocationInfo(new Throwable(), fqnOfCategoryClass);
从LocationInfo中,既可得到日志现场。
[size=x-large]7、改进封装log4j的技术[/size]
在log4j_1.2.16中,logMF和logSF提供了参考意见。
于是就有了上一章的封装log4j的实现。