1. 为什么需要日志框架?

在开发 Java 程序,尤其是企业级生产应用时,System.out.println 看似是最简单粗暴的调试方式——无需引入额外依赖,也不用配置,直接打印就完事了。

但这种做法在实际项目中会踩不少坑。虽然它适合写 Demo 或临时调试,但在正式项目中使用 System.out.println 会带来一系列问题。本文将深入探讨 **为什么以及何时应该用日志框架(如 Log4J2)替代 System.outSystem.err**,并结合实战示例说明其优势。

如果你还在生产代码里狂打 System.out,是时候升级你的姿势了。

2. 环境准备

要使用 Log4J2,我们需要添加必要的依赖并进行基础配置。

2.1. Maven 依赖

pom.xml 中引入 Log4J2 的核心依赖:

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>2.19.0</version> 
</dependency> 
<dependency> 
    <groupId>org.apache.logging.log4j</groupId> 
    <artifactId>log4j-core</artifactId> 
    <version>2.19.0</version> 
</dependency>

✅ 推荐始终使用最新稳定版本,可在 Maven Central 查看。

2.2. Log4J2 配置文件

System.out 不需要任何配置,但日志框架通常需要一个外部配置文件来控制行为。以下是 log4j2.xml 的基础配置:

<Configuration status="debug" name="baeldung" packages="">
    <Appenders>
        <Console name="stdout" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %p %m%n"/>
        </Console>
    </Appenders>
    <Root level="error">
        <AppenderRef ref="stdout"/>
    </Root>
</Configuration>

⚠️ 注意:status="debug" 可以输出 Log4J2 自身的内部日志,便于排查配置问题,上线前建议关闭。

几乎所有主流日志框架(Logback、Log4J2 等)都支持通过 XML、JSON 或代码方式配置,灵活性远超 System.out

3. 日志输出分离能力对比

3.1. System.out 与 System.err 的局限

当你把应用部署到 Tomcat 这类容器时,System.out.println 输出会被重定向到 catalina.out。多个应用或组件的日志混在一起,排查问题时简直是灾难。

更糟的是,你无法控制输出内容的类型。唯一“区分”方式是:

System.out.println("这是普通信息");     // INFO 类似
System.err.println("这是错误信息");     // ERROR 类似

❌ 这种做法既不规范也不可维护,且 System.err 默认没有颜色高亮,肉眼难分辨。

3.2. Log4J2 的日志级别控制

Log4J2 提供了标准的日志级别,支持精细化控制输出:

  • FATAL
  • ERROR
  • WARN
  • INFO
  • DEBUG
  • TRACE
  • ALL

通过配置,你可以做到:

✅ 开发环境输出 DEBUG 级别,全面观察流程
✅ 生产环境只输出 WARN 及以上,减少 I/O 压力和性能损耗
✅ 按包名单独设置级别,比如对第三方库只记录 ERROR

示例代码:

logger.trace("追踪日志,用于深度调试");
logger.debug("调试信息,开发时启用");
logger.info("普通业务日志");
logger.warn("警告,可能存在问题");
logger.error("错误,已发生异常");
logger.fatal("致命错误,应用可能崩溃");

你可以通过配置文件动态调整级别,无需改代码,热生效。

4. 写入文件的能力对比

4.1. System.out 重定向的缺陷

虽然可以通过 System.setOut() 把输出重定向到文件:

PrintStream outStream = new PrintStream(new File("outFile.txt"));
System.setOut(outStream);
System.out.println("这是一篇 Baeldung 文章");

System.err 同理:

PrintStream errStream = new PrintStream(new File("errFile.txt"));
System.setErr(errStream);
System.err.println("这是一篇 Baeldung 文章错误");

⚠️ 但这种方式存在严重问题:

  • ❌ 无法控制文件大小,日志无限增长
  • ❌ 没有归档机制,老日志无法压缩或删除
  • ❌ 多线程环境下可能丢失日志或乱序

4.2. Log4J2 的文件滚动策略

Log4J2 支持强大的日志滚动(Rolling)机制,能自动管理日志文件生命周期。

按时间滚动(每日一个文件)

<File name="fout" 
      fileName="log4j/target/baeldung-log4j2.log"
      immediateFlush="false" 
      append="false">
    <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %p %m%n"/>
</File>

按大小滚动 + 压缩归档

当文件达到 5KB 时自动切分,并用 gzip 压缩旧文件:

<RollingFile name="roll-by-size"
             fileName="target/log4j2/roll-by-size/app.log" 
             filePattern="target/log4j2/roll-by-size/app.%i.log.gz"
             ignoreExceptions="false">
    <PatternLayout>
        <Pattern>%d{yyyy-MM-dd HH:mm:ss} %p %m%n</Pattern>
    </PatternLayout>
    <Policies>
        <OnStartupTriggeringPolicy/>
        <SizeBasedTriggeringPolicy size="5 KB"/>
    </Policies>
</RollingFile>

✅ 实际项目中推荐组合使用时间 + 大小策略,避免磁盘被撑爆。

5. 输出到外部系统

日志框架的强大之处在于 Appender 机制——你可以把日志输出到任意目标。

除了控制台和文件,Log4J2 支持将日志发送到:

  • Kafka(用于日志收集)
  • Elasticsearch(用于集中查询和分析)
  • SMTP(错误日志邮件告警)
  • 数据库、Socket、AWS CloudWatch 等

例如,使用 KafkaAppender 可以轻松将日志推送到消息队列:

<Kafka name="KafkaAppender" topic="app-logs">
    <Property name="bootstrap.servers">kafka-server:9092</Property>
    <PatternLayout pattern="%d %p %c{1.} %m%n"/>
</Kafka>

System.out.println 完全不具备这种扩展能力。

6. 自定义日志格式

日志框架允许你灵活定义输出格式,包含:

  • 时间戳 ✅
  • 日志级别 ✅
  • 类名、方法名 ✅
  • 行号 ✅
  • 线程名 ✅
  • 彩色高亮 ✅

这些信息对排查问题至关重要。而用 System.out.println 实现这些,得手动拼接,代码冗余且易出错。

Log4J2 只需在配置中定义 Pattern:

<Console name="ConsoleAppender" target="SYSTEM_OUT">
    <PatternLayout pattern="%style{%date{DEFAULT}}{yellow}
      %highlight{%-5level}{FATAL=bg_red, ERROR=red, WARN=yellow, INFO=green} %message"/>
</Console>

效果如下:

2025-04-05 10:20:30 ERROR [main] UserService - 用户登录失败,用户名不存在

✅ 开发环境建议开启行号和方法名(%line %method),定位问题快人一步。

7. 异常日志处理:别再用 printStackTrace()

你是否经常看到这样的代码?

try {
    // 业务逻辑
} catch (Exception e) {
    e.printStackTrace();
}

⚠️ 这种写法有三大问题:

  1. 输出到 System.err,无法被日志框架统一管理
  2. 无法控制输出级别(ERROR 还是 WARN?)
  3. 无法添加上下文信息,比如“用户ID=123 登录失败”

正确做法是交给日志框架:

try {
    // 业务逻辑
} catch (Exception e) {
    logger.error("用户登录失败,userId={}", userId, e);
}

✅ 这样做的好处:

  • 日志级别明确(error)
  • 包含上下文参数(userId)
  • 异常堆栈完整输出
  • 可被文件、Kafka、ELK 等系统捕获

8. 总结

对比项 System.out.println 日志框架(如 Log4J2)
日志级别控制 ❌ 无 ✅ 支持 TRACE 到 FATAL
输出目标 ❌ 仅控制台/文件 ✅ 控制台、文件、Kafka、ES 等
文件滚动 ❌ 不支持 ✅ 支持按时间/大小滚动
格式定制 ❌ 手动拼接 ✅ PatternLayout 灵活配置
异常处理 ❌ printStackTrace() ✅ 结构化输出 + 上下文
性能 ✅ 轻量 ✅ 异步日志(AsyncAppender)接近零开销

✅ 结论:

  • 小 Demo 或临时调试可用 System.out
  • 所有生产级 Java 项目必须使用日志框架
  • 推荐使用 Log4J2 + SLF4J 组合,兼顾性能与灵活性

示例代码已上传至 GitHub:https://github.com/baeldung/tutorials/tree/master/logging-modules/log4j2


原始标题:System.out.println vs Loggers