如何使用Java8的lambda表达式改进日志记录机制

11

如何在没有字符串拼接的情况下改进日志机制呢?

考虑以下示例:

import java.util.logging.Level;
import java.util.logging.Logger;

public class LoggerTest {
    public static void main(String[] args) {
        // get logger
        Logger log = Logger.getLogger(LoggerTest.class.getName());

        // set log level to INFO (so fine will not be logged)
        log.setLevel(Level.INFO);

        // this line won't log anything, but will evaluate the getValue method
        log.fine("Trace value: " + getValue());
    }

    // example method to get a value with a lot of string concatenation
    private static String getValue() {
        String val = "";

        for (int i = 0; i < 1000; i++) {
            val += "foo";
        }

        return val;
    }
}

log.fine(...) 方法不会记录任何内容,因为日志级别已设置为 INFO。问题在于,getValue 方法仍将被评估。

对于具有许多调试语句的大型应用程序,这是一个性能问题。

那么,如何解决这个问题呢?


3
要使用 slf4j 吗? - assylias
谢谢提供信息!但是,如果我们说 log.fine("Trace value: {}", getValue()); 它在任何情况下都会评估 getValue 方法,不是吗? - bobbel
是的确实 - 我以为你只是在连接两个字符串 - 我没有意识到它是1000个字符串! - assylias
1
同时使用slf4j。对于99%的日志记录,可以使用语义“logger.debug(“看着我妈妈:{}”,me)”。但是,如果您知道要连接字符串一千次,则确保将其包装在if(logger.isDebug())中。Lambda很好,但我不确定它是否真正添加了任何有用的东西,并且更有效率。 - The Coordinator
3
为什么不直接使用java.util.logging.Logger.info(Supplier <String>)呢?一劳永逸地处理它,有何不可呢? - Klitos Kyriacou
5个回答

15
自从Java8推出lambda表达式,就可以用于这种情况。下面是修改后的日志示例:

LoggerTest.class

import java.util.logging.Level;
import java.util.logging.Logger;

public class LoggerTest {
    public static void main(String[] args) {
        // get own lambda logger
        LambdaLogger log = new LambdaLogger(LoggerTest.class.getName());

        // set log level to INFO (so fine will not be logged)
        log.setLevel(Level.INFO);

        // this line won't log anything, and will also not evaluate the getValue method!
        log.fine(()-> "Trace value: " + getValue());  // changed to lambda expression
    }

    // example method to get a value with a lot of string concatenation
    private static String getValue() {
        String val = "";

        for (int i = 0; i < 1000; i++) {
            val += "foo";
        }

        return val;
    }
}

LambdaLogger.class

import java.util.concurrent.Callable;
import java.util.logging.Level;
import java.util.logging.Logger;

public class LambdaLogger extends Logger {
    public LambdaLogger(String name) {
        super(name, null);
    }

    public void fine(Callable<String> message) {
        // log only, if it's loggable
        if (isLoggable(Level.FINE)) {
            try {
                // evaluate here the callable method
                super.fine(message.call());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

通过这个修改,如果你有很多仅用于调试目的的日志语句,你可以大大提高应用程序的性能。
当然,你可以使用任何你想要的Logger。这只是一个java.util.Logger的示例。

1
也许。如果您需要将数据从调用方法传递到 Callable.call 中,则可能会产生捕获成本。这可能比字符串连接更好,但仍然比在检查级别是否已启用的 if 语句中包装 fine 调用更昂贵。Goetz 在此处讨论了捕获成本 - McDowell
2
讨论视频中捕获成本的一个好的起点(跳过所有的背景信息)可以从[36:25]开始。 - ngreen
1
不需要扩展Logger类并实现自定义方法,因为Java 8已经具有具有类似签名的记录器方法:public void fine(Supplier<String> msgSupplier) - Genhis

11

@bobbel已经解释了如何做。

我想补充一点,虽然这比您原来的代码表示出了性能的提升,但经典的处理方式仍然更快:

if (log.isLoggable(Level.FINE)) {
    log.fine("Trace value: " + getValue());
}

原因是lambda版本需要额外的运行时开销来创建可调用实例(捕获成本)和一个额外的方法调用级别,因此速度更快。

最后,还有创建LambdaLogger实例的问题。 @bobbel的代码使用构造函数来完成这项工作,但实际上java.util.logging.Logger对象需要由一个工厂方法创建,以避免对象的过多产生。这意味着需要一堆额外的基础设施(和代码更改)来使其与Logger的自定义子类配合工作。

而且只是略微比较啰嗦/冗长。


当然,这也是一个选择(而且可能更快)!但是,想象一下,如果您必须为每个日志语句编写此额外的 if,那么该怎么办。因此,如果您没有很多日志语句,则应优先考虑此选项! - bobbel
2
@bobel - 抱歉,我不喜欢它。我不认为大多数日志调用需要“保护”,或者你的代码通常应该有那么多“细节”日志调用。 - Stephen C
另一个要点是保持if守卫和实际日志语句的级别同步。 - René
@René - 确实如此。但是这两个代码片段通常在连续的行上,而且两个日志级别符号都是大写字母。程序员必须非常粗心才能使它们不同步。 - Stephen C

10

3
SLF4J是否有针对Log4j API的包装器? - Shashank

6
只需创建当前记录器的包装方法,如下所示:
public static void info(Logger logger, Supplier<String> message) {
    if (logger.isLoggable(Level.INFO))
    logger.info(message.get());
}

并使用它:

info(log, () -> "x: " + x + ", y: " + y);

参考资料:JAVA SE 8 for the Really Impatient电子书,第48-49页。


2
在Java 8中,java.util.logging.Logger有一个Logger.info(Supplier<String>)方法,因此为什么要编写包装器,当您可以直接使用此Logger方法呢? - Klitos Kyriacou
因为,@KlitosKyriacou,你不能仅仅通过在类路径中放置一个JAR包来更改后备,例如log4j。 - Travis Spencer

1
使用格式为String和一个Supplier<String>数组。这样只有在记录实际可发布时才会调用toString方法。这样,您就不必在应用程序代码中烦恼丑陋的if语句来记录日志。

看看getValue()方法。如果将该方法作为字符串参数传入,它将在任何情况下都得到求值。所以这不是一个有效的解决方案。 - bobbel
1
没问题,我建议将日志记录调用更改为 log.fine("%s", this::getValue)。这需要你自己编写日志记录器类。 - aepurniet
啊,好的,我误解了你的回答。那太好了!你可以用这个解释来扩展你的答案。现在我在思考Supplier.get()Callable.call()之间的区别... - bobbel
在我看来,当您需要执行一些资源密集型操作以接收结果字符串时,使用供应商会更好。 - witek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接