日志

Spring Boot 默认使用 SLF4J(日志门面)+ Logback(实现),开箱即用,无需额外配置即可输出日志。

日志门面与实现

代码层          SLF4J API(接口)
                    │
实现层    Logback(默认)/ Log4j2 / JUL

始终通过 SLF4J 编写代码,不直接引用 Logback 类,方便切换实现:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
public class OrderService {
    private static final Logger log = LoggerFactory.getLogger(OrderService.class);
 
    // Lombok @Slf4j 等价写法,推荐
    // @Slf4j public class OrderService { ... }
 
    public void create(Order order) {
        log.debug("创建订单, orderId={}", order.getId());
        log.info("订单创建成功, orderId={}, userId={}", order.getId(), order.getUserId());
        log.warn("库存不足, productId={}, remaining={}", order.getProductId(), 0);
        log.error("订单创建失败", e);
    }
}

基础配置(application.yml)

logging:
  level:
    root: INFO                          # 全局默认级别
    com.example: DEBUG                  # 指定包开启 DEBUG
    com.example.service.OrderService: TRACE  # 指定类开启 TRACE
    org.springframework.web: DEBUG      # Spring MVC 请求日志
    org.hibernate.SQL: DEBUG            # 打印 SQL
    org.hibernate.orm.jdbc.bind: TRACE  # 打印 SQL 绑定参数
 
  # 日志文件输出(二选一)
  file:
    name: logs/app.log         # 指定文件路径(包含文件名)
    # path: logs/              # 只指定目录,文件名固定为 spring.log
 
  # 控制台输出格式
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
    file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
 
  # 滚动归档配置(Spring Boot 2.7+)
  logback:
    rollingpolicy:
      max-file-size: 100MB       # 单个文件最大
      max-history: 30            # 保留最近 30 天
      total-size-cap: 3GB        # 所有归档文件总大小上限
      file-name-pattern: logs/app-%d{yyyy-MM-dd}.%i.log.gz

日志级别

TRACE < DEBUG < INFO < WARN < ERROR
级别用途
TRACE最细粒度,方法入参/出参,仅开发调试
DEBUG关键中间状态,SQL、HTTP 请求详情
INFO业务关键节点(订单创建、用户登录)、启动信息
WARN可恢复的异常、降级、慢查询告警
ERROR需要人工介入的错误,必须携带异常堆栈

生产环境建议:root 级别设为 WARN,业务包设为 INFO,避免日志爆炸。


logback-spring.xml(精细控制)

application.yml 满足大多数场景,若需要条件配置或多 Appender,改用 logback-spring.xml(支持 Spring Profile):

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 引入 Spring Boot 默认基础配置 -->
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
 
    <springProperty scope="context" name="appName"
                    source="spring.application.name" defaultValue="app"/>
 
    <!-- ── 控制台 Appender ── -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level [%X{traceId},%X{spanId}] %logger{36} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>
 
    <!-- ── 滚动文件 Appender ── -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>logs/${appName}.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>logs/${appName}-%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 [%X{traceId}] %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
 
    <!-- ── JSON 结构化日志 Appender(用于日志聚合) ── -->
    <appender name="JSON_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>logs/${appName}-json.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>logs/${appName}-json-%d{yyyy-MM-dd}.log.gz</fileNamePattern>
            <maxHistory>7</maxHistory>
        </rollingPolicy>
        <encoder class="net.logstash.logback.encoder.LogstashEncoder">
            <includeMdcKeyName>traceId</includeMdcKeyName>
            <includeMdcKeyName>spanId</includeMdcKeyName>
            <includeMdcKeyName>userId</includeMdcKeyName>
        </encoder>
    </appender>
 
    <!-- ── 异步包装(减少日志 I/O 对主线程的影响) ── -->
    <appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
        <discardingThreshold>0</discardingThreshold>  <!-- 0=不丢弃,默认 80% 满时丢 TRACE/DEBUG/INFO -->
        <queueSize>512</queueSize>
        <appender-ref ref="FILE"/>
    </appender>
 
    <!-- ── Profile 条件配置 ── -->
    <springProfile name="dev">
        <root level="DEBUG">
            <appender-ref ref="CONSOLE"/>
        </root>
        <logger name="com.example" level="TRACE"/>
    </springProfile>
 
    <springProfile name="prod">
        <root level="WARN">
            <appender-ref ref="ASYNC_FILE"/>
            <appender-ref ref="JSON_FILE"/>
        </root>
        <logger name="com.example" level="INFO"/>
    </springProfile>
</configuration>

JSON 结构化日志需引入 logstash-logback-encoder

implementation 'net.logstash.logback:logstash-logback-encoder:7.4'

MDC(Mapped Diagnostic Context)

MDC 是线程本地的键值存储,记录的字段会自动追加到该线程的每条日志中:

// Filter 中统一注入请求上下文
MDC.put("requestId", UUID.randomUUID().toString());
MDC.put("userId", currentUser.getId().toString());
 
try {
    chain.doFilter(request, response);
} finally {
    MDC.clear();   // 请求结束后必须清空,防止线程池复用时污染
}

Pattern 中通过 %X{key} 引用 MDC 值:

%d{HH:mm:ss} [%X{requestId}] [%X{userId}] %-5level - %msg%n

链路追踪中 traceId / spanId 也通过 MDC 注入,详见 链路追踪


切换到 Log4j2

Log4j2 的异步性能优于 Logback(LMAX Disruptor),高吞吐场景可考虑切换:

configurations {
    all {
        // 排除 Logback
        exclude group: 'ch.qos.logback', module: 'logback-classic'
    }
}
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-log4j2'
}

配置文件改用 log4j2-spring.xml,其余 SLF4J 代码无需改动。


常用日志实践

不要用字符串拼接(每次都创建对象,即使该级别被过滤也执行):

// 错误:无论级别是否开启,都会拼接字符串
log.debug("用户信息: " + user.toString());
 
// 正确:占位符,仅在 DEBUG 开启时才格式化
log.debug("用户信息: {}", user);
 
// 极端性能场景:先判断级别
if (log.isDebugEnabled()) {
    log.debug("复杂对象: {}", buildExpensiveDebugInfo());
}

异常日志必须传入 Throwable

// 错误:只打了消息,没有堆栈
log.error("处理失败: " + e.getMessage());
 
// 正确:消息 + Throwable(Logback 自动打完整堆栈)
log.error("处理订单失败, orderId={}", orderId, e);

相关链接