47

我已经阅读了大量帖子和文档(在本网站和其他地方),指出 SFL4J 日志记录的推荐模式是:

public class MyClass {
    final static Logger logger = LoggerFactory.getLogger(MyClass.class);

    public void myMethod() {
        //do some stuff
        logger.debug("blah blah blah");
    }
}

我的老板更喜欢我们只使用包装器来拦截日志调用,并避免在每个类上声明记录器的样板代码:

public class MyLoggerWrapper {
    public static void debug(Class clazz, String msg){
        LoggerFactory.getLogger(clazz).debug(msg));
    }
}

并像这样简单地使用它:

public class MyClass {

    public void myMethod() {
        //do some stuff
        MyLoggerWrapper.debug(this.getClass(), "blah blah blah");
    }
}

我认为每次我们记录时都实例化一个记录器有点昂贵,但我一直无法找到任何支持该假设的文档。此外,他说框架(我们仍在决定的 LogBack 或 Log4J)肯定会“缓存”记录器,而且在任何情况下,服务器的运行都远远低于其容量,所以这不是问题。

任何帮助指出这种方法的潜在问题?

4

11 回答 11

31

这是这种方法的一个明显问题:字符串消息将在每次调用时构造debug(),没有明显的方法可以在包装器中使用保护子句。

log4j/commons-logging/slf4j 的标准习惯用法是使用保护子句,例如:

if (log.isDebugEnabled()) log.debug("blah blah blah");

目的是如果DEBUG没有为记录器启用级别,编译器可以避免将您可能发送的任何更长的字符串连接在一起:

if (log.isDebugEnabled()) log.debug("the result of method foo is " + bar 
     + ", and the length is " + blah.length());

请参阅“什么是(非)记录的最快方式?” 在 SLF4Jlog4j常见问题解答中。

我建议不要使用您老板建议的“包装”。像 slf4j 或 commons-logging 这样的库已经是实际使用的底层日志实现的外观。此外,记录器的每次调用都会变得更长 - 将上述内容与

 MyLoggerWrapper.debug(Foo.class, "some message");

这是一种琐碎且不重要的“包装”和混淆,除了添加间接层和丑陋的代码之外,没有任何实际用途。我认为你的老板可以找到更重要的问题来着迷。

于 2010-10-13T13:26:33.383 回答
12

记录器对象肯定会被重用,因此无论哪种方式都不会发生额外的实例化。我看到的更大问题是您的文件/行号信息将无用,因为记录器将始终忠实地记录每条消息是从LoggerWrapper第 12 行发出的 class :-(

OTOH SLF4J已经是一个包装外观,用于隐藏所使用的特定日志框架,允许您在不同的日志实现之间自由切换。因此,我认为将其隐藏在另一个包装器后面绝对没有意义。

于 2010-10-13T13:17:36.830 回答
12

重复调用不LoggerFactory.getLogger(clazz)应该每次都产生一个新的 Logger 对象。但这并不意味着电话是免费的。虽然实际行为取决于外观背后的日志系统,但很可能每个 getLogger 都需要在并发或同步数据结构1中进行查找以查找预先存在的实例。

如果您的应用程序对您的方法进行大量调用MyLoggerWrapper.debug,这都会对性能造成重大影响。在多线程应用程序中,它可能是并发瓶颈。

其他答案提到的其他问题也很重要:

  • 当调试被禁用时,您的应用程序不能再用于logger.isDebugEnabled()最小化开销。
  • 该类MyLoggerWrapper隐藏了应用程序调试调用的类名和行号。
  • 如果您进行多个记录器调用,使用的代码MyLoggerWrapper可能会更冗长。详细程度将出现在最影响可读性的区域;即在做需要记录的事情的方法中。

最后,这只是“不是这样做的方式”。


1 - 显然它是HashtableLogback 和 Log4j 中的一个,这意味着肯定存在并发瓶颈的可能性。请注意,这不是对那些日志框架的批评。相反,该getLogger方法并未设计/优化为以这种方式使用。

于 2010-10-13T13:51:21.223 回答
10

除了已经提到的原因,你老板的建议很糟糕,因为:

  • 每次要记录某些内容时,它都会迫使您重复输入与记录无关的内容:this.getClass()
  • 在静态和非静态上下文之间创建一个不统一的接口(因为this在静态上下文中没有)
  • 额外的不必要参数为错误创造了空间,使得同一类中的语句可以转到不同的记录器(想想粗心的复制粘贴)
  • 虽然它保存了 74 个字符的记录器声明,但它为每个记录调用添加了 27 个额外字符。这意味着如果一个类使用记录器超过 2 次,那么就字符数而言,您正在增加样板代码。
于 2010-10-13T14:48:58.263 回答
7

当使用类似的东西MyLoggerWrapper.debug(this.getClass(), "blah") 时:使用 AOP 框架或代码注入工具时,你会得到错误的类名。类名不像起源,而是生成的类名。使用包装器的另一个缺点是:对于每个日志语句,您必须包含额外的代码"MyClass.class"

记录器的“缓存”取决于使用的框架。但即使这样做,它仍然必须为您所做的每条日志语句查找所需的记录器。因此,在一个方法中有 3 个语句,它必须查找 3 次。将其用作static变量时,它只能查找一次!

并且之前说过:你失去了使用if( log.isXXXEnabled() ){}for 一组语句的能力。

您的老板对“社区默认接受和推荐的方式”有何反对?引入包装器并没有增加效率。相反,您必须为每个日志语句使用类名。过了一段时间,你想“改进”它,所以你添加了另一个变量,或者另一个包装器,让你自己更难。

于 2010-10-13T14:57:22.253 回答
6

这是使 Java 8 中的日志记录变得容易的一种可能性 - 定义一个接口来为您完成它。例如:

package logtesting;

import java.util.Arrays;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public interface Loggable { 
    enum LogLevel {
        TRACE, DEBUG, INFO, WARN, ERROR
    }

    LogLevel TRACE = LogLevel.TRACE;
    LogLevel DEBUG = LogLevel.DEBUG;
    LogLevel INFO = LogLevel.INFO;
    LogLevel WARN = LogLevel.WARN;
    LogLevel ERROR = LogLevel.ERROR;

    default void log(Object...args){
        log(DEBUG, args);
    }

    default void log(final LogLevel level, final Object...args){
        Logger logger = LoggerFactory.getLogger(this.getClass());
        switch(level){
        case ERROR:
            if (logger.isErrorEnabled()){
                logger.error(concat(args));
            }
            break;
        case WARN:
            if (logger.isWarnEnabled()){
                logger.warn(concat(args));
            }
            break;          
        case INFO:
            if (logger.isInfoEnabled()){
                logger.info(concat(args));
            }
        case TRACE:
            if (logger.isTraceEnabled()){
                logger.trace(concat(args));
            }
            break;
        default:
            if (logger.isDebugEnabled()){
                logger.debug(concat(args));
            }
            break;
        }
    }

    default String concat(final Object...args){ 
        return Arrays.stream(args).map(o->o.toString()).collect(Collectors.joining());
    }
}

然后,您所要做的就是确保您的类声明实现 Logged,并且从其中任何一个中,您都可以执行以下操作:

log(INFO, "This is the first part ","of my string ","and this ","is the last");

log() 函数负责连接你的字符串,但只有在它测试启用之后。它默认记录到调试,如果你想记录到调试,你可以省略 LogLevel 参数。这是一个非常简单的例子。您可以做很多事情来改进这一点,例如实现单个方法,即 error()、trace()、warn() 等。您也可以简单地将“logger”实现为返回 logger 的函数:

public interface Loggable {
    default Logger logger(){
        return LoggerFactory.getLogger(this.getClass());
    }
}

然后使用您的记录器变得非常简单:

logger().debug("This is my message");

您甚至可以通过为所有 Logger 方法生成委托方法来使其功能齐全,这样每个实现类都是 Logger 的实例。

package logtesting;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.Marker;

public interface Loggable extends Logger {
    default Logger logger(){
        return LoggerFactory.getLogger(this.getClass());
    }

    default String getName() {
        return logger().getName();
    }

    default boolean isTraceEnabled() {
        return logger().isTraceEnabled();
    }

    default void trace(String msg) {
        logger().trace(msg);
    }

    default void trace(String format, Object arg) {
        logger().trace(format, arg);
    }

    default void trace(String format, Object arg1, Object arg2) {
        logger().trace(format, arg1, arg2);
    }

    default void trace(String format, Object... arguments) {
        logger().trace(format, arguments);
    }

    default void trace(String msg, Throwable t) {
        logger().trace(msg, t);
    }

    default boolean isTraceEnabled(Marker marker) {
        return logger().isTraceEnabled(marker);
    }

    default void trace(Marker marker, String msg) {
        logger().trace(marker, msg);
    }

    default void trace(Marker marker, String format, Object arg) {
        logger().trace(marker, format, arg);
    }

    default void trace(Marker marker, String format, Object arg1, Object arg2) {
        logger().trace(marker, format, arg1, arg2);
    }

    default void trace(Marker marker, String format, Object... argArray) {
        logger().trace(marker, format, argArray);
    }

    default void trace(Marker marker, String msg, Throwable t) {
        logger().trace(marker, msg, t);
    }

    default boolean isDebugEnabled() {
        return logger().isDebugEnabled();
    }

    default void debug(String msg) {
        logger().debug(msg);
    }

    default void debug(String format, Object arg) {
        logger().debug(format, arg);
    }

    default void debug(String format, Object arg1, Object arg2) {
        logger().debug(format, arg1, arg2);
    }

    default void debug(String format, Object... arguments) {
        logger().debug(format, arguments);
    }

    default void debug(String msg, Throwable t) {
        logger().debug(msg, t);
    }

    default boolean isDebugEnabled(Marker marker) {
        return logger().isDebugEnabled(marker);
    }

    default void debug(Marker marker, String msg) {
        logger().debug(marker, msg);
    }

    default void debug(Marker marker, String format, Object arg) {
        logger().debug(marker, format, arg);
    }

    default void debug(Marker marker, String format, Object arg1, Object arg2) {
        logger().debug(marker, format, arg1, arg2);
    }

    default void debug(Marker marker, String format, Object... arguments) {
        logger().debug(marker, format, arguments);
    }

    default void debug(Marker marker, String msg, Throwable t) {
        logger().debug(marker, msg, t);
    }

    default boolean isInfoEnabled() {
        return logger().isInfoEnabled();
    }

    default void info(String msg) {
        logger().info(msg);
    }

    default void info(String format, Object arg) {
        logger().info(format, arg);
    }

    default void info(String format, Object arg1, Object arg2) {
        logger().info(format, arg1, arg2);
    }

    default void info(String format, Object... arguments) {
        logger().info(format, arguments);
    }

    default void info(String msg, Throwable t) {
        logger().info(msg, t);
    }

    default boolean isInfoEnabled(Marker marker) {
        return logger().isInfoEnabled(marker);
    }

    default void info(Marker marker, String msg) {
        logger().info(marker, msg);
    }

    default void info(Marker marker, String format, Object arg) {
        logger().info(marker, format, arg);
    }

    default void info(Marker marker, String format, Object arg1, Object arg2) {
        logger().info(marker, format, arg1, arg2);
    }

    default void info(Marker marker, String format, Object... arguments) {
        logger().info(marker, format, arguments);
    }

    default void info(Marker marker, String msg, Throwable t) {
        logger().info(marker, msg, t);
    }

    default boolean isWarnEnabled() {
        return logger().isWarnEnabled();
    }

    default void warn(String msg) {
        logger().warn(msg);
    }

    default void warn(String format, Object arg) {
        logger().warn(format, arg);
    }

    default void warn(String format, Object... arguments) {
        logger().warn(format, arguments);
    }

    default void warn(String format, Object arg1, Object arg2) {
        logger().warn(format, arg1, arg2);
    }

    default void warn(String msg, Throwable t) {
        logger().warn(msg, t);
    }

    default boolean isWarnEnabled(Marker marker) {
        return logger().isWarnEnabled(marker);
    }

    default void warn(Marker marker, String msg) {
        logger().warn(marker, msg);
    }

    default void warn(Marker marker, String format, Object arg) {
        logger().warn(marker, format, arg);
    }

    default void warn(Marker marker, String format, Object arg1, Object arg2) {
        logger().warn(marker, format, arg1, arg2);
    }

    default void warn(Marker marker, String format, Object... arguments) {
        logger().warn(marker, format, arguments);
    }

    default void warn(Marker marker, String msg, Throwable t) {
        logger().warn(marker, msg, t);
    }

    default boolean isErrorEnabled() {
        return logger().isErrorEnabled();
    }

    default void error(String msg) {
        logger().error(msg);
    }

    default void error(String format, Object arg) {
        logger().error(format, arg);
    }

    default void error(String format, Object arg1, Object arg2) {
        logger().error(format, arg1, arg2);
    }

    default void error(String format, Object... arguments) {
        logger().error(format, arguments);
    }

    default void error(String msg, Throwable t) {
        logger().error(msg, t);
    }

    default boolean isErrorEnabled(Marker marker) {
        return logger().isErrorEnabled(marker);
    }

    default void error(Marker marker, String msg) {
        logger().error(marker, msg);
    }

    default void error(Marker marker, String format, Object arg) {
        logger().error(marker, format, arg);
    }

    default void error(Marker marker, String format, Object arg1, Object arg2) {
        logger().error(marker, format, arg1, arg2);
    }

    default void error(Marker marker, String format, Object... arguments) {
        logger().error(marker, format, arguments);
    }

    default void error(Marker marker, String msg, Throwable t) {
        logger().error(marker, msg, t);
    }
}

当然,如前所述,这意味着每次登录时,都必须通过 LoggerFactory 中的 Logger 查找过程——除非你重写 logger() 方法……在这种情况下,你不妨这样做“推荐”的方式。

于 2014-10-28T03:30:50.260 回答
4

我只想说推荐的模式最容易阅读和实现。我认为没有理由偏离它。尤其没有好处。

但是,我的主要观点是关于前面提到的守卫。我不建议明确保护您的日志,因为这已经由 log4j 内部完成,并且是重复工作。

下载 log4j 的源代码并查看 Logger 和 Category 类以亲自了解。

于 2010-10-13T14:29:11.797 回答
4

正如SLF4J 团队在此处所述,您可以使用 JDK 1.7 中引入的 MethodLookup()。

final static Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

这样您就可以引用该类,而无需使用关键字“this”。

于 2017-05-02T23:48:47.980 回答
3

不,除了它弄乱了调用堆栈。这会破坏允许您查看执行日志的代码的方法名称和类的方法。

你可以考虑看看 Jetty web 容器,它包含自己的抽象,它建立在 slf4j 之上。非常好。

于 2010-10-13T13:17:17.017 回答
1

老板的方法没有达到他的想法有两个原因。

较小的原因是添加静态记录器的开销可以忽略不计。毕竟,记录器设置是这个相当长的序列的一部分:

  • 找到类,即遍历所有的.jars 和目录。Java 代码。由于文件系统调用,开销相当大。可以创建辅助对象,例如使用File.
  • 加载字节码,即将其复制到JVM 内部的数据结构中。本机代码。
  • 验证字节码。本机代码。
  • 链接字节码,即遍历字节码中的所有类名并将它们替换为指向所引用类的指针。本机代码。
  • 运行静态初始化程序。从本机代码触发,初始化程序当然是 Java 代码。Logger 在此处创建。
  • 过了一会儿,也许 JIT 编译这个类。本机代码。巨大的开销(无论如何与其他操作相比)。

此外,你的老板什么也没有节省。
第一次调用LoggerFactor.getLogger将创建记录器并将其放置在全局名称到记录器的 HashMap 中。即使isXxxEnabled调用也会发生这种情况,因为要执行这些操作,您首先需要构造 Logger 对象......
Class 对象将为静态变量携带一个额外的指针。这被传递参数的开销所抵消clazz- 字节码中的附加指令和附加指针大小的引用,因此您已经丢失了类大小中的至少一个字节。

代码还经历了额外的间接、LoggerFactory.getLogger(Class)使用Class#getName和委托给LoggerFactory.getLogger(String).

现在,如果您的老板不是在追求性能,而是在能够简单地复制静态声明之后,他可以使用一个检查调用堆栈并检索类名的函数。该函数应该被注释@CallerSensitive,并且它仍然需要在使用新的 JVM 时进行测试——如果你不控制用户运行代码的 JVM,那就不好了。

问题最少的方法是使用 IDE 来检查记录器的实例化。这可能意味着查找或编写插件。

于 2017-11-13T11:24:13.297 回答
0

我可能在较早的评论之一中错过了它,但我没有看到记录器是静态的,对 LoggerFactory 的调用是一次(每个类的实例化) - 所以最初担心多次调用创建记录器是错误的。

关于添加包装类的所有问题的其他评论也非常重要。

于 2015-12-14T16:07:15.587 回答