看 Log4j2 频繁爆雷给出几点日志使用建议

2021-12-27 14:52:46 浏览数 (1)

日志体系概述 日志接⼝ JCL:Apache 基金会所属的项⽬,是一套 Java 日志接⼝,之前叫 Jakarta Commons Logging,后更名为 Commons Logging,简称 JCL。 SLF4J:英文全称 Simple Logging Facade for Java,缩写 Slf4j,是⼀套简易 Java ⽇志⻔面,只提供相关接⼝,和其他日志工具之间需要桥接。 日志实现 JUL:JDK 中的⽇志工具,也称为 jdklog、jdk-logging,⾃ Java1.4 以来 Sun 的官方提供。 Log4j:⾪属于 Apache 基金会的一套日志框架,现已不再维护。 Log4j2:Log4j 的升级版本,与 Log4j 变化很大,且不兼容。 Logback:⼀个具体的⽇志实现框架,和 Slf4j 是同一个作者,性能很好。

1. ⻔面约束
  • 使⽤⻔面,⽽不是具体实现

使用 Log Facade 可以⽅方便便的切换具体的日志实现。⽽且,如果依赖多个项目,使⽤了不同的 Log Facade,还可以⽅方便便的通过 Adapter 转接到同一个实现上。如果依赖项目直接使用了多个不同的日志实现,会非常糟糕。

经验之谈:日志⻔面,⼀般现在推荐使用 Log4j-API 或者 SLF4J,不推荐继续使用 JCL。

  • 单⼀原则,只添加一个日志实现

项⽬中应该只使用一个具体的 Log Implementation,如果在依赖的项⽬中,使⽤的 Log Facade 不支持当前 Log Implementation,就添加合适的桥接器。

经验之谈:jul 性能一般,log4j 性能也有问题而且不再维护,建议使用 Logback 或者 Log4j2。

2. 依赖约束
  • 日志实现坐标应该设置 optional 并使⽤ runtime scope

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

代码语言:javascript复制
<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 为 true,依赖不会传递,这样如果你是个 lib 项⽬,然后别的项目使⽤了你这个 lib,不会被引入不想要的 Log Implementation 依赖;

scope 设置为 runtime,是为了防⽌开发⼈员在项⽬中直接使⽤ Log Implementation 中的类,强制约束开发人员使⽤ Facade 接口。

3. 避免传递
  • 尽量用 exclusion 排除依赖的第三⽅库中的⽇志坐标

依赖约束 所说,第三⽅库的开发者却未必会把具体的⽇志实现或者桥接器的依赖设置为 optional, 然后你的项目就会被迫传递引入这些依赖,⽽这些日志实现未必是你想要的,比如他依赖了 Log4j,你想使用 Logback,这时就很尴尬。另外,如果不同的第三方依赖使⽤了不同的桥接器和 Log 实现,极有可能会形成环。

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

示例:如依赖 jstorm 会引入 Logback 和 log4j-over-slf4j,如果在你⾃己的项⽬中使用 Log4j 或其他 Log 实现的话,就需要加上exclusion:

代码语言:javascript复制
<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>
4. 注意写法
  • 避免为不会输出的 log 买单

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

代码语言:javascript复制
logger.debug("this is debug: "   message);
logger.debug("this is json msg: {}", toJson(message));

第一条语句的字符串拼接,即使日志级别⾼于 debug 不会打印,依然会做字符串连接操作;

第二条语句虽然⽤了 SLF4J/Log4j2 中的占位符输出(懒求值方式),但是 toJson() 这个函数却是总会被调用并且开销更大。推荐的写法如下:

代码语言:javascript复制
// SLF4J/LOG4J2
logger.debug("this is debug:{}", message);
// LOG4J2
logger.debug("this is json msg: {}", () -> toJson(message));
// SLF4J/LOG4J2
if (logger.isDebugEnabled()) {
    logger.debug("this is debug: "   message);
}
5. 减少分析
  • 输出的日志中尽量不要使⽤行号、函数名等信息

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

6. 精简⾄上
  • log 中尽量不要输出稀奇古怪的字符,这是个习惯和约束问题

有人习惯写如下这样的语句:

代码语言:javascript复制
logger.debug("===============================>:{}",message);

输出了⼤量⽆关字符,虽然⾃己⼀时痛快一直爽,但是如果所有⼈都这样写的话,那 log 输出就没法看了!正确的写法是日志只输出必要信息,如果要过滤,后期可以使用 grep 命令来筛选,只查看自己关心的日志即可。

0 人点赞