日誌不僅是除錯工具,更是系統可觀測性的基石。不當的日誌級別、格式、內容會嚴重拖垮效能,甚至洩露敏感資訊。本文整理自國外優秀技術文章並結合實戰經驗,系統闡述 Java 日誌的 10 大核心技巧、效能優化陷阱、SLF4J 與 Log4j2 最佳配置模式,助你建立高效、安全、可維運的日誌體系。
System.out.println() 僅能輸出到控制台,缺乏級別控制、非同步寫入、滾動策略、結構化輸出等生產級能力。專業框架(Log4j2、Logback、java.util.logging)提供:
| 級別 | 適用場景 | 生產環境建議 |
|---|---|---|
| TRACE | 極細粒度追蹤(變數值、迴圈計數) | 關閉 |
| DEBUG | 開發/測試環境診斷邏輯流向 | 關閉(極少數核心模組可開啟) |
| INFO | 關鍵業務節點:服務啟動、請求入口、外部呼叫、狀態變更 | 開啟 |
| WARN | 潛在問題:重試、降級、配置缺失、接近閾值 | 開啟 |
| ERROR | 業務失效、異常捕獲、資料不一致 | 開啟(含堆疊追蹤) |
| FATAL | 進程即將崩潰、不可恢復錯誤 | 開啟 |
鐵律:生產環境嚴禁開啟 DEBUG 級別。曾見證股票交易系統因 DEBUG 大量字串拼接導致延遲飆升 200ms,直接造成訂單超時。
// 錯誤:無論級別,執行字串拼接
logger.debug("Processing order " + orderId + " for user " + userId);
// 正確:僅在 DEBUG 級別啟用時才格式化
logger.debug("Processing order {} for user {}", orderId, userId);
// SLF4J/Log4j2 佔位符語法:{} 避免 toString() 開銷
原理:isDebugEnabled() 內部檢查級別,參數化版本延遲格式化至實際寫入時刻。
if (logger.isDebugEnabled()) {
logger.debug("Cache snapshot: {}", expensiveToStringCache());
}
適用於參數構造成本高(大物件序列化、集合遍歷)的場景。
// Log4j2 JSON Template Layout 或 Logstash Encoder
logger.info("Order placed", kv("orderId", id), kv("amount", amt), kv("userId", uid));
輸出 JSON:{"level":"INFO","msg":"Order placed","orderId":123,"amount":99.9,"userId":456},直接接入 ELK/Loki,免 Grok 解析。
logger.info("[DB] Slow query detected: {}ms", duration);
logger.warn("[SESSION] Cache miss for key: {}", key);
logger.error("[CLIENT] Downstream timeout: {}", url);
統一前綴 [MODULE],配合 Kibana/Logtail 欄位提取,秒級定位問題域。
<!-- Log4j2 AsyncLogger 全域非同步(需 Disruptor 依賴) -->
<Configuration status="WARN">
<Appenders>
<RollingFile name="File" fileName="logs/app.log" filePattern="logs/$${date:yyyy-MM}/app-%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
<Policies><TimeBasedTriggeringPolicy/><SizeBasedTriggeringPolicy size="100MB"/></Policies>
</RollingFile>
</Appenders>
<Loggers>
<AsyncRoot level="INFO"><AppenderRef ref="File"/></AsyncRoot>
</Loggers>
</Configuration>
高併發系統必開 AsyncRoot 或 AsyncLogger,搭配 LMAX Disruptor 無鎖環形緩衝區,延遲微秒級。
monitorInterval="30" 自動重載配置;JMX/JConsole 動態修改 Logger 級別POST /actuator/loggers/com.example {"configuredLevel":"DEBUG"}// 自定義 Layout/Converter 脫敏
logger.info("User login: username={}, password=***MASKED***", username);
或使用 Log4j2 RegexReplacement 全域脫敏規則。
// 差:logger.info("Loading config");
// 好:logger.info("Config file not found: {}, fallback to default", path);
包含條件判斷結果、備選方案、影響範圍,事後複盤無需看程式碼即可還原決策鏈。
try { ... }
catch (Exception e) {
logger.error("Failed to process order {}: {}", orderId, e.getMessage(), e); // 最後傳 e 保證堆疊
}
避免 logger.error(e) 僅輸出訊息丟失堆疊;避免 e.printStackTrace() 竄改標準錯誤流。
// Filter/Interceptor 產生或傳遞 traceId
MDC.put("traceId", traceId);
logger.info("Request received"); // 自動攜帶 traceId
// 下游微服務透過 Header 傳遞 traceId
配合 Sleuth/Zipkin/OpenTelemetry 實現全鏈路可觀測。
slf4j-api,部署時綁定實現,保持遷移彈性。<Configuration monitorInterval="30">
<Properties>
<Property name="LOG_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="${LOG_PATTERN}"/></Console>
<RollingFile name="File" fileName="logs/app.log" filePattern="logs/app-%d{yyyy-MM-dd}.log.gz">
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies><TimeBasedTriggeringPolicy interval="1" modulate="true"/></Policies>
<DefaultRolloverStrategy max="30"/>
</RollingFile>
</Appenders>
<Loggers>
<AsyncRoot level="INFO"><AppenderRef ref="Console"/><AppenderRef ref="File"/></AsyncRoot>
<AsyncLogger name="com.example.dao" level="DEBUG" additivity="false"><AppenderRef ref="File"/></AsyncLogger>
</Loggers>
</Configuration>
優秀的日誌體系具備:級別精準、格式結構化、輸出非同步、敏感脫敏、關聯貫穿、動態可調。將日誌視為一等公民納入 Code Review、測試驗證、上線清單,能為系統全生命週期省下無數排查工時。記住:沒有日誌是盲目,亂七八糟的日誌是噪音,精準結構化的日誌才是資產。