在 Log4j2 中更好地执行非日志记录器调用

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观

使用 Log4j 1.x 时,使用 日志记录保护 是一种常见的做法,并且希望避免在某些情况下可能发生的额外性能影响,即使实际上没有记录消息。 Simple Logging Facade for Java ( SLF4J ) 为 Java 日志带来的最吸引人的特性之一是能够 减少 需要进行这些日志级别检查的情况。在这篇文章中,我将了解如何使用 Log4j 2.x 日志记录 API 更改 来实现类似的好处。

下一个代码清单演示了记录长时间运行的操作。第一个示例在名称为“slow”的实例上隐式调用 toString() 方法。第二个日志记录示例调用一个长时间运行的方法。

传统的、无人看守的日志记录


 // Will implicitly invoke slow's toString() method  
logger.debug("NORMAL: " + slow);  
// Will explicitly invoke the long-running method expensiveOperation()  
logger.debug(expensiveOperation());

在前面的示例中,即使实际上没有执行日志记录,两次日志记录操作也将花费很长时间。前面代码清单中的日志语句只会在日志级别为 DEBUG 或不太具体的日志级别(如 TRACE)时实际记录,但即使没有记录任何内容,它们的昂贵操作也会运行。

在 Log4j 1.x 中有两种方法来处理这个问题。一种方法,通常也是最好的方法,是尝试重写日志语句,以便 不涉及长时间运行的操作 。当这不切实际时(例如当 需要与长时间运行的操作相关联的上下文 以使日志消息有用时),可以使用日志保护。接下来将演示这种适用于 Log4j 1.x 的方法。

传统的、受保护的日志记录


 // Will implicitly invoke slow's toString() method  
logger.debug("NORMAL: " + slow);  
// Will explicitly invoke the long-running method expensiveOperation()  
logger.debug(expensiveOperation());

日志守卫,如前面的代码清单所示,可以有效地防止长时间运行的操作被调用,即使没有消息被记录。然而,使用日志保护确实带来了一些缺点。也许最重要的缺点是引入了额外的(有些人会说臃肿的)代码。另一个潜在的缺点不太常见,但更严重:随着条件和关联块引入的额外范围,它更容易在条件中引入错误代码,甚至可能在日志级别依赖中引入副作用代码块。

最常见的情况之一是,当对象被传递给记录器调用或 传递给记录器调用。这种情况在上面的两个代码清单中通过将字符串文字“GUARDED:”连接到名为“slow”的变量的隐式调用的 toString() 方法来传递给记录器调用的字符串进行了演示。

SLF4J 普及了 参数化日志记录调用 的概念,Log4j 2 在其日志记录 API 中提供了 类似的支持 。下面的代码演示了它是如何使用的。

参数化日志记录


 // Will implicitly invoke slow's toString() method  
logger.debug("NORMAL: " + slow);  
// Will explicitly invoke the long-running method expensiveOperation()  
logger.debug(expensiveOperation());

当上面的参数化日志记录示例以比 DEBUG 更具体的日志级别执行时,由于参数化日志记录,将不会尝试“慢”变量上的隐式 toString() 。但是,参数化日志记录无法帮助其他日志记录情况,因为尽管有参数化日志记录,方法 expensiveOperation() 仍将被调用。另请注意,虽然参数化日志记录在隐式 toString() 调用的情况下有所帮助,但它对显式 toString() 调用 没有帮助 。即使日志记录级别比 DEBUG 更具体,在记录器语句中调用 slow.toString() 仍然会导致性能下降。

Log4j 2.4 引入了一种 基于 Lambda 的机制 ,可用于延迟调用传递给记录器调用的方法,这样,如果语句的记录级别低于当前日志级别,则根本不需要执行这些方法.这在下一个代码清单中进行了演示,其中通过 lambda 表达式对“慢”变量的对象显式调用了 toString() 方法,并通过 方法引用 调用了 expensiveOperation 方法。

Lambda 表达式记录


 // Will implicitly invoke slow's toString() method  
logger.debug("NORMAL: " + slow);  
// Will explicitly invoke the long-running method expensiveOperation()  
logger.debug(expensiveOperation());

当上面的代码在日志级别设置为比 DEBUG 更具体的级别时执行,由于基于 lambda 表达式的延迟加载,“慢”对象的 toString() 方法和 expensiveOperation 方法都不会被调用。换句话说,类似于示例如何使用守卫,使用 lambda 表达式可以防止不必要地执行可能长时间运行的方法,除非它们的结果真的要被记录下来。此 lambda 表达式 支持已添加到版本 2.4 的 Log4j 中,当然需要 Java 8

概括

Log4j 2 (2.4) 提供了多种方法来避免在实际未记录消息时日志语句对性能的影响。

  1. 可以重写日志语句,以便根本不记录昂贵的方法(包括昂贵的 toString() 调用)。
  2. 日志守卫可用于确保日志语句的长时间运行的方法调用仅在实际记录消息时才执行。
  3. Log4j 2 的参数化(格式化)记录器 API 可用于消除隐式 toString() 方法的调用,除非确实记录了消息。
  4. Log4j 2.4 的 lambda 表达式记录器 API 可用于消除对记录消息所需的任何操作(隐式或显式)的调用,除非消息确实被记录了。