如何分解 Java 8 和 SLF4J 中的条件日志检查?

How to factor out conditional log checks in Java 8 & SLF4J?

我正在尝试分解 if (log.isInfoEnabled())if (log.isWarnEnabled()) 语句,所以我想在 Java 8 中创建一个接口,如下所示但是我不确定我是否可以 运行 遇到任何问题?

public interface Logging {

    Logger log_ = LoggerFactory.getLogger(Thread.currentThread().getContextClassLoader().getClass());

    default void logInfo(String... messages) {
        if (log_.isInfoEnabled()) {
            String meg = String.join("log message: ", messages)
            log_.info(msg);
        }
    }
}


public class Hello implements Logging {

    //Imagine this function is called lot of times like imagine
    // an infinite loop calling this function
    public void doSomething(String name) {
        if (name.equals("hello")) {
            logInfo("What class name does the log print here ? ")  
        }
    }

     //Imagine this function is called lot of times like imagine
    // an infinite loop calling this function
   public void doSomething2(String name) {
        if (name.equals("hello2")) {
            logInfo("What class name does the log print here ? ")  
        }
    }

    //Imagine this function is called lot of times like imagine
    // an infinite loop calling this function
    public void doSomething3(String name) {
        if (name.equals("hello3")) {
            logInfo("What class name does the log print here ? ")  
        }
    }
}

public class Hello {

    Logger log_ = LoggerFactory.getLogger(Hello.class);

    public void doSomething(String name) {
        if (name.equals("hello")) {
            if (log_.isInfoEnabled()) { // Don't want to have this code everywhere
                logInfo("What class name does the log print here ? ") 
            }
        }
    }
}

这两个是等价的吗?使用上面的Logging界面有什么问题吗?

我认为您的方法即使有效,也是毫无意义的。日志方法已经完成了这项工作。

我们使用日志保护条件的原因是关于字符串连接。

这是问题所在:

log.debug("This is the case where " + varToBeChecked);

在这种情况下你必须使用保护条件:

if (log.isDebugEnabled()) {
    log.debug("This is the case where " + varToBeChecked);
}  

这不仅避免了日志语句的执行,还避免了将在前面的示例中完成的字符串连接。

所以如果你只有一个没有连接的固定字符串,你就没有理由使用保护条件:

log.info("Method starts here");

在您的提案中,界面并未保护您免受此问题的影响,因此您将增加复杂性而不解决问题。

这个日志人员的解决方案可能是更改日志界面:

log.debug("My log message for %s of run", myVarToBeLogged);

在这种情况下,我们可以避免字符串连接,但我不确定使用保护条件是否具有相同的性能。

日志记录方法仅在启用特定日志记录级别时输出要记录的值。手动检查日志记录级别唯一有意义的情况是创建要记录的消息很昂贵。比方说,您必须从数据库中检索延迟加载的字段或遍历包含许多项的集合。在这种情况下,创建一个接口来模拟混合并使用函数式编程是有意义的,如:

public interface Logging {

    Logger getLogger();

    default void logInfo(Supplier<String> msg){
        Logger logger = getLogger();
        if (logger.isInfoEnabled()){
            logger.info(msg.get());
        }
    }

}

public class Something implements Logging {

    public static Logger LOG = LoggerFactory.getLogger(Something.class);

    @Override
    public Logger getLogger() {
        return LOG;
    }

    public void doSomething(){
        logInfo(this::createExpensiveMessage); 
    }

    private String createExpensiveMessage(){
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // Nobody cares 
        }

        return "Something happened";
   }

}

I am trying to factor out if (log.isInfoEnabled()) or if (log.isWarnEnabled()) statements

我认为您不理解 if (log.isLevelXXXEnabled()){...} 模式的使用方式和原因。

你在这里写的是什么:

public void doSomething(String name) {
    if (name.equals("hello")) {
        if (log_.isInfoEnabled()) { // Don't want to have this code everywhere
            logInfo("What class name does the log print here ? ") 
        }
    }
}

真的没有必要用:

括起来
if (log_.isInfoEnabled()){

为什么?
因为您将 String 传递给不需要任何计算的日志:"What class name does the log print here ?" 到您的记录器。
此外,记录器已经设计为仅当记录器的实际级别与请求的日志级别匹配时才写入日志。

现在,假设您使用昂贵的 toString() 方法传递了一个对象。
记录器确实调用传递对象的 toString() 方法来记录 当它实际执行 log.
但是在这种情况下,在记录之前使用检查仍然没有用。

假设 myObject 是一个引用 class 的实例的变量,其中 toString() 执行多个计算或只是一个对象集合。 这是无奈:

if (log_.isInfoEnabled()) { 
    logInfo(myObject); 
}

因为只有当记录器的有效日志级别与请求的记录级别匹配时,记录器实现才会执行 myObject.toString()

仅当您必须对传递的对象执行相对昂贵的计算时,使用此检查日志模式才有意义。
通常,当您自己执行计算并将其作为参数提供给日志操作时。
例如:

if (log_.isInfoEnabled()) { 
    log_.info("name :" + myPerson.getName() + ", order history :" + myPerson.getOrders()); 
}

在这里它有意义不是因为你节省了 4 个连接,而是因为 myPerson.getOrders() 是一个可能有数百个元素的集合,只有当你真的登录时才应该对它调用 toString()

现在您认为如何设计一个可以为您执行此特定检查和任何其他特定检查的通用方法?
不可能。
事实上,您尝试做的事情是可能的,但只会重复记录器库已经为您做的事情。
您必须针对每个特定情况执行检查,因为它很昂贵,因此您应该进行检查的特定情况。

现在根据喜好,你当然可以换成这个:

if (log_.isInfoEnabled()) { 
    log_.info("name :" + myPerson.getName() + ", order history :" + myPerson.getOrders()); 
}

通过其他语法:

log_.info("name : {}, order history : {}", myPerson.getName(), myPerson.getOrders()); 

依赖于

public void info(String format, Object... arguments);

org.slf4j.Loggerclass.

的方法

使用 if (log_.isInfoEnabled()) {public void info(String format, Object... arguments); 并没有真正改变问题,因为您在使用这种方法来节省潜在计算时应该始终考虑。

守卫(应该)主要用于在不需要时防止代价高昂的操作 - 仅用于调试。

所以下面是正确的模式:

    if (log_.isInfoEnabled()) {
        String meg = String.join("log message: ", f(), g(), h);
        log_.info(msg);
    }

default void logInfo(Supplier<String> messageSupplier) {
    if (log_.isInfoEnabled()) {
        log_.info(messageSupplier.get());
    }
}

logInfo(() -> String.join("log message: ", f(), g(), h));

但是,您冒着将此方法用于简单日志记录情况的风险,将其转化为代价高昂的额外函数调用 logInfo,以及 lambda。

如果您使用参数化消息并且参数是对对象或原始值的直接引用,则不需要守卫。

https://www.slf4j.org/faq.html#logging_performance

这是一条参数化消息:log.debug("Person is {}", person);

如果日志参数使用某种处理来获取我们想要记录的对象,仍然需要守卫。 slf4j 延迟的只是对对象的 toString() 方法的调用,而不是为获取对象本身而执行的计算。这个守卫放在这里是合理的:

if (log.isdebugEnabled()) {
 log.debug("Value {} is not valid", obj.connectToDBAndRetrieveValue());
}

因此,使用自定义日志包装器来封装保护逻辑可能会很方便。而且,它将这种情况的单元测试从客户端代码转移到具体包装器 class 这样做会更容易和更清洁。

附带说明一下,实际上 INFO 级别不需要这种包装器,因为 INFO 通常在生产中启用。除非这不是你的情况。