Log4J:创建Logger实例的策略

70

我决定在一个新的Java项目中使用Log4J日志框架。我想知道创建/管理Logger实例应该使用哪种策略,为什么?

  • one instance of Logger per class e.g.

    class Foo {
        private static final Logger log = Logger.getLogger(Foo.class);
    }
    
  • one instance of Logger per thread
  • one instance of Logger per application
  • horizontal slicing : one instance of Logger in each layer of an application (e.g. the view layer, the controller layer and the persistence layer)
  • vertical slicing : one instance of Logger within functional partitions of the application
注意:这个问题在这些文章中已经得到了一定程度的考虑: 创建Log4j Logger的开销是多少

请注意:最终的方法很可能是调查调用堆栈以获取调用者。在我的经验中,如果您明确编写代码而不是依靠神奇地获得它,Java程序更加健壮。 - Thorbjørn Ravn Andersen
JRE中的一个错误会导致日志记录不正确。 - Thorbjørn Ravn Andersen
10个回答

46
通常,您会为每个类设置记录器,因为这是一个很好的逻辑组件。如果您的过滤器显示线程,则线程已成为日志消息的一部分,因此以此方式切片记录器可能是多余的。
关于基于应用程序或层的记录器,问题在于您必须找到一个地方来放置该Logger对象。这并不是一个真正的大问题。更大的问题是,某些类可能在多个级别或多个应用程序中使用...使您的记录器正确可能很困难。或至少棘手。
...您最不想要的就是在日志设置中做出错误的假设。
如果您关心应用程序和层,并且有易于分离的分离点,则NDC是正确的方法。代码有时可能有些冗长,但我不知道有多少次准确的上下文堆栈向我展示了Foo.bar()从应用程序X在Y层中调用。

如果我有一个单例类,其中有一个静态日志记录器实例,并使所有类都访问此单例类的成员怎么样? - Jack

33

最常用的策略是为每个类创建一个日志记录器。如果您创建新线程,请给它们一个有用的名称,以便它们的日志记录容易区分。

按类创建记录器的好处在于可以在类包结构中开启/关闭日志记录:

log4j.logger.org.apache = INFO
log4j.logger.com.example = DEBUG
log4j.logger.com.example.verbose = ERROR
上述代码将把所有 Apache 库代码设置为“INFO”级别,将日志记录从您自己的代码切换到“DEBUG”级别,但不包括详细的包。

1
如果您想通过包结构来控制日志记录级别,那么我认为这样做是有好处的。除了将 private static Logger logger = Logger.getLogger(MyClass.class); 复制到每个类中之外,还有更好的方法可以实现吗? - Marcus Leon
1
考虑到您需要为包和类名提供记录器实例的方式,我认为没有其他选择,只能为每个类创建一个记录器。如果您为每个包创建记录器,仍然需要检索对记录器实例的引用才能使用它。 - rsp
你使用哪个Logger构造函数来为一个包创建一个Logger?是getLogger(String name),其中name只是指定包的字符串(即:“a.b.c”)吗?如果这样做,你能够像log4j.logger.a.b.c = INFO那样配置日志级别吗? - Marcus Leon
2
是的,我认为您会使用String变量,如private static Logger logger = Logger.getLogger(MyClass.class.getPackage()); 如果您创建名称为“a.b”、“a.b.c”等的记录器,则记录器层次结构也适用。请参阅http://logging.apache.org/log4j/1.2/manual.html上的手册。 - rsp

14

我确定这不是最佳实践,但在之前的应用程序中,我曾经放过一些启动时间以节省代码行数。具体来说,当粘贴以下内容时:

Logger logger = Logger.getLogger(MyClass.class);

开发者经常忘记将"MyClass"更改为当前类名,于是几个记录器总是指向错误的位置。这样就很糟糕。

我有时会写:

static Logger logger = LogUtil.getInstance(); 

并且:

class LogUtil {
   public Logger getInstance() {
      String callingClassName = 
         Thread.currentThread().getStackTrace()[2].getClass().getCanonicalName();
      return Logger.getLogger(callingClassName);
   }
}

那段代码中的“2”可能是错误的,但主要思路在于,在类加载时(作为静态变量),承受性能损失以查找类名,以便开发人员没有将其误拼或引入任何错误。

通常情况下,我并不喜欢为防止开发者在运行时出错而牺牲性能,但如果它只会在单例模式下发生一次?这通常对我来说听起来是一个不错的交易。


我更新了你的答案 - 它没有编译,并且我使用了getClass()。getCanonicalName()而不是只使用getClassName()。哦,2确实是魔数。 - ripper234
我已经为您提到的原因编写了一个类似的类。所以现在只需要:static final ClassLogger log = new ClassLogger(); 我已经整合了slf4j,以允许多个日志实现。有关详细信息,请参见www.sormula.org上找到的org.sormula.log.ClassLogger。 - Jeff Miller
1
使用getStackTrace()[2] .getClass().getCanonicalName(),您将始终获得“java.lang.StackTraceElement”。我使用'callingClassName = Thread.currentThread()。getStackTrace()[2] .getClassName()'获得了所需的结果。我投了赞成票,但没有编辑它。谢谢提示。 - bigleftie
我在我的Spring Boot项目中尝试了您的LogUtil。它打印出“[pool-2-thread-6] j.l.StackTraceElement”而不是类名。 - valijon

10

正如其他人所说,我会为每个类创建一个Logger:

private final static Logger LOGGER = Logger.getLogger(Foo.class);

或者

private final Logger logger = Logger.getLogger(this.getClass());

然而,过去我发现在日志记录器中包含其他信息很有用。例如,如果您拥有一个网站,您可以在每个日志消息中包含用户ID。这样,您就可以跟踪用户正在做什么(非常有用于调试问题等)。
最简单的方法是使用MDC,但您也可以为每个类的实例创建一个包含用户ID的名称的Logger来实现此目的。
使用MDC的另一个好处是,如果您使用SL4J,则可以根据MDC中的值更改设置。因此,如果您希望以DEBUG级别记录特定用户的所有活动,并将所有其他用户保留在ERROR级别,您可以这样做。您还可以根据MDC将不同的输出重定向到不同的位置。
一些有用的链接:

http://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/MDC.html

http://www.slf4j.org/api/index.html?org/slf4j/MDC.html


我喜欢 this.getClass() 这部分。这样就不会出现关于记录器名称的复制/粘贴错误。 - winklerrr

4
  • 每个类创建一个日志记录器。
  • 如果您有需要使用Commons Logging的依赖项(很可能),请使用slf4j的bridge来进行Commons Logging。使用Commons Logging接口实例化您的日志记录器(每个类):private static final Log log = LogFactory.getLog(MyClass.class);
  • 使用快捷方式在IDE中体现这种模式。我使用IDEA的live templates来实现此目的。
  • 使用NDC(字符串线程本地堆栈)或MDC(String → ?线程本地映射)为线程提供上下文信息。

模板示例:

private static final Log log = LogFactory.getLog($class$.class); // live template 'log'

if (log.isDebugEnabled())
    log.debug(String.format("$string$", $vars$)); // live template 'ld', 'lw', 'le' ...

3

另一种选择: 你可以尝试使用AspectJ来进行日志记录的横切。在这里查看详细信息:简化你的日志记录**。(如果你不想使用AOP,你可以考虑slf4j

//Without AOP

    Class A{
       methodx(){
        logger.info("INFO");
       }
    }
    
    Class B{
       methody(){
        logger.info("INFO");
       }
    }
    
//With AOP
    
    Class A{
       methodx(){
         ......
       }
    }
    
    Class B{
       methody(){
         ......
       }
    }
    
    Class LoggingInterceptor{

       //Catched defined method before process
       public void before(...xyz){
         logger.info("INFO" + ...xyz);
       }
    
       //Catched defined method after processed          
       public void after(...xyz){
         logger.info("INFO" + ...xyz);
       }
       .....
    
    }

P.S : 面向切面编程(AOP) 更好,这是一种符合DRY(Don't Repeat Yourself)原则的方式。


2
今日免费次数已满, 请开通会员/明日再来
// create logger
Logger customLogger = Logger.getLogger("myCustomLogName");

// create log file, where messages will be sent, 
// you can also use console appender
FileAppender fileAppender = new FileAppender(new PatternLayout(), 
                                             "/home/user/some.log");

// sometimes you can call this if you reuse this logger 
// to avoid useless traces
customLogger.removeAllAppenders();

// tell to logger where to write
customLogger.addAppender(fileAppender);

 // send message (of type :: info, you can use also error, warn, etc)
customLogger.info("Hello! message from custom logger");

现在,如果您需要在同一类中使用另一个记录器,没有问题 :) 只需创建新的记录器

// create logger
Logger otherCustomLogger = Logger.getLogger("myOtherCustomLogName");

现在看一下上面的代码并创建一个新的文件附加器,这样你的输出将被发送到其他文件。

这对于(至少)两种情况非常有用:

  • 当您想要将错误与信息和警告分开时

  • 当您管理多个进程并且需要来自每个进程的输出时

附注:有问题吗?随时问!:)


1
如果您没有共同的代码,那么这种方法是不错的。但是,如果您有两个不同的进程需要使用两个不同的日志文件,并且这两个进程都有一些共同的代码,那么这将是一个问题。 - Abdullah Shaikh

0

通常的约定是“每个类一个记录器,使用类名作为其名称”。这是很好的建议。

我的个人经验是,这个记录器变量不应该被声明为静态变量,而应该是一个实例变量,每次新建都要检索。这允许日志框架根据调用的来源不同而对两个调用进行不同的处理。静态变量对于该类(在该类加载器中)的所有实例都是相同的。

此外,您应该了解您选择的日志后端的所有可能性。您可能有意想不到的可能性。


只是一个提示,对于log4j来说,如果您使用类名作为查找,则每次都将是相同的记录器实例。没有理由为每个类实例再次获取它。您可以将其保持静态。 - PSpeed
1
在使用静态记录器时存在优缺点。它们在托管容器中可能会出现问题,并且不应在旨在在OSGi内运行的代码中使用。请参阅SLF4J的优缺点:http://www.slf4j.org/faq.html#declared_static - SteveD

0

如果您的应用程序遵循SOA原则,对于每个服务A,您将拥有以下组件:

  1. 控制器
  2. 服务实现
  3. 执行器
  4. 持久化

因此,拥有以下日志文件会使生活更轻松: aController.log aService.log aExecutor.log 和 aPersistance.log

这是一种基于层次的分离,因此所有您的Remoting/REST/SOAP类都将写入aController.log

所有调度机制、后端服务等都将写入aService.log

而所有任务执行都将写入aExecutor.log等。

如果您有一个多线程执行器,您可能需要使用日志累加器或另一种技术来正确对齐多个线程的日志消息。

这样,您总是会有4个日志文件,这不算太多也不算太少,我从经验中告诉您,这会让生活变得更加轻松。


0

当部署多个EAR / WAR时,最好将log4j.jar打包到类加载器层次结构的更高层。
即不要放在WAR或EAR中,而是放在容器的系统类加载器中,否则多个Log4J实例将同时写入同一文件,导致奇怪的行为。


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