【问题标题】:Java logging API overheadJava 日志记录 API 开销
【发布时间】:2026-01-08 08:55:01
【问题描述】:

我已经阅读了一些关于使用 Java 记录调试消息的各种方法,并且来自 C 背景,我的担忧如下:

在禁用日志记录的情况下(例如生产环境),这些库声称开销最小,但由于仍在评估其 log() 函数的参数,我担心的是实际场景中的开销实际上会,一点都不能忽略。

例如,log(myobject.toString(), "info message") 仍然有评估 myobject.toString() 的开销,这可能相当大,即使日志函数本身什么也不做。

有人有解决这个问题的办法吗?

PS:对于那些想知道我为什么提到 C 背景的人:C 允许您使用预处理器宏和编译时指令,这些指令将在编译时完全删除与调试相关的所有代码,包括宏参数(根本不会出现在全部)。

编辑: 在阅读了第一批答案之后,似乎 java 显然没有任何可以解决问题的方法(想想在 CPU 的每一点都很重要的移动环境中将数字的余弦记录在一个大循环中)。所以我要补充一点,我什至会选择基于 IDE 的解决方案。我最后的手段是构建类似“查找全部/替换”宏的东西。 我首先认为也许从面向方面的框架中获取的东西会有所帮助...... 任何人 ?

【问题讨论】:

    标签: java logging conditional-compilation overhead


    【解决方案1】:

    我认为the log4j FAQ 很好地解决了这个问题:

    对于一些logger l,写作,

    l.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));
    

    会产生构造消息参数的成本,即将整数 i 和 entry[i] 都转换为字符串,并连接中间字符串。这与是否记录消息无关。

    如果你担心速度,那就写

     if(l.isDebugEnabled()) {
         l.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));
     }
    

    这样,如果对 logger l 禁用调试,您将不会产生参数构造的成本。另一方面,如果记录器启用了调试,您将承担评估记录器是否启用的成本,两次:一次在 debugEnabled 中,一次在调试中。这是一个微不足道的开销,因为评估记录器所需的时间不到实际记录语句所需时间的 1%。

    在这里使用保护子句是避免字符串构造的一般方法。

    其他流行的框架,例如slf4j,采用using formatted strings / parameterized messages 的方法,因此除非需要,否则不会评估消息。

    【讨论】:

    • +1 表示最后一行。参数化参数始终是一个选项。
    • @Tim Bender,只要你没有任何原语。
    • 感谢您的回答。参数化消息不适合我,因为您有时需要构建复杂的表达式(例如计算),而我真的不想在生产环境中这样做。我会稍等片刻,看看是否有人提供了一个神奇的解决方案,因为我仍然觉得令人难以置信的是,java 没有一种完全没有开销的方法......
    • 在任何编程环境中,如何在没有任何开销的情况下完成它?需要解析表达式/变量才能传递给函数。您可以传递一个函数指针,这似乎更加复杂。在 Java 中,您可以通过传递一个扩展 Object 的匿名函数来完成此操作,其中 toString() 返回您想要(有条件地)记录的表达式,但这会使您的代码更加难看。
    • 看看 C 预处理器和宏,你会感到惊讶。另外,看看我的编辑评论。
    【解决方案2】:

    现代日志框架有变量替换。您的日志记录如下所示:

    log.debug("The value of my first object is %s and of my second object is %s", firstObject, secondObject).
    

    给定对象的 toString() 将仅在日志记录设置为调试时执行。否则它将忽略参数并返回。

    【讨论】:

      【解决方案3】:

      答案很简单:不要在日志调用本身中调用昂贵的方法。另外,如果无法避免,请在日志调用周围使用警卫。

       if(logger.isDebugEnabled()) {
          logger.debug("this is an "+expensive()+" log call.");
       }
      

      正如其他人指出的那样,如果您的日志框架中有可用的格式(即,如果您使用一种足够现代的格式来支持它,那么应该是每一个,但不是't),您应该依靠它来帮助支付记录时的费用。如果您选择的框架已经支持格式化,那么请切换或编写您自己的包装器。

      【讨论】:

      • 我发现这是一个弄乱我的代码的解决方法:为什么我要为一个琐碎的日志语句添加 3 行代码?
      • 那不用呢?我的意思是,嘿,您可以选择使用什么日志记录框架以及如何使用;我自己不使用保护语句,因为如果底层结构不支持对格式化语句的延迟评估,我确实使用包装器进行日志记录。
      • 对不起,如果您觉得这是一次攻击,我不是故意的。我只是想添加该评论,以便知道这不是一种普遍喜爱的方法;-)
      • 当然,NP。我没有感到受到攻击;我只是在呼应有关保护条款的党派路线(由 Ceki Gulcu 撰写)。我并不真正尊重大多数日志记录框架,因为它们确实相当不灵活,而且有趣的是 Ceki - 一个好人,顺便说一句 - 现在正在他第四次尝试编写全面的日志记录框架。
      • 省略大括号,3行合二为一。在你的 IDE 中绑定一个快捷方式/模板,以自动打印保护子句并以当前方法的名称启动日志消息,然后只需敲几下键。
      【解决方案4】:

      你是对的,评估 log() 调用的参数可以增加不必要的开销,可能代价高昂。

      这就是为什么大多数健全的日志框架也提供一些字符串格式化功能,以便您可以编写如下内容:

      log.debug("Frobnicating {0}", objectWithExpensiveToString);
      

      这样,您的唯一开销就是调用debug()。如果该级别已停用,则不再执行任何操作,如果已激活,则解释格式字符串,调用 objectWithExpensiveToString() 上的 toString() 并将结果插入到格式字符串中,然后再记录。

      一些日志语句使用 MessageFormat 样式占位符 ({0}),其他使用 format() style 占位符 (%s),还有一些可能采用第三种方法。

      【讨论】:

        【解决方案5】:

        您可以使用一种有趣的方式——虽然有点冗长——带有断言。打开断言,会有输出和开销,关闭断言,没有输出,绝对没有开销。

        public static void main(String[] args) {
            assert returnsTrue(new Runnable() {
                @Override
                public void run() {
                    // your logging code
                }
            });
        }
        
        public static boolean returnsTrue(Runnable r) {
            r.run();
            return true;
        }
        

        这里需要 returnTrue() 函数,因为我知道没有更好的方法让表达式返回 true,并且 assert 需要一个布尔值。

        【讨论】:

        • assert 不需要括号,我认为没有括号会更干净。因为这种方式看起来像一个方法调用,其实它不是。