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)
核心接口
// 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获取方式
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
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标记
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
<!-- Maven依赖 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.14</version>
</dependency>
<!-- logback-classic 自动引入 logback-core 和 slf4j-api -->
// 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
<!-- 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>
// 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)
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绑定原理
// 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绑定原理
// 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();
}
}
桥接器使用
<!-- 将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 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 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全异步模式配置
# JVM启动参数启用全异步(性能最优)
-DLog4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
# Disruptor RingBuffer大小(默认256*1024)
-DAsyncLogger.RingBufferSize=524288
# 等待策略
-DAsyncLogger.WaitStrategy=Busyspin
日志级别(TRACE/DEBUG/INFO/WARN/ERROR)
级别定义与使用场景
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);
}
}
级别过滤规则
// 级别从低到高: 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
组件关系
// Logger: 日志记录器,负责接收日志事件
// Appender: 输出目的地,决定日志写到哪里
// Layout/Encoder: 格式化,决定日志长什么样
// Filter: 过滤器,决定哪些日志通过
// 一个Logger可以关联多个Appender
// 一个Appender包含一个Layout和多个Filter
自定义Appender
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
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)
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)
基本使用
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
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传递
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
<!-- 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异步
// 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滚动策略
<!-- 按时间+大小滚动 -->
<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滚动策略
<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输出
<!-- 依赖: 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日志输出示例
{
"@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
}
编程式结构化日志
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)
<!-- 依赖 -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<dependency>
<groupId>io.zipkin.reporter2</groupId>
<artifactId>zipkin-reporter-brave</artifactId>
</dependency>
# 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
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集成
// 使用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();
}
}
}
日志性能优化
避免字符串拼接
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();
}
}
批量日志与采样
/**
* 高频日志采样输出
*/
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参数优化
# Log4j2 Garbage-Free模式(避免日志对象分配)
-Dlog4j2.enableThreadlocals=true
-Dlog4j2.enableDirectEncoders=true
-Dlog4j2.garbagefreeThreadContextMap=true
# 异步日志RingBuffer预分配
-DAsyncLogger.RingBufferSize=262144
-DAsyncLogger.WaitStrategy=Yield
# Logback异步队列预热
-Dlogback.asyncAppender.queueSize=4096
日志安全(脱敏/注入防护)
敏感信息脱敏
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;
}
}
日志注入防护
/**
* 防止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"/>
日志文件权限控制
/**
* 日志文件安全配置
*/
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)
}
最佳实践
编码规范总结
/**
* 日志编码最佳实践汇总
*/
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());
}
}
生产环境配置建议
# 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 |