第三章:日志框架:Log4j、Logback、SLF4J 原理与配置,以及我在项目中踩过的坑

日志框架这东西,说白了就是给系统装个「黑匣子」。

没有它,线上出了问题你只能干瞪眼。我见过太多团队,上线前不重视日志,出故障时连个排查线索都没有。嗯,今天我们就来聊聊 Java 生态里最常用的三个日志框架:Log4j、Logback 和 SLF4J。

3.1 三个框架的关系:别搞混了

很多新手容易把 SLF4J 和 Log4j 当成一回事。其实它们分工完全不同。

  • SLF4J:一套日志门面(接口)。它不干活,只定义规范。
  • Log4j / Logback:具体的日志实现。它们才是真正写日志的。

打个比方:SLF4J 就像餐厅的菜单,Log4j 和 Logback 就是后厨的厨师。你点菜(调用 SLF4J API),厨师(Logback)负责做菜(输出日志)。

核心原则:代码里只依赖 SLF4J,不要直接引用 Log4j 或 Logback 的 API。这样以后换日志实现,一行代码都不用改。

3.2 Log4j:老牌框架,但坑不少

Log4j 1.x 已经停止维护了。如果你还在用,我建议你尽快升级。

我在项目中遇到过最典型的问题:Log4j 1.x 的配置文件是 log4j.properties,格式比较古老。而且它不支持占位符 {},只能用字符串拼接。你想想看,每次打日志都拼字符串,性能能好吗?

// Log4j 1.x 的写法(不推荐)
logger.debug("User " + userId + " login failed, reason: " + reason);

// SLF4J 的写法(推荐)
logger.debug("User {} login failed, reason: {}", userId, reason);

Log4j 2.x 虽然改进了很多,但配置文件和 Logback 不通用。我个人习惯直接上 Logback,省心。

注意:Log4j 1.x 有严重的内存泄漏问题。我曾经在生产环境上遇到过,日志量一大,GC 频繁,最后 OOM 了。排查了两天才找到原因——Log4j 的 AsyncAppender 在队列满时会阻塞,导致线程卡死。

3.3 Logback:Spring Boot 的默认选择

Logback 是 Log4j 作者写的升级版。性能更好,功能更强。

Spring Boot 默认就用 Logback,所以大部分项目其实都在用。它的配置文件是 logback-spring.xmllogback.xml

这里有个常见的坑:日志滚动策略

我曾经接手过一个项目,日志文件越来越大,最后把磁盘撑爆了。原因就是没配滚动策略。Logback 默认是不滚动的,你得手动配。

<!-- logback-spring.xml 示例 -->
<configuration>
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>/var/log/myapp/app.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 每天滚动,保留30天 -->
            <fileNamePattern>/var/log/myapp/app.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="FILE"/>
    </root>
</configuration>

小技巧maxHistory 一定要配。我一般设 30 天,既保证能回溯问题,又不会占太多磁盘。另外,totalSizeCap 可以限制总大小,防止日志无限增长。

3.4 SLF4J:门面模式的典范

SLF4J 的设计思想很简单:你只管调用它的 API,具体用哪个实现,由运行时决定。

它的核心是 LoggerFactory.getLogger() 方法。你传一个类进去,它返回一个 Logger 实例。这个实例背后可能是 Logback,也可能是 Log4j,甚至可能是 JDK 自带的 java.util.logging

我在项目中遇到过一个问题:SLF4J 绑定冲突

当时项目依赖了多个第三方库,有的用 Log4j,有的用 Logback。结果启动时 SLF4J 报了一堆警告,说找到了多个绑定。虽然能运行,但日志输出混乱,有的日志丢了,有的重复了。

解决办法:排除多余的依赖。

<!-- Maven 中排除 Log4j,只保留 Logback -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </exclusion>
    </exclusions>
</dependency>

警告:千万不要在 classpath 里同时放 log4j-over-slf4j.jarslf4j-log4j12.jar。这会导致死循环,日志直接打爆 CPU。我曾经踩过这个坑,线上服务直接挂了。

3.5 避坑指南:我踩过的那些坑

做日志框架配置这么多年,我总结了几条血泪教训:

  1. 异步日志一定要配:同步日志在高并发下会拖慢业务线程。用 AsyncAppender 或 Logback 的 AsyncLogger,把日志写入操作放到后台线程。
  2. 日志级别别乱改:生产环境用 INFO 或 WARN,别开 DEBUG。我曾经见过有人把线上日志开到 DEBUG,结果一天产生了几百 GB 日志,磁盘直接爆了。
  3. 敏感信息别打日志:密码、身份证号、银行卡号,这些绝对不能打。我见过有同事把用户密码明文打出来,被安全部门通报了。
  4. 日志格式要统一:时间戳、线程名、日志级别、类名、消息,这些字段一个都不能少。方便后续用 ELK 或 Splunk 做分析。

我的建议:团队内部统一一个日志规范。比如所有异常日志必须打印堆栈,所有业务日志必须包含 traceId。这样排查问题时,你才能快速定位到是哪次请求出了问题。

3.6 总结

日志框架看似简单,但用不好就是灾难。SLF4J 是门面,Logback 是实现,两者配合使用最稳妥。Log4j 1.x 能不用就别用了,升级到 Logback 或 Log4j 2.x 吧。

嗯,最后说一句:日志不是写给别人看的,是写给未来的自己看的。你希望未来的自己看到什么样的日志?