Log4j2使用

log4j2的基础使用方式

log4j2是apache的一个使用非常广泛的日志框架,用于记录应用程序的日志信息。

需要的依赖

要想使用该日志框架,需要引入如下依赖。

1
2
3
4
5
6
7
8
9
10
11
<!-- <log4j2.version>2.17.2<log4j2.version> -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>${log4j2.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${log4j2.version}</version>
</dependency>

需要的配置

同时在resources目录下,创建日志配置文件,指定将日志输出到控制台。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# log4j2.properties
# 定义log4j2自身内部日志的级别
status = INFO

# appender定义
appender.console.type = Console
appender.console.name = STDOUT
appender.console.layout.type = PatternLayout
appender.console.layout.pattern = %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n

# 日志记录器定义
rootLogger.level = debug
rootLogger.appenderRefs = stdout
rootLogger.appenderRef.stdout.ref = STDOUT

就上面的日志配置,解释如下。

log4j2自身内部日志级别

log4j2作为一个日志框架,其在启动,初始化配置,或者其他事件的时候,也会产生一些日志,这些日志被称之为log4j2自身内部的日志。
我们通过status = INFO,实际上是定义了这些日志的级别。

日志记录器和日志输出器

在log4j2中,有两个核心概念。

  • 日志记录器(rootLogger/logger):用于在代码中输出日志信息。rootLogger是全局的根记录器,负责管理所有日志的默认行为。
  • 日志输出器(appender):负责将日志输出到指定的位置,比如控制台或文件。如上所示,我们定义了一个appender,该appender的功能就是可以将日志输出到控制台。
appender配置的逐行解释
  • appender.console.type = Console:定义了一个名为console的日志输出器,其类型为Console,表示将日志输出到控制台。
  • appender.console.name = STDOUT:定义了一个名字叫STDOUT的输出器,这个是该appender的唯一标识符,logger可以通过该name引用这个appender。
  • appender.console.layout.type = PatternLayout:定义了appender的布局类型为PatternLayout,表示日志内容的格式化方式,PatternLayout允许你通过一个模式字符串来完全自定义日志消息的输出格式。其他布局类型还包括JSONLayoutXMLLayout等。
  • appender.console.layout.pattern = %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n:定义了日志输出的格式模式。

其中appender.console.layout.pattern的格式说明如下:

  • %d{HH:mm:ss.SSS}:表示日志事件的时间戳,格式为小时:分钟:秒.毫秒。
  • [%t]:表示当前线程的名称。
  • %-5level:表示日志级别,左对齐,占5个字符宽度。
  • %logger{36}:表示日志记录器的名称,最多显示36个字符。
  • -:表示日志消息前的分隔符,只是为了格式美观
  • %msg:表示日志消息内容
  • %n:表示换行符, 确保每条日志消息后都有一个换行。
logger配置的逐行解释
  • rootLogger.level = debug:定义了根记录器的日志级别为debug
  • rootLogger.appenderRefs = stdout:指定rootLogger将引用哪些Appender来输出日志,appenderRefs是一个列表,可以包含多个appender, 表示logger可以将这个日志时间发送给多个appender进行输出。这里仅只定了一个,表示rootLogger将会把日志时间发送给名为stdout的appender。
  • rootLogger.appenderRef.stdout.ref = STDOUT:这里首先定义一个名字叫stdout的appender引用,该引用实际指向的appender指向上面定义的STDOUT,表示将日志输出到控制台。

代码示例

在Java代码中,使用该日志框架。

1
2
3
4
5
6
7
8
9
10
11
import org.apache.logging.log4j.LogManager;  
import org.apache.logging.log4j.Logger;

public class App {
private static final Logger log = LogManager.getLogger(App.class);
public static void main(String[] args) {
log.info("hello world");
log.debug("--------------");
System.out.println("aaaa");
}
}

运行,在控制台输出如下内容。

1
2
3
17:18:59.080 [main] INFO  com.guluo.App - hello world
17:18:59.087 [main] DEBUG com.guluo.App - --------------
aaaa

log4j2的实际应用方式

在实际开发中,我们通常不会直接在 Java 代码中使用 log4j2 的 API,因为这样会导致业务代码与具体日志框架强耦合,不利于后续日志框架的替换和维护。

为了实现业务代码与实际的日志框架解耦,常用的办法就是引入slf4j。

slf4j 主要为 Java 日志访问提供了一个标准化的 API 框架,其核心作用是作为日志门面,定义统一的接口,具体的日志实现则由其他框架(如 log4j、logback 等)完成。虽然 slf4j 自身也提供了简单的实现,但实际项目中很少直接使用。通常,Java 项目会选择 slf4j-api 作为日志门面,结合具体实现框架(如 log4j2、logback),并通过桥接器进行集成。

例如,项目中以 slf4j 作为统一日志入口,实际日志实现采用 log4j2 时,可以使用 log4j-slf4j-implslf4j-log4j12 进行绑定。两者区别在于:

  • slf4j-log4j12 主要用于对接 log4j 1.x 版本(虽然也可用于 log4j2,但不推荐);
  • log4j-slf4j-impl 专为 log4j2 设计,推荐用于 log4j2 集成。

这样可以实现业务代码与日志实现的解耦,便于后续维护和替换日志框架。

log4j2与log4j的简单对比

可以简单认为log4j2是log4j的进阶版本,弥补了log4j日志框架的一些不足。下面是两者的比较。

  • 安全性:最显著的区别是安全性。log4j存在一些安全漏洞,其中最为严重的是远程代码执行漏洞,可能导致系统被攻击。log4j2在安全性方面做了改进,修复了这些漏洞,提高了系统的安全性。
    详情参考: Log4j 严重漏洞修复方案参考 CVE-2021-44228 – KINGX
  • 性能:log4j2在设计上进行了优化,采用异步日志记录机制,因此在性能方面通常比log4j更高效。log4j2能够更好地处理大量日志记录并提供更好的性能表现。
  • 配置灵活性:log4j2提供了更灵活的配置选项和功能,使得开发人员可以更好地控制日志记录的行为。相对而言,log4j2在配置方面更加灵活和强大。

总的来说,log4j2相对于log4j在安全性、性能和配置灵活性方面都有所改进和优势,因此在选择日志框架时,更推荐使用log4j2以获得更好的体验和保障。

集成方式

由于目前log4j2使用更为普遍,因此在采用log4j-slf4j-impl作为桥接器实现log4j2和slf4j的集成。

在基础使用方式的基础上,再添加依赖,如下所示。

1
2
3
4
5
<dependency>  
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.17.2</version>
</dependency>

然后,日志仍然是log4j2.properties,我们额外添加了一个普通logger

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
status = INFO  

# 定义控制台输出
appender.console.type = Console
appender.console.name = STDOUT
appender.console.layout.type = PatternLayout
appender.console.layout.pattern = %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n

# 根记录器配置
rootLogger.level = debug
rootLogger.appenderRefs = stdout
rootLogger.appenderRef.stdout.ref = STDOUT

# 定义一个普通的logger
logger.http.name = test-name
logger.http.level = info
logger.http.appenderRefs = console
logger.http.appenderRef.console.ref = STDOUT
# 表示不再传递给父 Logger
logger.http.additivity = false

Java代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import org.slf4j.Logger;  
import org.slf4j.LoggerFactory;

public class SlfLogTest {
private static final Logger LOG = LoggerFactory.getLogger(SlfLogTest.class);
private static final Logger test_log = LoggerFactory.getLogger("test-name");

public static void main(String[] args) {
LOG.info("hello world.....");
LOG.debug("sssssssssssssssssssssss");

test_log.info("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
test_log.debug("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
}
}

输出如下所示。

1
2
3
11:34:03.573 [main] INFO  com.guluo.SlfLogTest - hello world.....
11:34:03.582 [main] DEBUG com.guluo.SlfLogTest - sssssssssssssssssssssss
11:34:03.583 [main] INFO test-name - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

由于test-name配置的是,仅输出info级别及以上的日志,因此test_log.debug()日志不会输出,结果也与预期一致。

快速切换日志实现

如果此时因为业务需要,我们需要将程序中的日志实现,从log4j2切换到logback,只需要做如下修改。

更新日志实现的依赖
删除上述依赖,并添加如下依赖。

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.6</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
</dependency>

更新日志配置文件
删除log4j2.properties,并添加logback.xml,内容如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>

<root level="debug">
<appender-ref ref="STDOUT" />
</root>

<logger name="test-name" level="info" additivity="false">
<appender-ref ref="STDOUT" />
</logger>
</configuration>

完成上述步骤,即可实现日志框架的快速切换。可以发现,我们仅仅是修改了依赖和配置文件,而业务代码完全不需要修改。
这就是使用日志门面(如 slf4j)和具体日志实现(如 log4j2、logback)的好处。通过这种方式,我们可以轻松地在不同的日志框架之间切换,而不需要修改业务代码,从而实现了日志框架的解耦和灵活性。