Java深入解析篇三十之Java日志框架

Java日志框架详解

Java日志体系概述

日志的作用与分类

日志是应用程序运行时产生的结构化记录,用于问题排查、性能监控、审计追踪和业务分析。

java 复制代码
// 日志的典型使用场景
public class OrderService {
    private static final Logger log = LoggerFactory.getLogger(OrderService.class);

    public Order createOrder(OrderRequest request) {
        // 业务关键节点 - INFO
        log.info("创建订单开始, userId={}, productId={}", request.getUserId(), request.getProductId());
        try {
            Order order = doCreate(request);
            log.info("创建订单成功, orderId={}", order.getId());
            return order;
        } catch (InventoryException e) {
            // 可预期的业务异常 - WARN
            log.warn("库存不足, productId={}, stock={}", request.getProductId(), e.getStock());
            throw e;
        } catch (Exception e) {
            // 不可预期的系统异常 - ERROR
            log.error("创建订单失败, request={}", request, e);
            throw new ServiceException("系统异常", e);
        }
    }
}

Java日志发展史

阶段 框架 特点
早期 System.out.println 无级别、无格式、无输出控制
JDK1.4 java.util.logging (JUL) JDK内置,功能有限
2001 Log4j 1.x 功能强大,但设计缺陷多
2004 Commons Logging (JCL) 门面思想,但类加载问题多
2004 SLF4J + Logback 现代日志门面+实现,性能优异
2012 Log4j2 重写Log4j,异步高性能
2021 Log4j2 CVE-2021-44228 Log4Shell漏洞,推动安全升级

日志体系架构

复制代码
┌─────────────────────────────────────────────────┐
│                  应用层代码                       │
├─────────────────────────────────────────────────┤
│         日志门面: SLF4J / Commons Logging         │
├───────────┬───────────────┬─────────────────────┤
│  Logback  │    Log4j2     │        JUL          │
├───────────┴───────────────┴─────────────────────┤
│     Appender: Console / File / Async / Kafka     │
├─────────────────────────────────────────────────┤
│     Layout: Pattern / JSON / HTML / XML          │
└─────────────────────────────────────────────────┘

日志门面(SLF4J API)

核心接口

java 复制代码
// org.slf4j.Logger - 核心日志接口
public interface Logger {
    String getName();

    // TRACE级别
    boolean isTraceEnabled();
    void trace(String msg);
    void trace(String format, Object arg);
    void trace(String format, Object arg1, Object arg2);
    void trace(String format, Object... arguments);
    void trace(String msg, Throwable t);

    // DEBUG级别
    boolean isDebugEnabled();
    void debug(String msg);
    void debug(String format, Object... arguments);
    void debug(String msg, Throwable t);

    // INFO级别
    boolean isInfoEnabled();
    void info(String msg);
    void info(String format, Object... arguments);
    void info(String msg, Throwable t);

    // WARN级别
    boolean isWarnEnabled();
    void warn(String msg);
    void warn(String format, Object... arguments);
    void warn(String msg, Throwable t);

    // ERROR级别
    boolean isErrorEnabled();
    void error(String msg);
    void error(String format, Object... arguments);
    void error(String msg, Throwable t);
}

Logger获取方式

java 复制代码
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class UserService {

    // 方式一:通过类获取(最常用)
    private static final Logger log = LoggerFactory.getLogger(UserService.class);

    // 方式二:通过名称获取
    private static final Logger auditLog = LoggerFactory.getLogger("AUDIT");

    // 方式三:Lombok注解(编译期生成)
    // @Slf4j 自动生成 private static final Logger log = ...
}

SLF4J 2.0 Fluent API

java 复制代码
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class FluentApiDemo {
    private static final Logger log = LoggerFactory.getLogger(FluentApiDemo.class);

    public void process(String orderId) {
        // SLF4J 2.0 流式API
        log.atInfo()
           .setMessage("处理订单")
           .addKeyValue("orderId", orderId)
           .addKeyValue("timestamp", System.currentTimeMillis())
           .log();

        // 带异常的流式API
        log.atError()
           .setMessage("订单处理失败")
           .addKeyValue("orderId", orderId)
           .setCause(new RuntimeException("timeout"))
           .log();

        // 条件日志(避免不必要的参数计算)
        log.atDebug()
           .setMessage("详细数据: {}")
           .addArgument(() -> buildExpensiveDebugInfo())
           .log();
    }

    private String buildExpensiveDebugInfo() {
        // 仅在DEBUG级别启用时才执行
        return "expensive computation result";
    }
}

Marker标记

java 复制代码
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;

public class MarkerDemo {
    private static final Logger log = LoggerFactory.getLogger(MarkerDemo.class);

    // 定义Marker用于分类过滤
    private static final Marker SECURITY = MarkerFactory.getMarker("SECURITY");
    private static final Marker AUDIT = MarkerFactory.getMarker("AUDIT");
    private static final Marker BILLING = MarkerFactory.getMarker("BILLING");

    public void loginAttempt(String username, boolean success) {
        if (!success) {
            log.warn(SECURITY, "登录失败, username={}", username);
        }
    }

    public void charge(String userId, double amount) {
        log.info(BILLING, "扣费成功, userId={}, amount={}", userId, amount);
    }
}

日志实现(Logback/Log4j2/JUL)

Logback

xml 复制代码
<!-- Maven依赖 -->
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.4.14</version>
</dependency>
<!-- logback-classic 自动引入 logback-core 和 slf4j-api -->
java 复制代码
// Logback特有功能:SiftingAppender按用户分离日志
// 编程式配置示例
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.core.ConsoleAppender;

public class LogbackProgrammaticConfig {
    public static void configure() {
        LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
        context.reset(); // 清除默认配置

        // 创建Encoder
        PatternLayoutEncoder encoder = new PatternLayoutEncoder();
        encoder.setContext(context);
        encoder.setPattern("%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n");
        encoder.start();

        // 创建Appender
        ConsoleAppender<ILoggingEvent> appender = new ConsoleAppender<>();
        appender.setContext(context);
        appender.setName("CONSOLE");
        appender.setEncoder(encoder);
        appender.start();

        // 配置Root Logger
        ch.qos.logback.classic.Logger rootLogger = context.getLogger(Logger.ROOT_LOGGER_NAME);
        rootLogger.setLevel(Level.INFO);
        rootLogger.addAppender(appender);
    }
}

Log4j2

xml 复制代码
<!-- Maven依赖 -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>2.23.1</version>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.23.1</version>
</dependency>
<!-- SLF4J桥接 -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-slf4j2-impl</artifactId>
    <version>2.23.1</version>
</dependency>
java 复制代码
// Log4j2特有功能:Lambda延迟求值
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class Log4j2LambdaDemo {
    private static final Logger log = LogManager.getLogger(Log4j2LambdaDemo.class);

    public void processData(List<Item> items) {
        // Lambda表达式仅在级别启用时才求值
        log.debug("Items: {}", () -> items.stream()
                .map(Item::toString)
                .collect(Collectors.joining(", ")));

        // 自定义Level
        log.log(Level.forName("AUDIT", 350), "审计事件: user={}", "admin");
    }
}

JUL(java.util.logging)

java 复制代码
import java.util.logging.*;

public class JulDemo {
    private static final Logger logger = Logger.getLogger(JulDemo.class.getName());

    public static void main(String[] args) throws Exception {
        // 配置Handler
        ConsoleHandler consoleHandler = new ConsoleHandler();
        consoleHandler.setLevel(Level.ALL);
        consoleHandler.setFormatter(new SimpleFormatter());

        FileHandler fileHandler = new FileHandler("app-%u.log", 1024 * 1024, 5, true);
        fileHandler.setLevel(Level.INFO);
        fileHandler.setFormatter(new XMLFormatter());

        logger.addHandler(consoleHandler);
        logger.addHandler(fileHandler);
        logger.setUseParentHandlers(false); // 不向父Logger传播
        logger.setLevel(Level.ALL);

        // 记录日志
        logger.finest("最细粒度信息");
        logger.finer("较细粒度信息");
        logger.fine("细粒度信息");
        logger.info("普通信息");
        logger.warning("警告信息");
        logger.severe("严重错误");

        // 带异常
        try {
            throw new IllegalStateException("demo");
        } catch (Exception e) {
            logger.log(Level.SEVERE, "操作失败", e);
        }
    }
}

SLF4J绑定机制

SLF4J 1.x绑定原理

java 复制代码
// SLF4J 1.x 通过 StaticLoggerBinder 实现绑定
// 每个实现jar包中必须包含:
// org/slf4j/impl/StaticLoggerBinder.class

// LoggerFactory源码简化:
public final class LoggerFactory {
    public static Logger getLogger(String name) {
        // 1. 查找classpath中的 org.slf4j.impl.StaticLoggerBinder
        // 2. 调用 StaticLoggerBinder.getSingleton().getLoggerFactory()
        // 3. 由具体实现创建Logger实例
        ILoggerFactory factory = StaticLoggerBinder.getSingleton().getLoggerFactory();
        return factory.getLogger(name);
    }
}

SLF4J 2.x绑定原理

java 复制代码
// SLF4J 2.x 使用 Java ServiceLoader 机制
// 实现jar包中提供:
// META-INF/services/org.slf4j.spi.SLF4JServiceProvider

// 自定义ServiceProvider示例
public class CustomServiceProvider implements SLF4JServiceProvider {
    private ILoggerFactory loggerFactory;
    private IMarkerFactory markerFactory;

    @Override
    public ILoggerFactory getLoggerFactory() {
        return loggerFactory;
    }

    @Override
    public IMarkerFactory getMarkerFactory() {
        return markerFactory;
    }

    @Override
    public String getRequestedApiVersion() {
        return "2.0";
    }

    @Override
    public void initialize() {
        loggerFactory = new CustomLoggerFactory();
        markerFactory = new BasicMarkerFactory();
    }
}

桥接器使用

xml 复制代码
<!-- 将Commons Logging调用桥接到SLF4J -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jcl-over-slf4j</artifactId>
    <version>2.0.12</version>
</dependency>

<!-- 将Log4j 1.x调用桥接到SLF4J -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>log4j-over-slf4j</artifactId>
    <version>2.0.12</version>
</dependency>

<!-- 将JUL调用桥接到SLF4J -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jul-to-slf4j</artifactId>
    <version>2.0.12</version>
</dependency>

<!-- 注意:不能同时存在桥接器和原实现,否则循环调用 -->
<!-- 错误示例:jcl-over-slf4j + slf4j-jcl 同时存在 -->

Logback配置(logback-spring.xml)

完整配置示例

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="30 seconds">

    <!-- 属性定义 -->
    <property name="LOG_PATH" value="${LOG_PATH:-./logs}"/>
    <property name="APP_NAME" value="my-application"/>
    <property name="LOG_PATTERN"
              value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} [%X{traceId}/%X{spanId}] - %msg%n"/>

    <!-- 控制台输出 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${LOG_PATTERN}</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>${LOG_PATTERN}</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!-- 错误日志单独输出 -->
    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_PATH}/${APP_NAME}-error.log</file>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/${APP_NAME}-error.%d{yyyy-MM-dd}.log.gz</fileNamePattern>
            <maxHistory>60</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
    </appender>

    <!-- 异步Appender -->
    <appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
        <queueSize>1024</queueSize>
        <discardingThreshold>0</discardingThreshold>
        <neverBlock>true</neverBlock>
        <appender-ref ref="FILE"/>
    </appender>

    <!-- 按环境区分 -->
    <springProfile name="dev">
        <root level="DEBUG">
            <appender-ref ref="CONSOLE"/>
        </root>
    </springProfile>

    <springProfile name="prod">
        <root level="INFO">
            <appender-ref ref="ASYNC_FILE"/>
            <appender-ref ref="ERROR_FILE"/>
        </root>
    </springProfile>

    <!-- 特定包级别控制 -->
    <logger name="com.example.mapper" level="DEBUG"/>
    <logger name="org.springframework" level="WARN"/>
    <logger name="org.apache.http" level="WARN"/>
</configuration>

Log4j2配置(log4j2.xml)

完整配置示例

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" monitorInterval="30">

    <Properties>
        <Property name="LOG_PATH">./logs</Property>
        <Property name="PATTERN">%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} [%X{traceId}] - %msg%n</Property>
    </Properties>

    <Appenders>
        <!-- 控制台 -->
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="${PATTERN}" charset="UTF-8"/>
        </Console>

        <!-- 滚动文件 -->
        <RollingFile name="RollingFile"
                     fileName="${LOG_PATH}/app.log"
                     filePattern="${LOG_PATH}/app-%d{yyyy-MM-dd}-%i.log.gz">
            <PatternLayout pattern="${PATTERN}"/>
            <Policies>
                <TimeBasedTriggeringPolicy interval="1"/>
                <SizeBasedTriggeringPolicy size="100MB"/>
            </Policies>
            <DefaultRolloverStrategy max="30">
                <Delete basePath="${LOG_PATH}" maxDepth="1">
                    <IfFileName glob="app-*.log.gz"/>
                    <IfLastModified age="30d"/>
                </Delete>
            </DefaultRolloverStrategy>
        </RollingFile>

        <!-- 异步Appender -->
        <Async name="AsyncFile">
            <AppenderRef ref="RollingFile"/>
        </Async>

        <!-- JSON格式输出(结构化日志) -->
        <RollingFile name="JsonFile"
                     fileName="${LOG_PATH}/app.json"
                     filePattern="${LOG_PATH}/app-%d{yyyy-MM-dd}-%i.json.gz">
            <JsonLayout compact="true" eventEol="true"
                        includeStacktrace="true" stacktraceAsString="true"/>
            <Policies>
                <SizeBasedTriggeringPolicy size="200MB"/>
            </Policies>
        </RollingFile>
    </Appenders>

    <Loggers>
        <!-- 全异步Logger(基于Disruptor) -->
        <AsyncRoot level="INFO">
            <AppenderRef ref="Console"/>
            <AppenderRef ref="AsyncFile"/>
        </AsyncRoot>

        <AsyncLogger name="com.example" level="DEBUG" additivity="false">
            <AppenderRef ref="AsyncFile"/>
        </AsyncLogger>

        <Logger name="org.springframework" level="WARN"/>
    </Loggers>
</Configuration>

Log4j2全异步模式配置

bash 复制代码
# JVM启动参数启用全异步(性能最优)
-DLog4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector

# Disruptor RingBuffer大小(默认256*1024)
-DAsyncLogger.RingBufferSize=524288

# 等待策略
-DAsyncLogger.WaitStrategy=Busyspin

日志级别(TRACE/DEBUG/INFO/WARN/ERROR)

级别定义与使用场景

java 复制代码
public class LogLevelGuide {
    private static final Logger log = LoggerFactory.getLogger(LogLevelGuide.class);

    public void demonstrate() {
        // TRACE: 最细粒度,方法进入/退出、变量值追踪
        log.trace("进入方法 processOrder, params={}", params);
        log.trace("SQL执行: {}", sqlStatement);

        // DEBUG: 开发调试信息,生产环境通常关闭
        log.debug("缓存命中: key={}, value={}", cacheKey, cacheValue);
        log.debug("HTTP响应: status={}, body={}", status, responseBody);

        // INFO: 业务关键节点,生产环境默认级别
        log.info("用户注册成功: userId={}", userId);
        log.info("订单支付完成: orderId={}, amount={}", orderId, amount);
        log.info("定时任务执行完毕: task={}, cost={}ms", taskName, cost);

        // WARN: 潜在问题,系统仍可正常运行
        log.warn("重试第{}次: service={}", retryCount, serviceName);
        log.warn("连接池使用率过高: active={}/{}", active, max);
        log.warn("配置项缺失,使用默认值: key={}, default={}", key, defaultVal);

        // ERROR: 错误事件,需要关注和处理
        log.error("数据库操作失败: sql={}", sql, exception);
        log.error("外部服务调用超时: url={}, timeout={}ms", url, timeout);
        log.error("消息消费失败: topic={}, msgId={}", topic, msgId, exception);
    }
}

级别过滤规则

java 复制代码
// 级别从低到高: TRACE < DEBUG < INFO < WARN < ERROR
// 设置某级别后,只输出该级别及以上的日志

public class LevelFilterDemo {
    public static void main(String[] args) {
        // 动态修改日志级别
        ch.qos.logback.classic.Logger logger =
            (ch.qos.logback.classic.Logger) LoggerFactory.getLogger("com.example");

        // 运行时调整级别
        logger.setLevel(ch.qos.logback.classic.Level.DEBUG);

        // 判断级别是否启用(避免不必要的计算)
        if (log.isDebugEnabled()) {
            log.debug("复杂对象: {}", buildExpensiveObject());
        }
    }
}

Logger/Appender/Layout/Filter

组件关系

java 复制代码
// Logger: 日志记录器,负责接收日志事件
// Appender: 输出目的地,决定日志写到哪里
// Layout/Encoder: 格式化,决定日志长什么样
// Filter: 过滤器,决定哪些日志通过

// 一个Logger可以关联多个Appender
// 一个Appender包含一个Layout和多个Filter

自定义Appender

java 复制代码
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.AppenderBase;

/**
 * 自定义Appender:将ERROR日志发送到告警系统
 */
public class AlertAppender extends AppenderBase<ILoggingEvent> {

    private String alertWebhook;
    private int threshold = 5; // 5分钟内超过N条ERROR才告警
    private final AtomicInteger errorCount = new AtomicInteger(0);

    @Override
    protected void append(ILoggingEvent event) {
        if (event.getLevel().isGreaterOrEqual(Level.ERROR)) {
            int count = errorCount.incrementAndGet();
            if (count >= threshold) {
                sendAlert(event);
                errorCount.set(0);
            }
        }
    }

    private void sendAlert(ILoggingEvent event) {
        String message = String.format("[%s] %s - %s",
            event.getLevel(), event.getLoggerName(), event.getFormattedMessage());
        // 发送到告警平台(钉钉/企业微信/PagerDuty)
        HttpClient.post(alertWebhook, message);
    }

    public void setAlertWebhook(String alertWebhook) {
        this.alertWebhook = alertWebhook;
    }
}

自定义Filter

java 复制代码
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.filter.Filter;
import ch.qos.logback.core.spi.FilterReply;

/**
 * 自定义Filter:过滤包含敏感词的日志
 */
public class SensitiveFilter extends Filter<ILoggingEvent> {

    private List<String> sensitivePatterns = List.of("password", "secret", "token");

    @Override
    public FilterReply decide(ILoggingEvent event) {
        String message = event.getFormattedMessage();
        for (String pattern : sensitivePatterns) {
            if (message != null && message.toLowerCase().contains(pattern)) {
                return FilterReply.DENY; // 拒绝输出
            }
        }
        return FilterReply.NEUTRAL; // 交给下一个Filter
    }
}

自定义Layout(Encoder)

java 复制代码
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.classic.spi.ILoggingEvent;

/**
 * 自定义Encoder:添加应用元数据
 */
public class MetadataEncoder extends PatternLayoutEncoder {

    private String appName;
    private String env;

    @Override
    public byte[] encode(ILoggingEvent event) {
        // 在MDC中注入元数据
        event.getMDCPropertyMap().put("app", appName);
        event.getMDCPropertyMap().put("env", env);
        return super.encode(event);
    }
}

MDC(Mapped Diagnostic Context)

基本使用

java 复制代码
import org.slf4j.MDC;

public class MdcDemo {
    private static final Logger log = LoggerFactory.getLogger(MdcDemo.class);

    public void handleRequest(HttpServletRequest request) {
        try {
            // 设置MDC上下文
            MDC.put("requestId", UUID.randomUUID().toString());
            MDC.put("userId", getCurrentUserId());
            MDC.put("clientIp", request.getRemoteAddr());
            MDC.put("uri", request.getRequestURI());

            log.info("请求开始处理");
            // 后续所有日志自动携带MDC信息
            doBusinessLogic();
            log.info("请求处理完成");

        } finally {
            // 必须清理,防止线程池复用导致数据污染
            MDC.clear();
        }
    }
}

Spring拦截器自动注入MDC

java 复制代码
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class MdcInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) {
        MDC.put("traceId", getOrCreateTraceId(request));
        MDC.put("requestId", UUID.randomUUID().toString().replace("-", ""));
        MDC.put("userId", extractUserId(request));
        MDC.put("clientIp", getClientIp(request));
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler, Exception ex) {
        MDC.clear();
    }

    private String getOrCreateTraceId(HttpServletRequest request) {
        String traceId = request.getHeader("X-Trace-Id");
        return traceId != null ? traceId : UUID.randomUUID().toString().replace("-", "");
    }
}

跨线程MDC传递

java 复制代码
import org.springframework.core.task.TaskDecorator;
import org.slf4j.MDC;
import java.util.Map;

/**
 * Spring线程池装饰器:自动传递MDC上下文
 */
public class MdcTaskDecorator implements TaskDecorator {

    @Override
    public Runnable decorate(Runnable runnable) {
        // 在提交线程中捕获MDC
        Map<String, String> contextMap = MDC.getCopyOfContextMap();
        return () -> {
            try {
                // 在执行线程中恢复MDC
                if (contextMap != null) {
                    MDC.setContextMap(contextMap);
                }
                runnable.run();
            } finally {
                MDC.clear();
            }
        };
    }
}

// 配置线程池
@Configuration
public class AsyncConfig {

    @Bean("taskExecutor")
    public ThreadPoolTaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(50);
        executor.setQueueCapacity(200);
        executor.setTaskDecorator(new MdcTaskDecorator());
        executor.setThreadNamePrefix("async-");
        executor.initialize();
        return executor;
    }
}

异步日志(AsyncAppender/Disruptor)

Logback AsyncAppender

xml 复制代码
<!-- logback异步配置 -->
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
    <!-- 队列大小,默认256 -->
    <queueSize>2048</queueSize>
    <!-- 队列剩余容量低于此值时,丢弃TRACE/DEBUG/INFO级别日志 -->
    <!-- 设为0表示永不丢弃 -->
    <discardingThreshold>0</discardingThreshold>
    <!-- 队列满时是否阻塞调用线程,true=不阻塞直接丢弃 -->
    <neverBlock>true</neverBlock>
    <!-- 是否包含调用者信息(类名、行号),获取代价较高 -->
    <includeCallerData>false</includeCallerData>
    <appender-ref ref="FILE"/>
</appender>

Log4j2 Disruptor异步

java 复制代码
// Log4j2异步日志性能测试对比
public class AsyncLogBenchmark {

    /**
     * Log4j2全异步模式(推荐)
     * 基于LMAX Disruptor RingBuffer
     * 无锁设计,单线程写入性能极高
     */
    public static void asyncLoggerDemo() {
        // 需要JVM参数:
        // -DLog4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
        Logger log = LogManager.getLogger(AsyncLogBenchmark.class);
        for (int i = 0; i < 1_000_000; i++) {
            log.info("Async message #{}", i);
        }
    }

    /**
     * 混合异步模式
     * 在配置中使用<Async>包裹Appender
     */
    public static void mixedAsyncDemo() {
        // log4j2.xml中:
        // <Async name="Async" bufferSize="262144">
        //     <AppenderRef ref="RollingFile"/>
        // </Async>
        Logger log = LogManager.getLogger(AsyncLogBenchmark.class);
        log.info("Mixed async message");
    }
}

异步日志性能对比

方案 吞吐量(msg/s) 延迟(avg) GC压力
同步写入 ~50,000 ~20μs
Logback AsyncAppender ~200,000 ~5μs
Log4j2 AsyncLogger ~1,000,000+ ~1μs
Log4j2 Garbage-Free ~1,200,000+ <1μs 极低

日志滚动策略(按大小/时间/数量)

Logback滚动策略

xml 复制代码
<!-- 按时间+大小滚动 -->
<appender name="ROLLING" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>logs/app.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
        <!-- 按天归档,同一天内按序号分片 -->
        <fileNamePattern>logs/app.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
        <!-- 单个文件最大100MB -->
        <maxFileSize>100MB</maxFileSize>
        <!-- 保留30天历史 -->
        <maxHistory>30</maxHistory>
        <!-- 所有归档文件总大小上限 -->
        <totalSizeCap>5GB</totalSizeCap>
        <!-- 启动时清理过期文件 -->
        <cleanHistoryOnStart>true</cleanHistoryOnStart>
    </rollingPolicy>
    <encoder>
        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
    </encoder>
</appender>

<!-- 按固定大小滚动 -->
<appender name="FIXED_SIZE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>logs/fixed.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
        <fileNamePattern>logs/fixed.%i.log.gz</fileNamePattern>
        <minIndex>1</minIndex>
        <maxIndex>10</maxIndex>
    </rollingPolicy>
    <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
        <maxFileSize>50MB</maxFileSize>
    </triggeringPolicy>
</appender>

Log4j2滚动策略

xml 复制代码
<RollingFile name="Rolling" fileName="logs/app.log"
             filePattern="logs/$${date:yyyy-MM}/app-%d{yyyy-MM-dd-HH}-%i.log.gz">
    <PatternLayout pattern="%d %p %c{1.} [%t] %m%n"/>
    <Policies>
        <!-- 每小时滚动 -->
        <TimeBasedTriggeringPolicy interval="1" modulate="true"/>
        <!-- 或按大小 -->
        <SizeBasedTriggeringPolicy size="250MB"/>
        <!-- 应用启动时滚动 -->
        <OnStartupTriggeringPolicy/>
    </Policies>
    <DefaultRolloverStrategy max="100">
        <!-- 自动删除超过7天的归档 -->
        <Delete basePath="logs" maxDepth="2">
            <IfFileName glob="*/app-*.log.gz"/>
            <IfLastModified age="7d"/>
        </Delete>
    </DefaultRolloverStrategy>
</RollingFile>

结构化日志(JSON格式)

Logback JSON输出

xml 复制代码
<!-- 依赖: net.logstash.logback:logstash-logback-encoder:7.4 -->
<appender name="JSON_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="net.logstash.logback.encoder.LogstashEncoder">
        <!-- 自定义字段 -->
        <customFields>{"app":"order-service","env":"prod"}</customFields>
        <!-- 包含MDC -->
        <includeMdcKeyName>traceId</includeMdcKeyName>
        <includeMdcKeyName>spanId</includeMdcKeyName>
        <includeMdcKeyName>userId</includeMdcKeyName>
        <!-- 时间戳格式 -->
        <timestampPattern>yyyy-MM-dd'T'HH:mm:ss.SSS'Z'</timestampPattern>
        <!-- 堆栈输出为字符串 -->
        <throwableConverter class="net.logstash.logback.stacktrace.ShortenedThrowableConverter">
            <maxDepthPerThrowable>30</maxDepthPerThrowable>
            <maxLength>2048</maxLength>
        </throwableConverter>
    </encoder>
</appender>

JSON日志输出示例

json 复制代码
{
  "@timestamp": "2024-01-15T10:30:45.123Z",
  "level": "INFO",
  "logger": "com.example.OrderService",
  "thread": "http-nio-8080-exec-1",
  "message": "创建订单成功",
  "traceId": "abc123def456",
  "spanId": "789ghi",
  "userId": "user_001",
  "app": "order-service",
  "orderId": "ORD-20240115-001",
  "amount": 99.99,
  "stack_trace": null
}

编程式结构化日志

java 复制代码
import net.logstash.logback.argument.StructuredArguments;
import static net.logstash.logback.argument.StructuredArguments.*;

public class StructuredLogDemo {
    private static final Logger log = LoggerFactory.getLogger(StructuredLogDemo.class);

    public void createOrder(Order order) {
        // 结构化参数:既输出到message,也作为JSON字段
        log.info("创建订单成功, orderId={}",
            value("orderId", order.getId()));

        // 仅作为JSON字段,不出现在message中
        log.info("订单详情",
            kv("orderId", order.getId()),
            kv("amount", order.getAmount()),
            kv("items", order.getItemCount()));

        // 嵌套对象
        log.info("支付回调",
            entries(Map.of(
                "channel", "alipay",
                "tradeNo", "2024011500001",
                "status", "SUCCESS"
            )));
    }
}

链路追踪集成(TraceId/SpanId)

Micrometer Tracing集成(Spring Boot 3.x)

xml 复制代码
<!-- 依赖 -->
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<dependency>
    <groupId>io.zipkin.reporter2</groupId>
    <artifactId>zipkin-reporter-brave</artifactId>
</dependency>
yaml 复制代码
# application.yml
management:
  tracing:
    sampling:
      probability: 1.0  # 采样率,生产建议0.1
  zipkin:
    tracing:
      endpoint: http://zipkin-server:9411/api/v2/spans

logging:
  pattern:
    level: "%5p [${spring.application.name},%X{traceId:-},%X{spanId:-}]"

手动传播TraceId

java 复制代码
import org.slf4j.MDC;

/**
 * 跨服务调用时传播TraceId
 */
public class TracePropagationFilter implements ClientHttpRequestInterceptor {

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body,
                                         ClientHttpRequestExecution execution) throws IOException {
        // 将当前线程的traceId放入HTTP Header
        String traceId = MDC.get("traceId");
        String spanId = MDC.get("spanId");

        if (traceId != null) {
            request.getHeaders().set("X-Trace-Id", traceId);
            request.getHeaders().set("X-Span-Id", spanId);
        }
        return execution.execute(request, body);
    }
}

/**
 * 接收端提取TraceId
 */
@Component
public class TraceExtractFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {
        String traceId = request.getHeader("X-Trace-Id");
        if (traceId == null || traceId.isEmpty()) {
            traceId = generateTraceId();
        }
        MDC.put("traceId", traceId);
        MDC.put("spanId", generateSpanId());
        try {
            chain.doFilter(request, response);
        } finally {
            MDC.clear();
        }
    }

    private String generateTraceId() {
        return UUID.randomUUID().toString().replace("-", "");
    }

    private String generateSpanId() {
        return Long.toHexString(ThreadLocalRandom.current().nextLong());
    }
}

OpenTelemetry集成

java 复制代码
// 使用OTel Java Agent自动注入(无需改代码)
// java -javaagent:opentelemetry-javaagent.jar \
//      -Dotel.service.name=order-service \
//      -Dotel.traces.exporter=otlp \
//      -Dotel.metrics.exporter=otlp \
//      -Dotel.logs.exporter=otlp \
//      -jar app.jar

// 手动创建Span
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;

public class OtelManualDemo {
    private static final Tracer tracer =
        GlobalOpenTelemetry.getTracer("order-service");

    public Order processOrder(String orderId) {
        Span span = tracer.spanBuilder("processOrder")
            .setAttribute("order.id", orderId)
            .startSpan();

        try (Scope scope = span.makeCurrent()) {
            // 业务逻辑,日志自动关联traceId
            Order order = doProcess(orderId);
            span.setAttribute("order.status", order.getStatus());
            return order;
        } catch (Exception e) {
            span.recordException(e);
            throw e;
        } finally {
            span.end();
        }
    }
}

日志性能优化

避免字符串拼接

java 复制代码
public class LogPerformance {
    private static final Logger log = LoggerFactory.getLogger(LogPerformance.class);

    public void badPractice(Object obj) {
        // 错误:无论级别是否启用,都会执行toString和字符串拼接
        log.debug("Object: " + obj.toString() + ", size: " + getList().size());

        // 错误:即使DEBUG关闭,参数仍会被求值
        log.debug("Data: {}", buildLargeString());
    }

    public void goodPractice(Object obj) {
        // 正确:使用占位符,级别不满足时不进行格式化
        log.debug("Object: {}, size: {}", obj, getList().size());

        // 正确:先判断再执行昂贵操作
        if (log.isDebugEnabled()) {
            log.debug("Data: {}", buildLargeString());
        }

        // 正确:SLF4J 2.0 Supplier方式
        log.atDebug().setMessage("Data: {}").addArgument(this::buildLargeString).log();
    }
}

批量日志与采样

java 复制代码
/**
 * 高频日志采样输出
 */
public class SampledLogger {
    private static final Logger log = LoggerFactory.getLogger(SampledLogger.class);
    private final AtomicLong counter = new AtomicLong(0);
    private static final int SAMPLE_RATE = 100; // 每100条输出1条

    public void highFrequencyEvent(String event) {
        long count = counter.incrementAndGet();
        if (count % SAMPLE_RATE == 0) {
            log.info("事件统计: event={}, totalCount={}", event, count);
        }
    }
}

/**
 * 日志聚合:合并重复日志
 */
public class AggregatedLogger {
    private static final Logger log = LoggerFactory.getLogger(AggregatedLogger.class);
    private final Map<String, AtomicLong> errorCounts = new ConcurrentHashMap<>();

    @Scheduled(fixedRate = 60000) // 每分钟汇总
    public void flushAggregatedLogs() {
        errorCounts.forEach((key, count) -> {
            if (count.get() > 0) {
                log.warn("聚合告警: error={}, count={}", key, count.getAndSet(0));
            }
        });
    }

    public void recordError(String errorType) {
        errorCounts.computeIfAbsent(errorType, k -> new AtomicLong(0)).incrementAndGet();
    }
}

JVM参数优化

bash 复制代码
# Log4j2 Garbage-Free模式(避免日志对象分配)
-Dlog4j2.enableThreadlocals=true
-Dlog4j2.enableDirectEncoders=true
-Dlog4j2.garbagefreeThreadContextMap=true

# 异步日志RingBuffer预分配
-DAsyncLogger.RingBufferSize=262144
-DAsyncLogger.WaitStrategy=Yield

# Logback异步队列预热
-Dlogback.asyncAppender.queueSize=4096

日志安全(脱敏/注入防护)

敏感信息脱敏

java 复制代码
import ch.qos.logback.classic.PatternLayout;
import ch.qos.logback.classic.spi.ILoggingEvent;

/**
 * 自定义Layout:自动脱敏
 */
public class DesensitizeLayout extends PatternLayout {

    // 手机号脱敏: 138****1234
    private static final Pattern PHONE_PATTERN =
        Pattern.compile("(1[3-9]\\d)\\d{4}(\\d{4})");
    // 身份证脱敏: 110***********1234
    private static final Pattern ID_CARD_PATTERN =
        Pattern.compile("(\\d{3})\\d{11}(\\d{4})");
    // 银行卡脱敏: 6222****1234
    private static final Pattern BANK_CARD_PATTERN =
        Pattern.compile("(\\d{4})\\d{8,12}(\\d{4})");
    // 邮箱脱敏: t***@example.com
    private static final Pattern EMAIL_PATTERN =
        Pattern.compile("(\\w)[\\w.]*(@\\w+\\.\\w+)");

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

日志注入防护

java 复制代码
/**
 * 防止CRLF注入攻击
 * 攻击者可能输入: "admin\n2024-01-15 INFO - 管理员登录成功"
 * 伪造日志条目
 */
public class LogSanitizer {

    /**
     * 清理日志消息中的换行和控制字符
     */
    public static String sanitize(String input) {
        if (input == null) return "null";
        return input
            .replace("\r", "\\r")
            .replace("\n", "\\n")
            .replace("\t", "\\t")
            .replaceAll("[\\x00-\\x1F\\x7F]", "");  // 移除所有控制字符
    }

    /**
     * 安全记录用户输入
     */
    public static void safeLog(Logger log, String userInput) {
        log.info("用户输入: {}", sanitize(userInput));
    }
}

// 使用Log4j2的%encode或%replace布局
// <PatternLayout pattern="%d %p %c - %replace{%msg}{[\r\n]}{}%n"/>

日志文件权限控制

java 复制代码
/**
 * 日志文件安全配置
 */
public class LogFileSecurity {

    public static void secureLogFile(String logPath) throws IOException {
        Path path = Paths.get(logPath);
        // 设置文件权限:仅所有者可读写
        Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rw-------");
        Files.setPosixFilePermissions(path, perms);
    }

    // 生产环境建议:
    // 1. 日志目录权限: 750 (rwxr-x---)
    // 2. 日志文件权限: 640 (rw-r-----)
    // 3. 日志文件属主: 应用运行用户
    // 4. 禁止日志目录被Web服务器直接访问
    // 5. 日志传输使用加密通道(TLS)
}

最佳实践

编码规范总结

java 复制代码
/**
 * 日志编码最佳实践汇总
 */
public class LoggingBestPractices {

    // 1. Logger声明:static final,类级别
    private static final Logger log = LoggerFactory.getLogger(LoggingBestPractices.class);

    public void bestPractices() {

        // 2. 使用占位符,不用字符串拼接
        log.info("userId={}", userId);           // 正确
        // log.info("userId=" + userId);         // 错误

        // 3. 异常日志必须包含堆栈(Throwable作为最后一个参数)
        try {
            riskyOperation();
        } catch (Exception e) {
            log.error("操作失败, param={}", param, e);  // 正确:包含异常对象
            // log.error("操作失败: " + e.getMessage()); // 错误:丢失堆栈
        }

        // 4. 不要记录敏感信息
        // log.info("用户密码: {}", password);  // 绝对禁止
        log.info("用户登录: userId={}", userId);  // 正确

        // 5. 避免在循环中记录日志
        // for (Item item : items) {
        //     log.debug("item: {}", item);     // 错误:可能产生海量日志
        // }
        log.debug("处理完成, count={}", items.size());  // 正确

        // 6. 条件判断保护昂贵操作
        if (log.isTraceEnabled()) {
            log.trace("完整请求体: {}", serializeFullRequest());
        }

        // 7. 使用有意义的日志消息
        log.info("订单状态变更: orderId={}, from={}, to={}",
            orderId, oldStatus, newStatus);  // 正确
        // log.info("状态变了");               // 错误:无上下文

        // 8. 一个异常只记录一次
        try {
            serviceA.call();
        } catch (ServiceAException e) {
            log.error("ServiceA调用失败", e);  // 在此记录
            throw new BusinessException("处理失败");  // 向上抛出时不再记录
        }
    }

    // 9. 为不同业务使用不同Logger
    private static final Logger AUDIT_LOG = LoggerFactory.getLogger("AUDIT");
    private static final Logger METRIC_LOG = LoggerFactory.getLogger("METRIC");

    public void audit(String action, String operator) {
        AUDIT_LOG.info("action={}, operator={}, time={}",
            action, operator, Instant.now());
    }
}

生产环境配置建议

yaml 复制代码
# Spring Boot生产环境日志配置
logging:
  level:
    root: INFO
    com.example: INFO
    org.springframework: WARN
    org.hibernate: WARN
    com.zaxxer.hikari: WARN
  file:
    name: /var/log/app/application.log
  logback:
    rollingpolicy:
      max-file-size: 100MB
      max-history: 30
      total-size-cap: 5GB
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%X{traceId:-}] - %msg%n"
    file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} [%X{traceId:-}] - %msg%n"

日志框架选型决策

复制代码
是否需要高性能异步?
├── 是 → Log4j2 + Disruptor
│        适用:高并发网关、交易系统
└── 否 → 是否使用Spring Boot?
         ├── 是 → Logback(默认,零配置)
         │        适用:大多数业务系统
         └── 否 → 是否有外部依赖限制?
                  ├── 是 → JUL(JDK内置)
                  │        适用:SDK、工具库
                  └── 否 → SLF4J + Logback
                           适用:通用Java应用

常见问题排查

问题 原因 解决方案
SLF4J: Failed to load class StaticLoggerBinder 缺少实现依赖 添加logback-classic或log4j-slf4j-impl
SLF4J: Class path contains multiple SLF4J bindings 多个实现冲突 排除多余依赖,保留一个实现
日志不输出 级别设置过高 检查logger级别和appender级别
MDC值为空 异步线程未传递 使用TaskDecorator或TTL
日志文件不滚动 权限或路径问题 检查目录写权限和磁盘空间
日志性能差 同步写入+频繁IO 启用异步Appender,增大buffer
Log4j2安全漏洞 版本过低 升级到2.17.1+,禁用JNDI lookup
相关推荐
Herbert_hwt1 小时前
【C语言基础】常量、选择结构与运算符全解析
c语言·开发语言·算法
AI视觉网奇1 小时前
动作识别 视频理解大模型
开发语言·python·音视频
Suxing92 小时前
C语言基础分享:从“租房”到“拆迁”,C语言动态内存管理
java·数据结构·算法
减瓦2 小时前
别让 Java 版本选择变成生产事故
java
ikun_文2 小时前
Java集合框架Collection拓展进阶
java·java ee
SomeB1oody2 小时前
【RustyML入门】3.8. 正则化与归一化层
开发语言·后端·机器学习·rust·教程
__zRainy__2 小时前
Node系列 · Node基础:全局变量与全局对象
开发语言·前端·javascript
码匠许师傅2 小时前
【C++ 面试真题】聊聊 C++ 的虚函数和多态
java·c++·面试
chuan.bai2 小时前
Java RAG 实战(第 8 篇):Spring Boot RAG 查询 API
java·spring boot·贪心算法