本文基於 Log4j 2.x (現行主流版本,Log4j 1.x 已停止維護並存在 Log4Shell 漏洞)。配置文件格式為 log4j2.xml 或 log4j2.properties。
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" monitorInterval="30">
<Properties>
<Property name="LOG_DIR">logs</Property>
<Property name="PATTERN">%d{yyyy-MM-dd HH:mm:ss.SSS} %5p [%t] (%F:%L) - %m%n</Property>
</Properties>
<Appenders>
<!-- 控制台輸出 -->
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="${PATTERN}"/>
</Console>
<!-- 滾動文件:按大小/時間 -->
<RollingFile name="RollingFile" fileName="${LOG_DIR}/app.log"
filePattern="${LOG_DIR}/app-%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="${PATTERN}"/>
<Policies>
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
<SizeBasedTriggeringPolicy size="100 MB"/>
</Policies>
<DefaultRolloverStrategy max="30"/>
</RollingFile>
<!-- 錯誤專用文件 -->
<RollingFile name="ErrorFile" fileName="${LOG_DIR}/error.log"
filePattern="${LOG_DIR}/error-%d{yyyy-MM-dd}.log.gz">
<ThresholdFilter level="ERROR" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="${PATTERN}"/>
<Policies>
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
</Policies>
</RollingFile>
</Appenders>
<Loggers>
<!-- 根記錄器 -->
<Root level="INFO">
<AppenderRef ref="Console"/>
<AppenderRef ref="RollingFile"/>
<AppenderRef ref="ErrorFile"/>
</Root>
<!-- 特定包調試級別 -->
<Logger name="com.myapp.dao" level="DEBUG" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="RollingFile"/>
</Logger>
<!-- 第三方庫靜默 -->
<Logger name="org.hibernate" level="WARN"/>
<Logger name="org.springframework" level="WARN"/>
</Loggers>
</Configuration>
public class UserService {
// 類名自動作為 logger name,支援重構重命名
private static final Logger logger = LogManager.getLogger(UserService.class);
public void doSomething() {
logger.debug("Processing user {}", userId); // 佔位符避免字符串拼接開銷
logger.info("User {} logged in", username);
}
}
private static final Logger logger = LogManager.getLogger(MyClass.class);
// 錯誤:無論級別是否啟用,都執行字符串拼接
logger.debug("User " + name + " logged in at " + time);
// 正確:僅在 DEBUG 啟用時構造字符串
logger.debug("User {} logged in at {}", name, time);
try {
riskyOperation();
} catch (Exception e) {
// 錯誤:e.getMessage() 丟失堆棧
// logger.error("Failed: " + e.getMessage());
// 正確:傳入異常對象,自動記錄完整堆棧
logger.error("Operation failed for user {}", userId, e);
// 或明確指定訊息
logger.error("Database connection lost", e);
}
// 僅在 DEBUG 級別啟用時執行昂貴操作
logger.debug(() -> "Expensive dump: " + dumpLargeObject());
ThreadContext.put("traceId", traceId);
ThreadContext.put("userId", userId);
logger.info("Order placed"); // 自動包含上下文字段
ThreadContext.clearAll();
無需代碼,log4j2.xml 放在 classpath 根目錄即可自動載入。
@WebListener
public class Log4jInitializer implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
// 可編程式配置或驗證配置文件存在
System.setProperty("log4j.configurationFile", "classpath:log4j2.xml");
}
}
@WebListener
public class Log4jShutdown implements ServletContextListener {
@Override
public void contextDestroyed(ServletContextEvent sce) {
LogManager.shutdown(); // 關鍵:確保所有日誌寫入磁盤
}
}
| 問題 | 原因 | 解決 |
|---|---|---|
| 控制台無輸出 | 配置文件未生效/級別過高 | 檢查 classpath、status="DEBUG" 看內部日誌 |
| 文件不滾動 | Policy 配置錯誤/權限不足 | 檢查 filePattern 含 %i、目錄可寫 |
| 異常堆棧缺失 | 傳入字符串而非異常對象 | 用 logger.error(msg, e) 重載 |
| 內存洩漏 (ThreadContext) | 未清理 ThreadLocal | Filter/Interceptor 中 finally { ThreadContext.clearAll(); } |
| 類加載器洩漏 (Web 熱部署) | Logger 持有類引用 | 關閉時 LogManager.shutdown()、避免靜態 Logger 引用第三方類 |
<AsyncRoot level="INFO">
<AppenderRef ref="RollingFile"/>
</AsyncRoot>
<AsyncLogger name="com.myapp" level="DEBUG"/>
%F:%L (文件名:行號) 有額外開銷,生產環境可移除<RollingFile bufferSize="8192">BurstFilter 或采樣器log4j.properties → log4j2.xml (語法完全不同)Logger.getLogger() → LogManager.getLogger()RollingFileAppender → RollingFile (配置屬性名變更)log4j:log4j → org.apache.logging.log4j:log4j-api + log4j-corelog4j-1.2-api 兼容舊代碼調用 (不建議長期使用)Log4j 2.x 提供 異步高效能、豐富配置、完美異常堆棧、結構化日誌。核心要訣:佔位符參數化、異常對象直傳、異步根記錄器、優雅關閉。配置文件統一管理級別與輸出,代碼中零硬編碼配置,是企業級 Java 應用觀測性的基石。