一般情况下登录Java:最佳实践?


76

有时,当我看到自己的日志记录代码时,我想知道自己是否做对了。可能没有确切的答案,但是我有以下担忧:

图书馆课程

我有几个库类可能记录一些INFO消息。致命错误被报告为例外。当前,我在类中有一个静态记录器实例,其类名称为记录名称。(Log4j的:Logger.getLogger(MyClass.class)

这是正确的方法吗?也许该库类的用户不需要我的实现中的任何消息,或者想要将它们重定向到应用程序特定的日志。我是否应该允许用户从“外部世界”设置记录器?您如何处理这种情况?

常规日志

在某些应用程序中,我的班级可能希望将日志消息写入未由班级名称标识的特定日志。(即:)HTTP Request log做这种事情的最好方法是什么?想到了查找服务...

Answers:


42

您的约定很标准,也很不错(恕我直言)。

要观察的一件事是过多的无用调试调用导致的内存碎片,因此,使用Log4J(和大多数其他Java日志记录框架),最终会得到如下所示:

if (log.isDebugEnabled()) {
  log.debug("...");
}

因为构造该日志消息(您可能没有使用)可能会很昂贵,尤其是经过数千或数百万次的时候。

您的INFO级别日志记录不应太“闲谈”(从您所说的来看,听起来好像不是)。INFO消息通常应该有意义且有意义,例如正在启动和停止应用程序。如果遇到问题,您可能想知道的事情。当您实际要诊断的问题时,调试/精细级别日志记录将更有用。通常仅在需要时才打开调试/精细日志记录。信息通常始终显示。

如果某人不希望从您的类中获得特定的INFO消息,那么他们当然可以自由更改log4j配置以免得到它们。Log4j在该部门非常简单(与Java 1.4日志记录相反)。

至于您的HTTP事情,我通常不会发现Java日志记录是一个问题,因为通常一个类负责您感兴趣的事情,因此您只需要将其放在一个地方即可。在(我的经验很少)中,当您想要看似无关的类之间的通用日志消息时,只需放入一些令牌即可轻松获取。


4
顺便说一句,在Scala中,可以编写一种调试方法,该调试方法使用by-name参数精确地执行该代码而无需冗长。编译器自动将debug()参数中的代码转换为仅在debug()决定调用该函数时才对其求值的函数。因此,除非isDebugEnabled()为true,否则不会连接字符串。示例代码实现此:覆盖DEF调试(MSG:=> AnyRef)=如果(isDebugEnabled)logger.debug(MSG) github.com/dpp/liftweb/blob/...
布莱尔Zajac的

8
这是使用slf4j提供的{} -construct的另一个很好的理由。
托尔比约恩Ravn的安徒生

16

以下是我在所有项目中都遵循的以确保良好性能的一组准则。我是根据互联网上各种来源的输入来形成这套准则的。

与今天一样,我相信Log4j 2是迄今为止登录Java的最佳选择。

基准可在此处获得。为了获得最佳性能,我遵循以下做法:

  1. 由于以下原因,我暂时避免使用SLF4J:
    • 它与标记有一些并发性问题,我想使用这些标记来管理SQL语句的日志记录(标记不如slf4j强大-请参阅Ralph Goers的第一条评论)
    • 它不支持Java 8 Lambda,我想再次使用Java 8 Lambda以获得更好的性能(在Logger中支持lambda表达式
  2. 使用异步记录器执行所有常规记录,以提高性能
  3. 使用同步记录器将错误消息记录在单独的文件中,因为我们希望尽快查看错误消息
  4. 请勿在常规日志记录中使用位置信息,例如文件名,类名,方法名,行号,因为为了导出这些信息,框架会获取堆栈快照并逐步遍历。这会影响性能。因此,仅在错误日志中使用位置信息,而不在常规日志中使用位置信息
  5. 对于跟踪由单独的线程来处理单个请求的目的,可以考虑使用线程上下文和随机UUID作为解释这里
  6. 由于我们将错误记录在一个单独的文件中,因此将上下文信息也记录在错误日志中非常重要。例如,如果应用程序在处理文件时遇到错误,请在错误日志文件中打印文件名和正在处理的文件记录以及stacktrace
  7. 日志文件应该是可重复的并且易于理解。例如,如果应用程序处理多个文件中的客户记录,则每个日志消息应如下所示:
12:01:00,127 INFO FILE_NAME=file1.txt - Processing starts
12:01:00,127 DEBUG FILE_NAME=file1.txt, CUSTOMER_ID=756
12:01:00,129 INFO FILE_NAME=file1.txt - Processing ends
  1. 使用SQL标记记录所有SQL语句,如下所示,并使用过滤器启用或禁用它:
private static final Marker sqlMarker = 
  MarkerManager.getMarker("SQL");

private void method1() {
    logger.debug(sqlMarker, "SELECT * FROM EMPLOYEE");
}
  1. 使用Java 8 Lambdas记录所有参数。当给定的日志级别被禁用时,这将使应用程序免于格式化消息:
int i=5, j=10;
logger.info("Sample output {}, {}", ()->i, ()->j);
  1. 不要使用字符串连接。使用上面显示的参数化消息

  2. 使用动态重载日志记录配置,以便应用程序自动重载日志记录配置中的更改,而无需重新启动应用程序

  3. 请勿使用printStackTrace()System.out.println()

  4. 该应用程序应在退出之前关闭记录器:

LogManager.shutdown();
  1. 最后,供大家参考,我使用以下配置:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration monitorinterval="300" status="info" strict="true">
    <Properties>
        <Property name="filePath">${env:LOG_ROOT}/SAMPLE</Property>
        <Property name="filename">${env:LOG_ROOT}/SAMPLE/sample
        </Property>
        <property name="logSize">10 MB</property>
    </Properties>
    <Appenders>
        <RollingFile name="RollingFileRegular" fileName="${filename}.log"
            filePattern="${filePath}/sample-%d{yyyy-dd-MM}-%i.log">
            <Filters>
                <MarkerFilter marker="SQL" onMatch="DENY"
                    onMismatch="NEUTRAL" />
            </Filters>
            <PatternLayout>
                <Pattern>%d{HH:mm:ss,SSS} %m%n
                </Pattern>
            </PatternLayout>
            <Policies>
                <TimeBasedTriggeringPolicy
                    interval="1" modulate="true" />
                <SizeBasedTriggeringPolicy
                    size="${logSize}" />

            </Policies>
        </RollingFile>
        <RollingFile name="RollingFileError" 
            fileName="${filename}_error.log"
            filePattern="${filePath}/sample_error-%d{yyyy-dd-MM}-%i.log"
            immediateFlush="true">
            <PatternLayout>
                <Pattern>%d{HH:mm:ss,SSS} %p %c{1.}[%L] [%t] %m%n
                </Pattern>
            </PatternLayout>
            <Policies>
                <TimeBasedTriggeringPolicy
                    interval="1" modulate="true" />
                <SizeBasedTriggeringPolicy
                    size="${logSize}" />
            </Policies>
        </RollingFile>
    </Appenders>
    <Loggers>
        <AsyncLogger name="com"
            level="trace">
            <AppenderRef ref="RollingFileRegular"/>
        </AsyncLogger>
        <Root includeLocation="true" level="trace">
            <AppenderRef ref="RollingFileError" level="error" />
        </Root>
    </Loggers>
</Configuration>
  1. 所需的Maven依赖项在这里:
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>2.8.1</version>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.8.1</version>
</dependency>
<dependency>
    <groupId>com.lmax</groupId>
    <artifactId>disruptor</artifactId>
    <version>3.3.6</version>
</dependency>
<!-- (Optional)To be used when working 
with the applications using Log4j 1.x -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-1.2-api</artifactId>
    <version>2.8.1</version>
</dependency>

1
我同意你的大部分观点。特别是应该使用8.中的那些提供程序,并且if (LOGGER.isDebugEnabled()) ...应该完全认为它们已过时。
Aitch

对于CPU敏感的应用程序,请注意第2点,异步日志记录可能会占用大量CPU。
WestFarmer

13

@cletus的回答中,他写道了

if (log.isDebugEnabled()) {
  log.debug("val is " + value);
}

这可以通过使用SLF4J来克服。它提供了格式化帮助

log.debug("val is {}", value);

仅在级别为debug的情况下构造消息的位置。

因此,出于性能和稳定性的原因,建议现在使用SL4J及其配套的记录器Logback 。


2
术语“已评估”可能有点误导,因为Java不支持按名称调用。但是,参数化消息不会“在禁用log语句的情况下增加参数构造的成本”。正如您已经指出的那样,这更加简洁,并且可以带来更好的性能。
Beatngu13年

@ beatngu13:谢谢,现在可以吗?
serv-inc

当然,您仍然有我的支持。;)但是感谢您的澄清。
Beatngu13 '16

2
我有个问题。如果将变量:value更改为方法:getValue()。而且该方法可能会花费大量时间。if (log.isDebugEnabled()) {log.debug("val is " + getValues());}费用是否与log.debug("val is {}", getValue());
user2256235 '16

1
@ user2256235:正如@user所提到的,两个语句都将立即求值。另外,如果您使用Java 8和适当的日志记录框架,则可以传递Supplier。查看此博客文章,以查看Log4j的一些示例。
Beatngu13

7

关于实例化记录器,使用Eclipse Java模板设置记录器已经取得了一些成功:

private static Logger log = Logger.getLogger(${enclosing_type}.class);

这避免了JVM与您的堆栈跟踪混为一谈的问题,并且减少了(也许是)首先创建堆栈跟踪的开销。

使用这样的模板的好处是,如果您想为记录器全面设置一致的标准,则可以与您的团队共享。

看起来IntelliJ支持代表封装类型名称的模板变量的相同概念。我没有找到在NetBeans中轻松实现此目标的方法。


5

我正在查看应用程序的日志级别,目前正在检测一种模式:

private static final Logger logger = Logger.getLogger(Things.class)

public void bla() {
  logger.debug("Starting " + ...)
  // Do stuff
  ...
  logger.debug("Situational")

  // Algorithms
  for(Thing t : things) {
    logger.trace(...)
  }

  // Breaking happy things
  if(things.isEmpty){
    logger.warn("Things shouldn't be empty!")
  }

  // Catching things
  try {
    ...
  } catch(Exception e) {
    logger.error("Something bad happened")
  }

  logger.info("Completed "+...)
}

一个log4j2文件定义一个套接字附加程序,以及一个故障转移文件附加程序。和一个控制台追加程序。有时我会在需要时使用log4j2标记。

认为额外的观点可能会有所帮助。


4

您要描述的log4j配置类型的首选选项是使用log4j配置文件。这使实现的用户可以完全按照您的要求进行操作,因为他们以后可以使用更适合自己的实现的方式覆盖您的配置。请参阅此处以获取非常详尽的入门。


...还有一种非XML格式,可读性更高。
2009年

4

我可能是从某个地方偷来的,但是很好。

它减少了复制和粘贴重构时混淆记录器的风险,而且键入起来更少。

在您的代码中:

private final static Logger logger = LoggerFactory.make();

...在LoggerFactory中:

public static Logger make() {
    Throwable t = new Throwable();
    StackTraceElement directCaller = t.getStackTrace()[1];
    return Logger.getLogger(directCaller.getClassName());
}

(请注意,stackdump是在初始化期间完成的。JVM可能不会优化stacktrace ,但实际上没有保证)


* ...最有可能是从海因茨·卡布兹博士那里偷走的。但我不确定
KarlP

10
请记住,VM可以自由地忽略堆栈帧或执行其他优化,因此最终可能会得到一个空数组StackTraceElements。有关更多信息,请参见java.sun.com/j2se/1.5.0/docs/api/java/lang/…
孟买

1
您错过了类变量定义中的类型。
塞巴斯蒂安

3

另外,我认为重要的是要表示Java简单日志记录外观(SLF4J)(http://www.slf4j.org/)非常重要。由于在大型项目的各个部分中使用不同的日志记录框架存在一些问题,SLF4J是解决成功管理这些部分的问题的事实标准,不是吗?

第二个概念:它似乎有些老派的任务可以被1-3面向方面编程,春frmwrk有它自己的实现,AOP的方式进行记录认为这里在StackOverflow的和这里的春天博客。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.