Java日志框架
作为一个Java程序员,肯定离不开日志框架,现在最优秀的Java日志框架是Log4j2,没有之一。根据官方的测试表明,在多线程环境下,Log4j2的异步日志表现更加优秀。在异步日志中,Log4j2使用独立的线程去执行I/O操作,可以极大地提升应用程序的性能。
在官方的测试中,下图比较了Sync、Async Appenders和Loggers all async三者的性能。其中Loggers all async表现最为出色,而且线程数越多,Loggers all async性能越好。
除了对Log4j2自身的不同模式做对比以外,官方还做了Log4j2/Log4j1/Logback的对比,如下图所示
其中,Loggers all async是基于LMAX Disruptor实现的。
使用Log4j2
需要哪些JAR
使用Log4j2最少需要两个JAR,分别是log4j-和,其它JAR包根据应用程序需要添加。
配置文件位置
默认的,Log4j2在classpath下寻找名为的配置文件。也可以使用system property指定配置文件的全路径。-=path/to/,在Java代码中指定路径如下所示
import org.apache.logging.;
import .;
import ;
public class Demo {
public static void main(String[] args) {
LoggerContext loggerContext = (LoggerContext) (false);
File file = new File("path/to/a/different/");
(());
}
}
一般的,不需要手动关闭Log4j2,如果想手动在代码中关闭Log4j2如下所示
import .;
import .;
import .;
public class Demo {
public static void main(String[] args) {
((LoggerContext) ());
}
}
不同的线程输出日志到不同的文件中有关Log4j2的内容很多,不能一一列出,如果在开发中遇到任何问题,推荐去官方文档中寻找解决方案。
方法一使用ThreadContext
在多线程编程中,如果不做特殊的设置,那么多个线程的日志会输出到同一个日志文件中,这样在查阅日志的时候,会带来诸多不便。很自然地,我们想到了让不同的线程输出日志到不同的文件中,这样不是更好吗?在翻阅官方文档过程中,找到了FAQ(Frequently Asked Questions),其中有个问题配置如下:
filePattern="./logs/${date:yyyy-MM}/${ctx:ROUTINGKEY}-special-%d{yyyy-MM-dd}-%">
%d{ISO8601} [%t] %p %c{3} - %m%n
filePattern="./logs/${date:yyyy-MM}/default-%d{yyyy-MM-dd}-%">
%d{ISO8601} [%t] %p %c{3} - %m%n
filePattern="./logs/${date:yyyy-MM}/${ctx:ROUTINGKEY}-other-%d{yyyy-MM-dd}-%">
%d{ISO8601} [%t] %p %c{3} - %m%n
测试类如下所示
import .log4j.Log4j2;
import .;
@Log4j2
public class TestLog {
public static void main(String[] args) {
new Thread(() -> {
("ROUTINGKEY", ().getName());
("info");
("debug");
("error");
("ROUTINGKEY");
}).start();
new Thread(() -> {
("ROUTINGKEY", ().getName());
("info");
("debug");
("error");
("ROUTINGKEY");
}).start();
}
}
运行测试类,会得到如下两个日志文件,和,每个日志文件对应着一个线程。该程序使用Gradle构建,依赖的JAR包如下:
dependencies {
compile ':lombok:1.16.10'
compile '.log4j:log4j-core:2.6'
compile '.log4j:log4j-api:2.6'
}
需要注意的一点是,每次在使用log对象之前,需要先设置("ROUTINGKEY", ().getName());,设置的key和配置文件中的key要一致,而value可以是任意值,参考配置文件即可理解。
有没有发现,每次使用log对象,还需要添加额外的代码,这不是恶心他妈给恶心开门——恶心到家了吗?有没有更加优雅地解决办法呢?且看下节。
方法二实现StrLookup
修改配置文件如下
fileName="logs/concurrent-${thread:threadName}.log"
filePattern="logs/concurrent-${thread:threadName}-%d{MM-dd-yyyy}-%">
实现StrLookup中的lookup方法,代码如下:
import .;
import ..plugins.Plugin;
import .;
@Plugin(name = "thread", category = )
public class ThreadLookup implements StrLookup {
@Override
public String lookup(String key) {
return ().getName();
}
@Override
public String lookup(LogEvent event, String key) {
return () == null ? ().getName()
: ();
}
}
编写测试类,代码如下:
import .log4j.Log4j2;
@Log4j2
public class TestLog {
public static void main(String[] args) {
new Thread(() -> {
("info");
("debug");
("error");
}).start();
new Thread(() -> {
("info");
("debug");
("error");
}).start();
}
}
该测试类同样会得到两个日志文件,每个线程分别对应一个,但是在使用log对象之前不再需要设置("ROUTINGKEY", ().getName());,是不是清爽多了。
根据官方的性能测试我们知道,Loggers all async的性能最高,但是方法一使用的是Sync模式(因为Appender默认是synchronous的),方法二使用的是Async Appender模式,那么如何更进一步让所有的Loggers都是Asynchronous的,让我们的配置更完美呢?想要使用Loggers all async还需要做一步设置,如果是Maven或Gradle项目,需要在src/main/resources目录下添加配置文件,根据官网说明,其中内容为
Log4jContextSelector=.
dependencies {
compile ':lombok:1.16.10'
compile '.log4j:log4j-core:2.6'
compile '.log4j:log4j-api:2.6'
compile ':disruptor:3.3.6'
}
同时还需要在classpath下添加依赖的disruptor JAR包:disruptor:3.3.6。当配置使用了AsyncLoggerContextSelector之后,所有的Loggers就都是异步的了。有方法证明使用了Loggers all async吗,答案是有,默认的location不会传递给Loggers all async的I/O线程,所以如果不设置includeLocation=true的话,打印出来的日志中location信息是“?”,例如“2016-12-16 16:38:47,285 INFO [Thread-3] ? - info”,如果设置includeLocation=”true”之后,打印出“2016-12-16 16:39:14,899 INFO [Thread-3] TestLog - info”,Gradle构建依赖如下:
不同级别的日志输出到不同的文件中
通常情况下,用到的日志级别主要是info/debug/error三个,而如果不做特殊配置,这三者信息是写到一个日志文件中的,当我们需要不同级别的日志输出到不同的文件中时,需要如何做呢?配置信息如下:
logs
testLog
filePattern="${logFilePath}/$${date:yyyy-MM}/${logFileName}-%d{yyyy-MM-dd}_%">
filePattern="${logFilePath}/$${date:yyyy-MM}/${logFileName}-%d{yyyy-MM-dd}_%">
filePattern="${logFilePath}/$${date:yyyy-MM}/${logFileName}-%d{yyyy-MM-dd}_%">
src\main\resources\内容不变,如下
Log4jContextSelector=.
测试代码如下
import .log4j.Log4j2;
@Log4j2
public class TestLog {
public static void main(String[] args) {
new Thread(() -> {
("info");
("debug");
("error");
}).start();
new Thread(() -> {
("info");
("debug");
("error");
}).start();
}
}
Thread 1该程序会生成三份日志文件【,,】,如果你足够细心的话,就会发现线程1和线程2的info|debug|error信息都输出到一个info|debug|error日志文件中了。如何解决这个问题呢?换句话说,我想得到
Thread 1
Thread 1的info日志
Thread 1的debug日志
Thread 1的error日志
Thread 2
Thread 2的info日志
Thread 2的debug日志
Thread 2的error日志
六个日志文件,要如何实现呢?这正是下一节要讲述的内容。
不同线程不同级别的日志输出到不同的文件中
要实现该功能,还要从RoutingAppender身上做文章。RoutingAppender主要用来评估LogEvents,然后将它们路由到下级Appender。目标Appender可以是先前配置的并且可以由其名称引用的Appender,或者可以根据需要动态地创建Appender。RoutingAppender应该在其引用的任何Appenders之后配置,以确保它可以正确关闭。
RoutingAppender中的name属性用来指定该Appender的名字,它可以包含多个Routes子节点,用来标识选择Appender的条件,而Routes只有一个属性“pattern”,该pattern用来评估所有注册的Lookups,并且其结果用于选择路由。在Routes下可以有多个Route,每个Route都必须配置一个key,如果这个key匹配“pattern”的评估结果,那么这个Route就被选中。同时每个Route都必须引用一个Appender,如果这个Route包含一个ref属性,那么这个Route将引用一个在配置中定义的Appender,如果这个Route包含一个Appender的定义,那么这个Appender将会根据RoutingAppender的上下文创建并被重用。
废话说多了,直接上配置才简洁明了。配置如下:
logs
testLog
fileName="${logFilePath}/${logFileName}_${thread:threadName}_debug.log"
filePattern="${logFilePath}/$${date:yyyy-MM}/${logFileName}-%d{yyyy-MM-dd}-${thread:threadName}-debug_%">
fileName="${logFilePath}/${logFileName}_${thread:threadName}_info.log"
filePattern="${logFilePath}/$${date:yyyy-MM}/${logFileName}-%d{yyyy-MM-dd}-${thread:threadName}-info_%">
fileName="${logFilePath}/${logFileName}_${thread:threadName}_error.log"
filePattern="${logFilePath}/$${date:yyyy-MM}/${logFileName}-%d{yyyy-MM-dd}-${thread:threadName}-error_%">
和ThreadLookup类不变,依赖的JAR包和上一节一样。测试类如下
import .log4j.Log4j2;
@Log4j2
public class TestLog {
public static void main(String[] args) {
new Thread(() -> {
("info");
("debug");
("error");
}).start();
new Thread(() -> {
("info");
("debug");
("error");
}).start();
}
}
testLog_Thread-2_debug.log该程序会输出六个日志文件,分别是
testLog_Thread-2_error.log
testLog_Thread-2_info.log
testLog_Thread-3_debug.log
testLog_Thread-3_error.log
testLog_Thread-3_info.log
至此,就实现了不同线程不同级别的日志输出到不同文件中的功能。
如何启用All Loggers Asynchronous
为了使得所有的Loggers都是异步的,除了添加一个新的配置文件,就是外,还有其它方式吗?有的,仅列举如下
例如【IntelliJ IDEA】中使用Gradle构建项目,那么可以在Settings | Build, Execution, Deployment | Build Tools | Gradle | Gradle VM options中填入 -DLog4jContextSelector=.
另一种就是在前面提到的ThreadLookup类中,添加静态代码块 static {
("Log4jContextSelector", ".");
}
根据参考手册,有一点需要注意的就是,要使用或标签,而不是和,原文如下:
When AsyncLoggerContextSelector is used to make all loggers asynchronous, make sure to use normal and elements in the configuration. The AsyncLoggerContextSelector will ensure that all loggers are asynchronous, using a mechanism that is different from what happens when you configure or . The latter elements are intended for mixing async with sync loggers.
混合使用Synchronous和Asynchronous Loggers
需要disruptor-3.0.或更高版本的jar包,不需要设置系统属性Log4jContextSelector,在配置中可以混合使用Synchronous和asynchronous loggers,使用或者去指定需要异步的Loggers,元素还可以包含和用于同步的Loggers。注意如果使用的是或者,那么就无需设置系统属性Log4jContextSelector了。
一个混合了同步和异步的Loggers配置如下:
immediateFlush="false" append="false">
%d %p %class{1.} [%t] %location %m %ex%n
参考资料