Java 日志框架解析(下) - 最佳实践

上一篇文章中, 讲了Java常用的日志库以及之间的关系,现在来说说我们在项目中怎么使用日志库。

1. 总是使用Log Facade,而不是具体Log Implementation

正如之前所说的,使用 Log Facade 可以方便的切换具体的日志实现。而且,如果依赖多个项目,使用了不同的Log Facade,还可以方便的通过 Adapter 转接到同一个实现上。如果依赖项目使用了多个不同的日志实现,就麻烦的多了。

具体来说,现在推荐使用 Log4j-API 或者 SLF4j,不推荐继续使用 JCL。

2. 只添加一个 Log Implementation依赖

毫无疑问,项目中应该只使用一个具体的 Log Implementation,建议使用 Logback 或者Log4j2。如果有依赖的项目中,使用的 Log Facade不支持直接使用当前的 Log Implementation,就添加合适的桥接器依赖。具体的桥接关系可以看上一篇文章的图

3. 具体的日志实现依赖应该设置为optional和使用runtime scope

在项目中,Log Implementation的依赖强烈建议设置为runtime scope,并且设置为optional。例如项目中使用了 SLF4J 作为 Log Facade,然后想使用 Log4j2 作为 Implementation,那么使用 maven 添加依赖的时候这样设置:

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>${log4j.version}</version>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-slf4j-impl</artifactId>
    <version>${log4j.version}</version>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>

设为optional,依赖不会传递,这样如果你是个lib项目,然后别的项目使用了你这个lib,不会被引入不想要的Log Implementation 依赖;

Scope设置为runtime,是为了防止开发人员在项目中直接使用Log Implementation中的类,而不适用Log Facade中的类。

4. 如果有必要, 排除依赖的第三方库中的Log Impementation依赖

这是很常见的一个问题,第三方库的开发者未必会把具体的日志实现或者桥接器的依赖设置为optional,然后你的项目继承了这些依赖——具体的日志实现未必是你想使用的,比如他依赖了Log4j,你想使用Logback,这时就很尴尬。另外,如果不同的第三方依赖使用了不同的桥接器和Log实现,也极容易形成环。

这种情况下,推荐的处理方法,是使用exclude来排除所有的这些Log实现和桥接器的依赖,只保留第三方库里面对Log Facade的依赖。

比如阿里的JStorm就没有很好的处理这个问题,依赖jstorm会引入对Logback和log4j-over-slf4j的依赖,如果你想在自己的项目中使用Log4j或其他Log实现的话,就需要加上excludes:

<dependency>
    <groupId>com.alibaba.jstorm</groupId>
    <artifactId>jstorm-core</artifactId>
    <version>2.1.1</version>
    <exclusions>
        <exclusion>
            <groupId>org.slf4j</groupId>
            <artifactId>log4j-over-slf4j</artifactId>
        </exclusion>
        <exclusion>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
        </exclusion>
    </exclusions>
</dependency>

5. 避免为不会输出的log付出代价

Log库都可以灵活的设置输出界别,所以每一条程序中的log,都是有可能不会被输出的。这时候要注意不要额外的付出代价。

先看两个有问题的写法:

logger.debug("start process request, url: " + url);
logger.debug("receive request: {}", toJson(request));

第一条是直接做了字符串拼接,所以即使日志级别高于debug也会做一个字符串连接操作;第二条虽然用了SLF4J/Log4j2 中的懒求值方式来避免不必要的字符串拼接开销,但是toJson()这个函数却是都会被调用并且开销更大。

推荐的写法如下:

logger.debug("start process request, url:{}", url); // SLF4J/LOG4J2
logger.debug("receive request: {}", () -> toJson(request)); // LOG4J2
logger.debug(() -> "receive request: " + toJson(request)); // LOG4J2
if (logger.isDebugEnabled()) { // SLF4J/LOG4J2
    logger.debug("receive request: " + toJson(request)); 
}

6. 日志格式中最好不要使用行号,函数名等字段

原因是,为了获取语句所在的函数名,或者行号,log库的实现都是获取当前的stacktrace,然后分析取出这些信息,而获取stacktrace的代价是很昂贵的。如果有很多的日志输出,就会占用大量的CPU。在没有特殊需要的情况下,建议不要在日志中输出这些这些字段。

最后, log中不要输出稀奇古怪的字符!

部分开发人员为了方便看到自己的log,会在log语句中加上醒目的前缀,比如:

logger.debug("========================start process request=============");

虽然对于自己来说是方便了,但是如果所有人都这样来做的话,那log输出就没法看了!正确的做法是使用grep 来看只自己关心的日志。

编辑于 2018-03-05

文章被以下专栏收录