Java 完全指南 / 25 - 日志:SLF4J、Logback、Log4j2、结构化日志
25 - 日志:SLF4J、Logback、Log4j2、结构化日志
日志框架体系
┌─────────────────────────────────────┐
│ 应用代码 │
│ 使用 SLF4J / Log4j2 API │
├──────────────┬──────────────────────┤
│ SLF4J 门面 │ Log4j2 API │
├──────────────┴──────────────────────┤
│ 日志实现 │
│ ┌─────────┬──────────┬──────────┐ │
│ │ Logback │ Log4j2 │ JUL │ │
│ └─────────┴──────────┴──────────┘ │
└─────────────────────────────────────┘
| 组件 | 角色 | 说明 |
|---|
| SLF4J | 门面(Facade) | 统一 API,不实现日志 |
| Logback | 实现 | Spring Boot 默认,性能好 |
| Log4j2 | 实现 | Apache 出品,功能丰富 |
| JUL | 实现 | JDK 内置,不推荐 |
SLF4J 使用
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class OrderService {
private static final Logger log = LoggerFactory.getLogger(OrderService.class);
// 或使用 Lombok
// @Slf4j
// public class OrderService {
public Order createOrder(String userId, List<Item> items) {
log.info("创建订单: userId={}, 商品数={}", userId, items.size());
try {
Order order = doCreateOrder(userId, items);
log.info("订单创建成功: orderId={}, 金额={}", order.getId(), order.getTotal());
return order;
} catch (InsufficientStockException e) {
log.warn("库存不足: {}", e.getMessage());
throw e;
} catch (Exception e) {
log.error("创建订单失败: userId={}", userId, e); // e 作为最后参数会打印堆栈
throw e;
}
}
}
日志级别
| 级别 | 用途 | 示例 |
|---|
TRACE | 最细粒度追踪 | 方法进入退出 |
DEBUG | 调试信息 | SQL 语句、参数 |
INFO | 关键业务事件 | 订单创建、用户登录 |
WARN | 警告 | 库存不足、配置缺失 |
ERROR | 错误 | 异常、服务不可用 |
💡 使用占位符 {} 而非字符串拼接:log.info("name={}", name) 优于 log.info("name=" + name)。
Logback 配置
<!-- src/main/resources/logback-spring.xml -->
<configuration>
<property name="LOG_PATH" value="logs"/>
<property name="APP_NAME" value="myapp"/>
<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 文件输出(滚动) -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${APP_NAME}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/${APP_NAME}.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- JSON 格式(结构化日志) -->
<appender name="JSON" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${APP_NAME}-json.log</file>
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<customFields>{"app":"${APP_NAME}","env":"${ENV:-dev}"}</customFields>
</encoder>
</appender>
<!-- 环境差异化配置 -->
<springProfile name="dev">
<root level="DEBUG">
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="FILE"/>
<appender-ref ref="JSON"/>
</root>
</springProfile>
<!-- 包级别控制 -->
<logger name="org.hibernate.SQL" level="DEBUG"/>
<logger name="com.zaxxer.hikari" level="WARN"/>
<logger name="org.springframework.web" level="INFO"/>
</configuration>
Log4j2 配置
<!-- src/main/resources/log4j2-spring.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Properties>
<Property name="LOG_PATH">logs</Property>
<Property name="APP_NAME">myapp</Property>
</Properties>
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
<RollingFile name="File"
fileName="${LOG_PATH}/${APP_NAME}.log"
filePattern="${LOG_PATH}/${APP_NAME}-%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="%d %-5level [%t] %logger{50} - %msg%n"/>
<Policies>
<SizeBasedTriggeringPolicy size="100MB"/>
<TimeBasedTriggeringPolicy interval="1"/>
</Policies>
<DefaultRolloverStrategy max="30"/>
</RollingFile>
<Async name="AsyncFile" bufferSize="1024">
<AppenderRef ref="File"/>
</Async>
</Appenders>
<Loggers>
<Logger name="org.hibernate.SQL" level="DEBUG"/>
<Root level="INFO">
<AppenderRef ref="Console"/>
<AppenderRef ref="AsyncFile"/>
</Root>
</Loggers>
</Configuration>
Logback vs Log4j2
| 维度 | Logback | Log4j2 |
|---|
| Spring Boot 默认 | ✅ 是 | 需切换 |
| 性能 | 好 | 更好(异步) |
| 配置格式 | XML | XML/YAML/JSON/Properties |
| 异步 | 需配置 | 内置 AsyncLogger |
| 社区 | 成熟 | 活跃 |
结构化日志
// 使用 MDC(Mapped Diagnostic Context)
import org.slf4j.MDC;
public class LoggingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
try {
MDC.put("traceId", UUID.randomUUID().toString());
MDC.put("userId", getUserId(request));
MDC.put("requestUri", ((HttpServletRequest) request).getRequestURI());
chain.doFilter(request, response);
} finally {
MDC.clear();
}
}
}
⚠️ 注意事项
- 不要使用
System.out.println — 没有级别、时间戳、线程信息。 - 生产环境不要开 DEBUG — 日志量大,影响性能。
- 异常日志传入异常对象 —
log.error("msg", e) 而非 log.error(e.getMessage())。 - 避免字符串拼接 — 使用
{} 占位符,性能更好。
💡 技巧
- Lombok
@Slf4j — 自动生成 private static final Logger log。 - 异步日志 — Log4j2 的
AsyncLogger 可大幅减少日志对业务线程的影响。 - 日志采样 — 高并发场景可配置只记录部分 DEBUG 日志。
🏢 业务场景
- 线上排错: ERROR 日志 + 堆栈信息定位问题根因。
- 审计追踪: INFO 日志记录关键业务操作。
- 性能分析: TRACE/DEBUG 日志分析方法执行时间。
- ELK 集构: JSON 格式日志 → Filebeat → Elasticsearch → Kibana。
📖 扩展阅读