前置知识: Java

Java日志系统

2 minIntermediate2026/6/14

SLF4J、Logback与日志框架

概述

日志是 Java 应用开发中不可或缺的组成部分,用于记录运行状态、排查问题和监控系统健康度。Java 日志生态以 SLF4J 为日志门面、Logback 为日志实现最为常见。本文介绍日志框架的配置、使用和最佳实践。

基础概念

日志框架体系

框架定位说明
SLF4J日志门面统一日志 API,不涉及实现
Logback日志实现SLF4J 的原生实现,性能优秀
Log4j 2日志实现Apache 出品,异步性能极佳
JUL日志实现JDK 内置,功能有限

日志级别

从低到高依次为:TRACE < DEBUG < INFO < WARN < ERROR。设置某个级别后,只输出该级别及更高级别的日志。

快速上手

基本使用

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

// 获取 Logger 实例(推荐使用类名作为标识)
private static final Logger logger = LoggerFactory.getLogger(MyClass.class);

// 使用占位符,避免不必要的字符串拼接
logger.info("用户 {} 登录成功,IP: {}", userId, clientIp);
logger.debug("查询参数: {}", params);
logger.warn("缓存未命中,key: {}", key);
logger.error("处理订单失败,订单号: {}", orderId, exception);

// 不要这样写(性能差)
logger.debug("结果: " + result);  // 即使 DEBUG 级别关闭也会拼接字符串

Logback 基础配置

<!-- logback.xml 基础配置 -->
<configuration>
    <!-- 控制台输出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- 根日志级别 -->
    <root level="INFO">
        <appender-ref ref="STDOUT" />
    </root>

    <!-- 按包设置不同级别 -->
    <logger name="com.example" level="DEBUG" />
    <logger name="org.springframework" level="WARN" />
</configuration>

详细用法

文件滚动输出

<!-- 按日期和大小滚动的文件输出 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>logs/application.log</file>
    <!-- 滚动策略 -->
    <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
        <fileNamePattern>logs/application.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
        <maxFileSize>100MB</maxFileSize>   <!-- 单个文件最大 100MB -->
        <maxHistory>30</maxHistory>         <!-- 保留 30 天 -->
        <totalSizeCap>5GB</totalSizeCap>    <!-- 总大小上限 5GB -->
    </rollingPolicy>
    <encoder>
        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        <charset>UTF-8</charset>
    </encoder>
</appender>

异步日志

<!-- 异步 Appender,避免日志 I/O 阻塞业务线程 -->
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
    <!-- 队列容量,默认 256 -->
    <queueSize>1024</queueSize>
    <!-- 队列满时不丢弃 WARN 以上级别的日志 -->
    <discardingThreshold>0</discardingThreshold>
    <!-- 队列满时是否阻塞(false=丢弃日志,true=阻塞等待) -->
    <neverBlock>true</neverBlock>
    <appender-ref ref="FILE" />
</appender>

按业务分离日志

<!-- 将不同业务的日志输出到不同文件 -->
<appender name="ORDER_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>logs/order.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
        <fileNamePattern>logs/order.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
        <maxFileSize>200MB</maxFileSize>
        <maxHistory>60</maxHistory>
    </rollingPolicy>
    <encoder>
        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %msg%n</pattern>
    </encoder>
</appender>

<!-- 业务 Logger -->
<logger name="ORDER_LOGGER" level="INFO" additivity="false">
    <appender-ref ref="ORDER_LOG" />
</logger>
// 在代码中使用独立的 Logger
private static final Logger orderLogger = LoggerFactory.getLogger("ORDER_LOGGER");
orderLogger.info("订单创建: orderId={}, amount={}", orderId, amount);

常见场景

Spring Boot 日志配置

# application.yml 日志配置
logging:
  level:
    root: INFO
    com.example: DEBUG
    org.hibernate.SQL: DEBUG
    org.hibernate.type.descriptor.sql.BasicBinder: TRACE # 显示 SQL 参数
  file:
    name: logs/application.log
  logback:
    rollingpolicy:
      max-file-size: 100MB
      max-history: 30
      total-size-cap: 5GB

结构化日志(JSON 格式)

<!-- 输出 JSON 格式日志,便于 ELK 采集 -->
<appender name="JSON_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>logs/application.json</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
        <fileNamePattern>logs/application.%d{yyyy-MM-dd}.%i.json.gz</fileNamePattern>
        <maxFileSize>100MB</maxFileSize>
        <maxHistory>30</maxHistory>
    </rollingPolicy>
    <encoder class="net.logstash.logback.encoder.LogstashEncoder">
        <includeContext>true</includeContext>
        <includeMdc>true</includeMdc>
    </encoder>
</appender>

MDC 追踪日志

// 使用 MDC 在日志中添加追踪信息
import org.slf4j.MDC;

// 在请求入口设置追踪 ID
public class TraceFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
        String traceId = UUID.randomUUID().toString().replace("-", "");
        MDC.put("traceId", traceId);
        MDC.put("userId", getCurrentUserId());
        try {
            chain.doFilter(request, response);
        } finally {
            MDC.clear(); // 请求结束后清理
        }
    }
}
<!-- 在日志格式中引用 MDC 变量 -->
<pattern>%d{HH:mm:ss.SSS} [trace:%X{traceId}] [user:%X{userId}] %-5level %logger - %msg%n</pattern>

注意事项

  • 使用 SLF4J 占位符而非字符串拼接,避免日志级别关闭时的性能浪费
  • 异步日志的队列容量需要根据日志量合理设置,过小会丢失日志
  • 不要在循环中打印 DEBUG 日志,即使使用占位符也有方法调用开销
  • ERROR 级别日志应包含异常对象,便于定位问题
  • 生产环境不要使用 DEBUG/TRACE 级别,日志量过大会影响性能
  • 日志文件应配置滚动策略,避免磁盘写满

进阶用法

Log4j 2 异步日志

<!-- Log4j 2 的全异步模式,性能比 Logback 异步更高 -->
<!-- pom.xml 添加依赖 -->
<!-- disruptor 依赖用于全异步 -->
<!-- 启动参数: -Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector -->

<!-- log4j2.xml -->
<Configuration>
    <Appenders>
        <RollingFile name="File" fileName="logs/app.log"
            filePattern="logs/app-%d{yyyy-MM-dd}-%i.log.gz">
            <PatternLayout pattern="%d %p %c{1.} [%t] %m%n"/>
            <Policies>
                <TimeBasedTriggeringPolicy/>
                <SizeBasedTriggeringPolicy size="100MB"/>
            </Policies>
        </RollingFile>
    </Appenders>
    <Loggers>
        <Root level="info">
            <AppenderRef ref="File"/>
        </Root>
    </Loggers>
</Configuration>

自定义日志脱敏

// 日志脱敏:防止敏感信息写入日志
public class SensitiveDataConverter extends MessageConverter {
    // 手机号脱敏: 13812345678 -> 138****5678
    private static final Pattern PHONE_PATTERN =
        Pattern.compile("(\\d{3})\\d{4}(\\d{4})");

    @Override
    public String convert(ILoggingEvent event) {
        String message = super.convert(event);
        return PHONE_PATTERN.matcher(message).replaceAll("$1****$2");
    }
}