1. 概述

日志是软件开发的重要组成部分,为我们提供了深入理解应用程序行为的关键洞察。在这个教程中,我们将探讨一个重要的日志功能——参数化日志。通过利用参数化日志,我们可以增强日志的全面性和效率。

Simple Logging Facade for Java(SLF4J)是一个广为人知的日志库,它提供了一个统一的抽象层,让开发者可以使用单一API,并在部署时插入任何兼容的日志框架,如Logback、log4j或SLF4J简单日志器。实际上,SLF4J API本身并不进行日志记录,我们可以在部署时选择任何想要的日志框架。

2. Maven依赖项

在深入到日志本身之前,让我们配置所需的依赖项。通常,我们需要包含两个依赖:slf4j-api,它将提供统一的外观,以及一个执行日志的实际日志实现。在本例中,我们将使用Logback作为日志实现,我们可以采用不同的方法。只需要添加一个名为logback-classic的单个依赖,它已经包含了slf4j-api

请在Maven的pom.xml中添加logback-classic依赖:

<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.4.8</version>
</dependency>

您可以在Maven中央仓库Maven Central仓库找到各自最新版本的slf4j-apilogback-classic

3. 日志初始化

第一步是初始化日志。这可以根据项目设置手动完成,或者通过Lombok来实现。让我们看看这两种方法。

手动初始化始终应使用LoggerLoggerFactory来自org.slf4j包:

public static final Logger log = LoggerFactory.getLogger(LoggingPlayground.class);

在类级别使用@Slf4j注解,Lombok会生成与上述手动初始化相同的代码行。

为了保持一致性,并为可能的Lombok迁移做好准备,我们可以为所有手动初始化的日志使用log名称。

4. 参数化日志

术语上讲,参数化日志是指将提供的参数注入到日志消息中。在过去,一些库版本并没有提供一种统一的方法来处理带有多个值的参数化日志。这就是为什么我们可以看到纯字符串连接、String.format()和其他技巧的使用。这些技术现在已不再必要,我们可以使用花括号{}在消息中添加任意数量的参数。

我们可以只记录一个参数:

log.info("App is running at {}", LocalDateTime.now());

我们也可以记录多个参数,占位符将按顺序填充。只需确保花括号的数量与传递的参数数量相匹配。幸运的是,大多数IDE会在出现这种不匹配时高亮显示。

以下是记录多个参数的示例:

log.info("App is running at {}, zone = {}, java version = {}, java vm = {}", LocalDateTime.now(), ZonedDateTime.now()
  .getZone(), System.getProperty("java.version"), System.getProperty("java.vm.name"));

上述代码的输出将是:

15:41:48.749 [main] INFO  c.b.p.logging.LoggingPlayground - App is running at 2023-07-20T15:41:48.749435, zone = Europe/Helsinki, java version = 11.0.15, java vm = Java HotSpot(TM) 64-Bit Server VM 

当库不支持多个参数时,常见的做法是使用Object[]来记录数据,但在新版本中不应使用这种方法:

log.info("App is running at {}, zone = {}, java version = {}, java vm = {}",
  new Object[] { ZonedDateTime.now(), ZonedDateTime.now().getZone(), System.getProperty("java.version"), System.getProperty("java.vm.name") });

输出将与四个单独的对象输出相同。

5. 流式日志

从SLF4J 2.0开始,流式日志提供了另一种向后兼容现有框架的方法。流式提供了一个构建日志事件的构建器API,逐步构建日志信息。因此,我们也可以使用此特性实现参数化日志。每个日志级别都有专用的构建器。每次创建构建器时,都应使用log()调用来实际打印消息。

例如,我们可以使用addArgument()方法,并将参数值添加到消息中的每个占位符:

log.atInfo().setMessage("App is running at {}, zone = {}")
  .addArgument(LocalDateTime.now())
  .addArgument(ZonedDateTime.now().getZone())
  .log();

我们的输出与非流式方法相同:

15:50:20.724 [main] INFO  c.b.p.l.FluentLoggingPlayground - App is running at 2023-07-20T15:50:20.724532900, zone = Europe/Helsinki 

另一种选择是使用addKeyValue(),并指定参数名及其值:

log.atInfo().setMessage("App is running at")
  .addKeyValue("time", LocalDateTime.now())
  .addKeyValue("zone", ZonedDateTime.now().getZone())
  .setCause(exceptionCause)
  .log();

为了使用addKeyValue()方法,我们的日志配置需要能够接受它。对于Logback,我们需要更新日志格式以包含%kvp占位符。如果没有指定,那么所有添加的数据都将被忽略:

<appender name="out" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
        <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg %kvp%n</pattern>
    </encoder>
</appender>

使用键值对方法,我们的参数值输出会有所不同:

15:52:35.835 [main] INFO  c.b.p.l.FluentLoggingPlayground - App is running at time="2023-07-20T15:52:35.834338500" zone="Europe/Helsinki"

6. 参数化日志与异常日志

经常有人问如何结合参数化日志和异常日志,因为声明的方法提供了传递参数或异常的能力。从SLF4J 1.6开始,这个问题已经被解决,我们可以将参数化日志与异常日志结合起来。

默认情况下,SLF4J将最新的参数视为Throwable的候选者。如果提供的参数是一个异常,SLF4J将在日志输出中打印完整的堆栈跟踪。

例如,对于给定的日志行:

log.info("App is running at {}, zone = {}, java version = {}, java vm = {}", LocalDateTime.now(), ZonedDateTime.now()
  .getZone(), System.getProperty("java.version"), System.getProperty("java.vm.name"), exceptionCause);

输出将是:

15:54:43.771 [main] INFO  c.b.p.logging.LoggingPlayground - App is running at 2023-07-20T15:54:43.771587300, zone = Europe/Helsinki, java version = 11.0.15, java vm = Java HotSpot(TM) 64-Bit Server VM 
java.lang.Exception: java.lang.IllegalArgumentException: Something unprocessable
    at com.baeldung.parameterized.logging.LoggingPlayground.main(LoggingPlayground.java:30)
Caused by: java.lang.IllegalArgumentException: Something unprocessable
    ... 1 common frames omitted

如果我们在这中间的某个位置传递Throwable,它将被视为普通对象,堆栈跟踪不会被打印。

也可以使用流式方法通过setCause()方法指定Throwable

log.atInfo()
  .setMessage("App is running at {}, zone = {}")
  .addArgument(LocalDateTime.now())
  .addArgument(ZonedDateTime.now().getZone())
  .setCause(exceptionCause)
  .log();

7. 总结

在这篇文章中,我们复习了如何使用参数化日志记录多个参数,并探索了更灵活的流式日志方法。此外,我们还探讨了如何将参数化日志与异常结合使用。

完整的示例可在GitHub上查看。