1. 背景
用了几年的Java日志框架,但却对里面的逻辑关系不是特别清楚,准备花时间理清一下其中的关系以及基本的使用说明
1.1 常见Java日志框架
Log4j
Log4j 是 Apache 的一个 Java 的日志库,通过使用 Log4j,我们可以控制日志信息输送的目的地(控制台、文件、数据库等);我们也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。
Logback
Logback,一个 “可靠、通用、快速而又灵活的 Java 日志框架”。logback 当前分成三个模块:logback-core,logback- classic 和 logback-access。logback-core 是其它两个模块的基础模块。logback-classic 是 log4j 的一个改良版本。此外 logback-classic 完整实现 SLF4J API 使你可以很方便地更换成其它日志系统,如 log4j 或 JDK14 Logging。logback-access 模块与 Servlet 容器(如 Tomcat 和 Jetty)集成,以提供 HTTP 访问日志功能。请注意,您可以在 logback-core 之上轻松构建自己的模块。
Log4j 2
Apache Log4j 2 是对 Log4j 的升级,它比其前身 Log4j 1.x 提供了重大改进,并提供了 Logback 中可用的许多改进,同时修复了 Logback 架构中的一些问题。
现在最优秀的 Java 日志框架是 Log4j2,没有之一。根据官方的测试表明,在多线程环境下,Log4j2 的异步日志表现更加优秀。在异步日志中,Log4j2 使用独立的线程去执行 I/O 操作,可以极大地提升应用程序的性能。
在官方的测试中,Log4j1/Logback/Log4j2 三个日志框架的异步日志性能比较如下图所示。
其中,Loggers all async是基于 LMAX Disruptor 实现的。可见 Log4j2 的异步日志性能是最棒的。

log4j,log4j2,logback 异步日志性能比较
下图比较了 Log4j2 框架Sync、Async Appenders和Loggers all async三者的性能。其中Loggers all async表现最为出色,而且线程数越多,Loggers all async性能越好。

log4j2 同步异步 Appender 比较
1.2 日志通用接口
上述介绍的是一些日志框架的实现(Log4j、Logback、log4j2),他们都有各自的API可以调用,但是我们更多是使用通用的日志调用接口来解决系统与日志实现框架的耦合性。日志通用接口,它不是一个真正的日志实现,而是一个抽象层( abstraction layer),它允许你在后台使用任意一个日志实现。常见的通用日志接口有commons logging、slf4j,由于前面一个基本没有使用过,所以不过多进行介绍。
2. Apache Log4j 2 详解
2.1 简介
Apache Log4j 2 是对 Log4j 的升级,它比其前身 Log4j 1.x 提供了重大改进,并提供了 Logback 中可用的许多改进,同时修复了 Logback 架构中的一些问题。所以后面的例子环境为slf4j&log4j2
2.2 依赖引入
1
2
3
4
5
6
7
// log4j核心包
implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.14.1'
implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.14.1'
// 除了核心包意外,还需要将log4j2与slf4j建立连接
implementation group: 'org.apache.logging.log4j', name: 'log4j-slf4j-impl', version: '2.14.1'
// 最后引入Slf4j的API
implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.31'
2.3 配置文件详解
Configuration
根节点,有以下两个属性
status
有 “trace”, “debug”, “info”, “warn”, “error” and “fatal”,用于控制 log4j2 日志框架本身的日志级别,如果将 stratus 设置为较低的级别就会看到很多关于 log4j2 本身的日志,如加载 log4j2 配置文件的路径等信息
monitorInterval
含义是每隔多少秒重新读取配置文件,可以不重启应用的情况下修改配置
Properties
属性。使用来定义常量,以便在其他配置项中引用,该配置是可选的,例如定义日志的存放位置
Appenders
输出源,用于定义日志输出的地方。 log4j2 支持的输出源有很多,有控制台 ConsoleAppender、文件 FileAppender、AsyncAppender、RandomAccessFileAppender、RollingFileAppender、RollingRandomAccessFile 等
ConsoleAppender
控制台输出源是将日志打印到控制台上,开发的时候一般都会配置,以便调试。
- name:指定 Appender 的名字。
- target:SYSTEM_OUT 或 SYSTEM_ERR, 一般只设置默认: SYSTEM_OUT。
- PatternLayout:输出格式,不设置默认为:%m%n。
AsyncAppender
异步输出。AsyncAppender 接受对其他 Appender 的引用,并使 LogEvents 在单独的 Thread 上写入它们。
默认情况下,AsyncAppender 使用 java.util.concurrent.ArrayBlockingQueue ,它不需要任何外部库。请注意,多线程应用程序在使用此 appender 时应小心:阻塞队列容易受到锁争用的影响,并且我们的 测试表明, 当更多线程同时记录时性能可能会变差。考虑使用无锁异步记录器以获得最佳性能。
FileAppender
文件输出源,用于将日志写入到指定的文件,其底层是一个 OutputStreamAppender,需要配置输入到哪个位置(例如:D:/logs/mylog.log)
- name:指定 Appender 的名字。
- fileName:指定输出日志的目的文件带全路径的文件名。
- PatternLayout:输出格式,不设置默认为:%m%n。
RollingFileAppender
RollingFileAppender 是一个 OutputStreamAppender,它写入 fileName 参数中指定的 File,并根据 TriggeringPolicy 和 RolloverPolicy 滚动文件。
RandomAccessFileAppender
RandomAccessFileAppender 类似于标准的 FileAppender ,除了它总是被缓冲(这不能被关闭),并且在内部它使用
ByteBuffer + RandomAccessFile而不是BufferedOutputStream。与 FileAppender 相比,我们在测量中看到 “bufferedIO = true”,性能提升了 20-200% 。RollingRandomAccessFileAppender
RollingRandomAccessFileAppender 类似于标准的 RollingFileAppender, 除了它总是被缓冲(这不能被关闭),并且在内部它使用
ByteBuffer + RandomAccessFile而不是BufferedOutputStream。与 RollingFileAppender 相比,我们在测量中看到 “bufferedIO = true”,性能提升了 20-200%。RollingRandomAccessFileAppender 写入 fileName 参数中指定的文件,并根据 TriggeringPolicy 和 RolloverPolicy 滚动文件。- name:指定 Appender 的名字。
- fileName 指定当前日志文件的位置和文件名称
- filePattern 指定当发生 Rolling 时,文件的转移和重命名规则
- immediateFlush 设置为 true 时 - 默认值,每次写入后都会进行刷新。这将保证数据写入磁盘,但可能会影响性能。
- bufferSize 缓冲区大小,默认为 262,144 字节(256 * 1024)。
- Policies:指定滚动日志的策略,就是什么时候进行新建日志文件输出日志。
- SizeBasedTriggeringPolicy 指定当文件大小大于 size 指定的值时,触发 Rolling
- TimeBasedTriggeringPolicy 这个配置需要和 filePattern 结合使用,日期格式精确到哪一位,interval 也精确到哪一个单位。注意 filePattern 中配置的文件重命名规则是 ${FILE_NAME}-%d{yyyy-MM-dd HH-mm-ss}-%i,最小的时间粒度是 ss,即秒钟。TimeBasedTriggeringPolicy 默认的 size 是 1,结合起来就是每 1 秒钟生成一个新文件。如果改成 %d{yyyy-MM-dd HH},最小粒度为小时,则每一个小时生成一个文件
- DefaultRolloverStrategy 指定最多保存的文件个数
Filters
Filters 决定日志事件能否被输出。过滤条件有三个值:ACCEPT(接受),DENY(拒绝),NEUTRAL(中立)。
ThresholdFilter
输出 warn 级别一下的日志
1 2 3 4 5 6
<Filters> <!--如果是error级别拒绝,设置 onMismatch="NEUTRAL" 可以让日志经过后续的过滤器--> <ThresholdFilter level="error" onMatch="DENY" onMismatch="NEUTRAL"/> <!--如果是debug\info\warn级别的日志输出--> <ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY"/> </Filters>
只输出 error 级别以上的日志
1 2 3
<Filters> <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/> </Filters>
TimeFilter
时间过滤器可用于将过滤器限制为仅当天的某个部分。
1 2 3 4
<Filters> <!-- 只允许在每天的 8点~8点半 之间输出日志 --> <TimeFilter start="08:00:00" end="08:30:00" onMatch="ACCEPT" onMismatch="DENY" /> </Filters>
PatternLayout
控制台或文件输出源(Console、File、RollingRandomAccessFile)都必须包含一个 PatternLayout 节点,用于指定输出文件的格式(如 日志输出的时间 文件 方法 行数 等格式)。
简单示例:
1
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss,SSS} [%t] %-5level %logger{0} - %msg%n" charset="UTF-8"/>
详细配置请查看官网
Policy & Strategy
Policy 触发策略
SizeBasedTriggeringPolicy
基于日志文件大小的触发策略。单位有:KB,MB,GB
1
<SizeBasedTriggeringPolicy size="10 MB"/>
CronTriggeringPolicy
基于
Cron表达式的触发策略,很灵活。1
<CronTriggeringPolicy schedule="0/5 * * * * ?" />
TimeBasedTriggeringPolicy
基于时间的触发策略。该策略主要是完成周期性的 log 文件封存工作。有两个参数:
interval,integer 型,指定两次封存动作之间的时间间隔。这个配置需要和filePattern结合使用,filePattern日期格式精确到哪一位,interval 也精确到哪一个单位。注意filePattern中配置的文件重命名规则是 %d{yyyy-MM-dd HH-mm-ss}-%i,最小的时间粒度是 ss,即秒钟。 TimeBasedTriggeringPolicy 默认的 size 是 1,结合起来就是每 1 秒钟生成一个新文件。如果改成 %d{yyyy-MM-dd HH},最小粒度为小时,则每一个小时生成一个文件
modulate,boolean 型,说明是否对封存时间进行调制。若 modulate=true, 则封存时间将以 0 点为边界进行偏移计算。比如,modulate=true,interval=4hours, 那么假设上次封存日志的时间为 03:00,则下次封存日志的时间为 04:00, 之后的封存时间依次为 08:00,12:00,16:00
简单示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
<Appenders>
<RollingRandomAccessFile name="File" fileName="./log/app.log"
filePattern="./log/app-%d{yyyy-MM-dd HH-mm}-%i.log">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss,SSS} [%t] %-5level %logger{0} - %msg%n" charset="UTF-8"/>
<Policies>
<!-- 每 5s 翻滚一次 -->
<!--<CronTriggeringPolicy schedule="0/5 * * * * ?"/>-->
<!--根据当前filePattern配置,日志文件每3分钟滚动一次-->
<TimeBasedTriggeringPolicy interval="3"/>
<!--日志文件大于10 KB滚动一次-->
<SizeBasedTriggeringPolicy size="10 KB"/>
</Policies>
<!--保存日志文件个数-->
<DefaultRolloverStrategy max="10"/>
</RollingRandomAccessFile>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="File"/>
</Root>
</Loggers>
</Configuration>
Strategy 滚动策略
DefaultRolloverStrategy
默认滚动策略
常用参数:max,保存日志文件的最大个数,默认是 7,大于此值会删除旧的日志文件。
1 2
<!--保存日志文件个数--> <DefaultRolloverStrategy max="10"/>
DirectWriteRolloverStrategy
日志直接写入由文件模式表示的文件。
这两个Strategy都是控制如何进行日志滚动的,平时大部分用DefaultRolloverStrategy就可以了。
Loggers
Loggers 节点,常见的有两种:Root 和 Logger。 Root节点用来指定项目的根日志,如果没有单独指定Logger,那么就会默认使用该 Root 日志输出
Root
每个配置都必须有一个根记录器 Root。如果未配置,则将使用默认根 LoggerConfig,其级别为 ERROR 且附加了 Console appender。根记录器和其他记录器之间的主要区别是:1. 根记录器没有 name 属性。2. 根记录器不支持 additivity 属性,因为它没有父级。
- level:日志输出级别,共有 8 个级别,按照从低到高为:All < Trace < Debug < Info < Warn < Error < Fatal < OFF
- AppenderRef:Root 的子节点,用来指定该日志输出到哪个 Appender.
Logger
Logger 节点用来单独指定日志的形式,比如要为指定包下的 class 指定不同的日志级别等。
使用Logger元素必须有一个 name 属性,root logger 不用 name 元属性 每个Logger可以使用 TRACE,DEBUG,INFO,WARN,ERROR,ALL 或 OFF 之一配置级别。如果未指定级别,则默认为 ERROR。可以为 additivity 属性分配值 true 或 false。如果省略该属性,则将使用默认值 true。
Logger还可以配置一个或多个 AppenderRef 属性。引用的每个 appender 将与指定的Logger关联。如果在Logger上配置了多个 appender,则在处理日志记录事件时会调用每个 appender。
name:用来指定该 Logger 所适用的类或者类所在的包全路径,继承自 Root 节点。一般是项目包名或者框架的包名,比如:com.jourwon,org.springframework
- level:日志输出级别,共有 8 个级别,按照从低到高为:All < Trace < Debug < Info < Warn < Error < Fatal < OFF
- AppenderRef:Logger 的子节点,用来指定该日志输出到哪个 Appender,如果没有指定,就会默认继承自 Root。如果指定了,那么会在指定的这个 Appender 和 Root 的 Appender 中都会输出,此时我们可以设置 Logger 的 additivity=”false” 只在自定义的 Appender 中进行输出。
2.4 添加配置文件
默认情况下,Log4j2 在 classpath 下查找名为log4j2.xml的配置文件。你也可以使用 Java 启动命令指定配置文件的全路径。-Dlog4j.configurationFile=opt/demo/log4j2.xml,你还可以使用 Java 代码指定配置文件路径
常用日志配置文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
<Properties>
<!-- 日志输出级别 -->
<Property name="LOG_INFO_LEVEL" value="info"/>
<!-- error级别日志 -->
<Property name="LOG_ERROR_LEVEL" value="error"/>
<!-- 在当前目录下创建名为log目录做日志存放的目录 -->
<Property name="LOG_HOME" value="./log"/>
<!-- 档案日志存放目录 -->
<Property name="LOG_ARCHIVE" value="./log/archive"/>
<!-- 模块名称, 影响日志配置名,日志文件名,根据自己项目进行配置 -->
<Property name="LOG_MODULE_NAME" value="spring-boot"/>
<!-- 日志文件大小,超过这个大小将被压缩 -->
<Property name="LOG_MAX_SIZE" value="100 MB"/>
<!-- 保留多少天以内的日志 -->
<Property name="LOG_DAYS" value="15"/>
<!--输出日志的格式:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度, %msg:日志消息,%n是换行符 -->
<Property name="LOG_PATTERN" value="%d [%t] %-5level %logger{0} - %msg%n"/>
<!--interval属性用来指定多久滚动一次-->
<Property name="TIME_BASED_INTERVAL" value="1"/>
</Properties>
<Appenders>
<!-- 控制台输出 -->
<Console name="STDOUT" target="SYSTEM_OUT">
<!--输出日志的格式-->
<PatternLayout pattern="${LOG_PATTERN}"/>
<!--控制台只输出level及其以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
<ThresholdFilter level="${LOG_INFO_LEVEL}" onMatch="ACCEPT" onMismatch="DENY"/>
</Console>
<!-- 这个会打印出所有的info级别以上,error级别一下的日志,每次大小超过size或者满足TimeBasedTriggeringPolicy,则日志会自动存入按年月日建立的文件夹下面并进行压缩,作为存档-->
<RollingRandomAccessFile name="RollingRandomAccessFileInfo"
fileName="${LOG_HOME}/${LOG_MODULE_NAME}-infoLog.log"
filePattern="${LOG_ARCHIVE}/${LOG_MODULE_NAME}-infoLog-%d{yyyy-MM-dd}-%i.log.gz">
<Filters>
<!--如果是error级别拒绝,设置 onMismatch="NEUTRAL" 可以让日志经过后续的过滤器-->
<ThresholdFilter level="${LOG_ERROR_LEVEL}" onMatch="DENY" onMismatch="NEUTRAL"/>
<!--如果是info\warn输出-->
<ThresholdFilter level="${LOG_INFO_LEVEL}" onMatch="ACCEPT" onMismatch="DENY"/>
</Filters>
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies>
<!--interval属性用来指定多久滚动一次,根据当前filePattern设置是1天滚动一次-->
<TimeBasedTriggeringPolicy interval="${TIME_BASED_INTERVAL}"/>
<SizeBasedTriggeringPolicy size="${LOG_MAX_SIZE}"/>
</Policies>
<!-- DefaultRolloverStrategy属性如不设置,则默认同一文件夹下最多保存7个文件-->
<DefaultRolloverStrategy max="${LOG_DAYS}"/>
</RollingRandomAccessFile>
<!--只记录error级别以上的日志,与info级别的日志分不同的文件保存-->
<RollingRandomAccessFile name="RollingRandomAccessFileError"
fileName="${LOG_HOME}/${LOG_MODULE_NAME}-errorLog.log"
filePattern="${LOG_ARCHIVE}/${LOG_MODULE_NAME}-errorLog-%d{yyyy-MM-dd}-%i.log.gz">
<Filters>
<ThresholdFilter level="${LOG_ERROR_LEVEL}" onMatch="ACCEPT" onMismatch="DENY"/>
</Filters>
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies>
<TimeBasedTriggeringPolicy interval="${TIME_BASED_INTERVAL}"/>
<SizeBasedTriggeringPolicy size="${LOG_MAX_SIZE}"/>
</Policies>
<DefaultRolloverStrategy max="${LOG_DAYS}"/>
</RollingRandomAccessFile>
</Appenders>
<Loggers>
<!-- 开发环境使用 -->
<!--<Root level="${LOG_INFO_LEVEL}">
<AppenderRef ref="STDOUT"/>
</Root>-->
<!-- 测试,生产环境使用 -->
<Root level="${LOG_INFO_LEVEL}">
<AppenderRef ref="RollingRandomAccessFileInfo"/>
<AppenderRef ref="RollingRandomAccessFileError"/>
</Root>
</Loggers>
</Configuration>
2.5 日志答应重复问题
如果Root中的日志包含了Logger中的日志信息,并且AppenderRef是一样的配置,则日志会打印两次。
这是 log4j2 继承机制问题,在 Log4j2 中,logger 是有继承关系的,root 是根节点,在 log4j2 中,有个 additivity 的属性,它是子 Logger 是否继承 父 Logger 的 输出源(appender) 的属性。具体说,默认情况下子 Logger 会继承父 Logger 的 appender,也就是说子 Logger 会在父 Logger 的 appender 里输出。若是 additivity 设为 false,则子 Logger 只会在自己的 appender 里输出,而不会在父 Logger 的 appender 里输出。
要打破这种传递性,也非常简单,在 logger 中添加 additivity = “false”,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
<Appenders>
<Console name="Console">
<PatternLayout>
<Pattern>%d{yyyy-MM-dd HH:mm:ss,SSS} [%t] %-5level %logger{0} - %msg%n</Pattern>
</PatternLayout>
</Console>
</Appenders>
<Loggers>
<!-- name属性为项目包名或者类名 -->
<Logger name="com.jourwon" level="debug" additivity="false">
<AppenderRef ref="Console"/>
</Logger>
<Root level="error">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
2.6 使用 Lombok 工具简化创建 Logger 类
lombok 就是一个注解工具 jar 包,能帮助我们省略一繁杂的代码。
引入依赖
1
2
3
4
5
6
// lombok
compileOnly 'org.projectlombok:lombok:1.18.20'
annotationProcessor 'org.projectlombok:lombok:1.18.20'
testCompileOnly 'org.projectlombok:lombok:1.18.20'
testAnnotationProcessor 'org.projectlombok:lombok:1.18.20'
使用 Lombok 后,@Slf4j 注解生成了 log 日志常量,无需去声明一个 log 就可以在类中使用 log 记录日志。
1
2
3
4
5
6
@Slf4j
public class Log4jTest {
public static void main(String[] args) {
log.error("Something else is wrong here");
}
}