摘要
本文主要介紹了阿里巴巴編碼規范中關于日志處理的相關實踐解析。強調了使用日志框架(如 SLF4J、JCL)而非直接使用日志系統(如 Log4j、Logback)的 API 的重要性,包括解耦日志實現、統一日志調用方式等好處。同時,還涉及了日志文件的保存規范、擴展日志的命名方式、日志輸出時字符串拼接的占位符方式、日志級別的開關判斷以及避免重復打印日志等多方面的內容,旨在提升日志系統的可維護性、性能和合規性。
1. 【強制】應用中不可直接使用日志系統(Log4j、Logback)中的 API,而應依賴使用日志框架(SLF4J、JCL—Jakarta Commons Logging)中的 API,使用門面模式的日志框架,有利于維護和各個類的日志處理方式統一。
說明:日志框架(SLF4J、JCL--Jakarta Commons Logging)的使用方式(推薦使用 SLF4J)
使用 SLF4J:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(Test.class);使用 JCL:
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
private static final Log log = LogFactory.getLog(Test.class);
這是面向接口編程思想在日志系統中的體現,使用**門面模式(Facade Pattern)**的日志框架如 SLF4J,可以將日志 API 與具體實現解耦。主要好處包括:
1.1. ? 解耦日志實現
- 直接使用
Log4j
或Logback
,代碼就“綁死”在某個實現上。 - 如果以后想從 Log4j 切換為 Logback,需要大規模修改代碼。
- 使用 SLF4J 接口編程,只需要更換依賴包即可,無需改業務代碼。
1.2. ? 日志調用方式統一
- 所有類都用統一的 API,比如
LoggerFactory.getLogger(...)
。 - 日志格式、等級統一,便于維護和查錯。
1.3. ? 錯誤示例(直接使用 Log4j)
import org.apache.log4j.Logger;public class UserService {private static final Logger logger = Logger.getLogger(UserService.class);public void createUser() {logger.info("創建用戶...");}
}
如果以后換用 Logback,就得改成用 ch.qos.logback 的類,改動大。
1.4. ? 正確示例(使用 SLF4J)
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;public class UserService {private static final Logger logger = LoggerFactory.getLogger(UserService.class);public void createUser() {logger.info("創建用戶...");}
}
🔧 此時你在 pom.xml
中引入:
<dependency><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId>
</dependency>
<dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId>
</dependency>
將來你想換成 Log4j 也很簡單,只需換 Log4j 的綁定依賴即可,無需改業務代碼。
2. 【強制】日志文件至少保存 15 天,因為有些異常具備以“周”為頻次發生的特點。對于當天日志,以“應用名.log”來保存,保存在/{統一目錄}/{應用名}/logs/目錄下,過往日志格式為:{logname}.log.{保存日期},日期格式:yyyy-MM-dd
2.1. 日志保留周期要求
“日志文件至少保存 15 天”
- 有些問題并非每天都發生,而是按周循環出現(如每周一的定時任務、周末批處理等);
- 如果日志只保留幾天,可能無法回溯歷史問題;
- 因此,強制日志保留至少 15 天,以便問題排查。
2.2. 當前日志文件
- 命名規則:
應用名.log
- 存儲路徑:
/{統一目錄}/{應用名}/logs/
例如:
/data/apps/user-service/logs/user-service.log
2.3. 歷史日志文件
- 命名規則:
{logname}.log.{保存日期}
- 日期格式:
yyyy-MM-dd
例如:
/data/apps/user-service/logs/user-service.log.2025-05-27
/data/apps/user-service/logs/user-service.log.2025-05-26
2.4. Logback 配置(以 Spring Boot 工程為例)
以下是一個使用 SLF4J + Logback,滿足該規范的配置片段:
2.4.1. logback-spring.xml
<configuration><!-- 定義日志目錄和應用名 --><property name="LOG_HOME" value="/data/apps/user-service/logs" /><property name="APP_NAME" value="user-service" /><appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"><!-- 當前日志文件 --><file>${LOG_HOME}/${APP_NAME}.log</file><rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><!-- 過往日志文件命名 --><fileNamePattern>${LOG_HOME}/${APP_NAME}.log.%d{yyyy-MM-dd}</fileNamePattern><!-- 保留歷史日志天數 --><maxHistory>15</maxHistory></rollingPolicy><encoder><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern></encoder></appender><root level="INFO"><appender-ref ref="FILE"/></root></configuration>
3. 【強制】根據國家法律,網絡運行狀態、網絡安全事件、個人敏感信息操作等相關記錄,留存的日志不少于六個月,并且進行網絡多機備份。
3.1. 合規性要求(《網絡安全法》第21條等)
國家法律要求對“網絡運行、網絡安全事件、用戶操作敏感數據”等日志至少留存6個月。
必須記錄以下行為,并保留:
- 網絡運行狀態(服務啟停、接口調用、異常狀態)
- 網絡安全事件(攻擊、入侵、漏洞、越權訪問)
- 個人敏感信息操作(查看、導出、修改用戶敏感數據等)
?? 否則將面臨罰款、吊銷許可證等法律責任。
3.2. 留存時間:不少于6個月
- 日志存儲不能只保留15天或一個月,而要長期歸檔保存6個月以上。
3.3. 多機備份要求(防單點失敗)
所謂“網絡多機備份”,是指:
- 日志不僅保存在本機,還應同步到另一臺機器或遠程日志服務器;
- 防止機器損壞或系統故障導致日志丟失。
3.4. 如何實現這項規范?(示例方案)
方案一:本地持久 + 多機遠程備份(推薦)
本地日志配置保留 180 天(Logback)
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><fileNamePattern>${LOG_HOME}/${APP_NAME}.log.%d{yyyy-MM-dd}</fileNamePattern><maxHistory>180</maxHistory> <!-- 保留180天 -->
</rollingPolicy>
使用 rsync / scp / 日志采集工具進行多機備份
- 定期同步本地日志到遠程備份機(如每小時同步一次):
rsync -az /data/apps/user-service/logs/ logserver:/backup/user-service/
或使用 ELK/EFK 等集中采集日志:
- Filebeat + Elasticsearch + Kibana
- Flume + HDFS
- Kafka + Logstash
方案二:Spring Boot + ELK 日志采集方案
- 使用 Filebeat 收集本地日志
- 發往 Logstash → Elasticsearch
- 設置 Elasticsearch 索引生命周期(ILM)策略,保留180天日志
- Kibana 可視化查詢、安全審計
要求項 | 解釋 | 實現方式 |
保留日志時間 ≥6個月 | 符合國家《網絡安全法》《等級保護2.0》要求 | 日志文件保留180天或存入長期歸檔系統(如HDFS、ES) |
多機備份 | 避免日志因故障丟失 | rsync/rsyslog/Filebeat → 日志服務器 |
記錄重點內容 | 網絡運行、異常事件、敏感信息操作 | 通過埋點/日志攔截記錄操作 |
4. 【強制】應用中的擴展日志(如打點、臨時監控、訪問日志等) 命名方式:appName_logType_logName.log。logType:日志類型,如 stats / monitor / access 等;logName:日志描述。這種命名的好處:通過文件名就可知道日志文件屬于什么應用,什么類型,什么目的,也有利于歸類查找。
說明:推薦對日志進行分類,將錯誤日志和業務日志分開放,便于開發人員查看,也便于通過日志對系統進行及時監控。
正例:mppserver 應用中單獨監控時區轉換異常,如:mppserver_monitor_timeZoneConvert.log
擴展日志必須使用統一規范的命名格式,以提高可讀性、可分類性與可運維性。
4.1. 如何理解這條規則?
在實際開發中,我們的系統往往輸出多種不同目的的日志,比如:
類型 | 示例內容 |
訪問日志 | 用戶訪問接口的信息 |
監控日志 | 系統關鍵指標、性能監控等 |
打點日志 | 埋點數據、用戶行為路徑 |
業務操作日志 | 某個業務流程的處理記錄 |
錯誤日志 | 異常堆棧、錯誤信息 |
這些日志如果都輸出到一個文件中,就會:
- 不便查找
- 不利于自動監控
- 日志量爆炸,影響性能
解決辦法:分類輸出日志,并采用統一命名規范
命名規則:
appName_logType_logName.log
部分 | 說明 | 示例 |
appName | 應用名 |
|
logType | 日志類型(如 access / stats / monitor) |
|
logName | 日志內容描述(模塊或業務名稱) |
|
好處:
- 文件名一看就知道日志內容,方便開發 & 運維;
- 日志文件容易歸類,便于定向排查、自動告警等;
- 可以設置不同的日志滾動策略與等級。
正例示例分析
mppserver_monitor_timeZoneConvert.log
含義如下:
部分 | 含義 |
mppserver | 應用名 |
monitor | 日志類型:監控日志 |
timeZoneConvert | 日志主題:時區轉換相關 |
這個日志就可能記錄了:
[INFO] 2025-05-27 10:00:01 時區轉換失敗,源=GMT+8,目標=UTC+1,用戶ID=123
4.2. 日志分類輸出示例(以 Logback 為例)
logback-spring.xml
示例配置:
<property name="LOG_PATH" value="/data/apps/mppserver/logs"/><!-- 監控日志 -->
<appender name="MONITOR_TIMEZONE" class="ch.qos.logback.core.rolling.RollingFileAppender"><file>${LOG_PATH}/mppserver_monitor_timeZoneConvert.log</file><rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><fileNamePattern>${LOG_PATH}/mppserver_monitor_timeZoneConvert.log.%d{yyyy-MM-dd}</fileNamePattern><maxHistory>15</maxHistory></rollingPolicy><encoder><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level - %msg%n</pattern></encoder>
</appender><!-- 訪問日志 -->
<appender name="ACCESS_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender"><file>${LOG_PATH}/mppserver_access_gateway.log</file><rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><fileNamePattern>${LOG_PATH}/mppserver_access_gateway.log.%d{yyyy-MM-dd}</fileNamePattern><maxHistory>15</maxHistory></rollingPolicy><encoder><pattern>%msg%n</pattern></encoder>
</appender><!-- 日志分類寫入 -->
<logger name="com.example.monitor.TimeZoneService" level="INFO" additivity="false"><appender-ref ref="MONITOR_TIMEZONE"/>
</logger><logger name="com.example.gateway.AccessLogger" level="INFO" additivity="false"><appender-ref ref="ACCESS_LOG"/>
</logger>
5. 【強制】在日志輸出時,字符串變量之間的拼接使用占位符的方式。
說明:因為 String 字符串的拼接會使用 StringBuilder 的 append() 方式,有一定的性能損耗。使用占位符僅是替換動作,可以有效提升性能。
正例:logger.debug("Processing trade with id : {} and symbol : {}", id, symbol);
5.1. ?? 避免不必要的字符串拼接開銷
假設我們使用拼接方式:
logger.debug("Processing trade with id: " + id + " and symbol: " + symbol);
即使當前日志級別是 INFO
,不會真正輸出這條 DEBUG
日志,但拼接操作仍會執行:
String s = "Processing trade with id: " + id + " and symbol: " + symbol;
// 實際生成一個新的 String 對象,性能浪費
這在高并發或大量日志打印場景下性能損耗非常明顯。
5.2. ?? 占位符方式性能更優
SLF4J / Log4j 等日志門面在內部做了優化,只有當對應日志級別開啟時才會替換 {}
:
logger.debug("Processing trade with id: {} and symbol: {}", id, symbol);
- 如果
DEBUG
級別關閉,連字符串拼接都不會做; - 性能更優,垃圾更少(無多余 StringBuilder 創建);
5.3. ? 正確與錯誤用法對比
? 錯誤用法 | ? 正確用法 |
|
|
|
|
5.4. ? SLF4J 占位符說明
logger.info("User {} logged in from IP {}", username, ip);
{}
是占位符,不需要寫成{0}
,{1}
;- 變量順序一一對應;
- 也可以傳數組或異常對象:
logger.error("Request failed: {}", e.getMessage(), e); // 可打印異常棧
5.5. ? 附加示例:錯誤與業務日志對比
String orderId = "ORD123";
String product = "Camera";
BigDecimal price = new BigDecimal("1999.00");// ? 錯誤方式(始終拼接)
logger.debug("Creating order: " + orderId + ", product=" + product + ", price=" + price);// ? 推薦方式
logger.debug("Creating order: {}, product={}, price={}", orderId, product, price);
6. 【強制】對于 trace / debug / info 級別的日志輸出,必須進行日志級別的開關判斷:
說明:雖然在 debug(參數) 的方法體內第一行代碼 isDisabled(Level.DEBUG_INT) 為真時(Slf4j 的常見實現 Log4j 和Logback) , 就直接 return, 但是參數可能會進行字符串拼接運算。 此外, 如果 debug(getName()) 這種參數內有getName() 方法調用,無謂浪費方法調用的開銷。
正例:
// 如果判斷為真,那么可以輸出 trace 和 debug 級別的日志
if (logger.isDebugEnabled()) {
logger.debug("Current ID is: {} and name is: {}", id, getName());
}
6.1. 為什么要加 logger.isDebugEnabled()
判斷?
防止不必要的函數調用和拼接操作,即使我們使用了占位符 {}
,但傳參中包含方法調用或對象構造時,這些操作仍然會執行:
6.2. ? 示例(不加判斷):
logger.debug("Current ID is: {} and name is: {}", id, getName());
- 即使
DEBUG
日志關閉了, getName()
這個函數還是會執行,可能造成性能浪費或副作用!
6.3. ? 示例(加判斷):
if (logger.isDebugEnabled()) {logger.debug("Current ID is: {} and name is: {}", id, getName());
}
- 如果日志級別關閉,整個代碼塊不會執行
- 避免無謂函數調用,提高性能
6.4. 有些方法計算成本高或可能拋異常
舉個例子:
logger.debug("Big JSON result: {}", toJSONString(largeObject));
toJSONString()
比較耗時;- 如果 DEBUG 沒開啟,這個方法白執行了;
- 有可能還拋異常,影響主流程!
這時候最好加判斷:if (logger.isDebugEnabled())
6.5. 正確寫法示例
if (logger.isDebugEnabled()) {logger.debug("Current ID is: {} and name is: {}", id, getName());
}
如果 getName()
是一個代價比較高的方法,或者日志中拼接了龐大的對象(如 Map、JSON),建議使用這種寫法。
6.6. 其他級別也適用
日志級別 | 判斷方法 | 適用場景 |
|
| 最低級別,性能敏感 |
|
| 開發調試時大量使用 |
| 一般不加判斷(輕量) | 可省略 |
| 通常不加判斷 | 可省略 |
| 不需要判斷 | 永遠輸出 |
7. 【強制】避免重復打印日志,浪費磁盤空間,務必在日志配置文件中設置 additivity=false
正例:<logger name="com.taobao.dubbo.config" additivity="false">
7.1. 如何理解 additivity=false
?
7.1.1. 📌 additivity
是什么?
在日志系統(如 Logback、Log4j)中,logger
是有層級結構的,例如:
com└── taobao└── dubbo└── config
- 每個層級的 logger 默認 會把日志向上傳遞 到父 logger(這叫 "additivity")。
- 如果不禁止傳遞(即
additivity=true
,默認值),那么日志可能被父 logger 重復處理并輸出。
7.1.2. ? 問題示例:重復日志打印
你配置了兩個 logger:
<logger name="com.taobao.dubbo.config"><appender-ref ref="A1"/>
</logger><root><appender-ref ref="A2"/>
</root>
如果 additivity=true
(默認):
com.taobao.dubbo.config
的日志:
-
- 會被
A1
打一次 - 然后“冒泡”到 root,被
A2
再打一次 ?
- 會被
7.1.3. 🔁 結果:日志被打印兩遍,占用兩倍磁盤空間!
7.2. ? 正確做法:設置 additivity="false"
<logger name="com.taobao.dubbo.config" additivity="false"><level value="INFO"/><appender-ref ref="A1"/>
</logger>
這樣:
- 日志只輸出一次到
A1
; - 不會再上傳給父 logger(如 root);
- ? 減少重復、避免浪費磁盤。
7.3. 實際示例(Logback)完整配置片段
<configuration><appender name="DUBBO_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender"><file>/app/logs/dubbo.log</file><rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><fileNamePattern>/app/logs/dubbo.log.%d{yyyy-MM-dd}</fileNamePattern><maxHistory>15</maxHistory></rollingPolicy><encoder><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern></encoder></appender><!-- 👇 防止日志向上傳遞、重復輸出 --><logger name="com.taobao.dubbo.config" level="INFO" additivity="false"><appender-ref ref="DUBBO_LOG"/></logger><!-- 根日志,輸出系統其他日志 --><root level="INFO"><appender-ref ref="CONSOLE"/></root>
</configuration>
項目 | 內容 |
🔧 設置項 |
|
📌 功能說明 | 防止日志上傳父 logger,重復打印 |
🚫 如果不加 | 日志可能被打印多次,占磁盤、擾亂分析 |
? 推薦寫法 | 任何定義了 appender 的子 logger,都應顯式設置 |
8. 優秀的Spring項目中日志分類應該是怎么樣?Logback配置文件應該是怎么樣設計?
在一個良好結構化的 Java Spring項目 中,日志分類和 Logback配置應當遵循可讀性、可維護性、按模塊分類、可定位問題、環境適配幾個核心原則。
8.1. ? 日志分類建議(按職責和層級)
通常可以按照以下分類方式命名 logger,并做等級管理:
類別/層 | 包路徑示例 | log level 建議 | 說明 |
Controller 層 |
| INFO/WARN | 記錄接口訪問、參數、響應耗時等 |
Service 層 |
| INFO/DEBUG | 業務核心邏輯,建議包含調用鏈信息 |
DAO 層 |
| DEBUG | 數據庫操作,調試使用 |
異常處理層 |
| ERROR | 異常堆棧、關鍵異常處理 |
第三方調用層 |
| INFO/ERROR | 外部服務接口日志 |
定時任務 |
| INFO/DEBUG | 定時調度相關日志 |
通用工具類 |
| WARN/DEBUG | 工具類、通用組件 |
框架組件日志 |
| WARN | Spring 框架日志 |
數據源、MyBatis |
| WARN/INFO | 數據源和持久層日志 |
8.2. ? Logback 配置文件標準示例(logback-spring.xml)
這是一個功能齊全、分模塊控制、環境切換靈活的樣板:
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="30 seconds"><property name="LOG_HOME" value="${LOG_HOME:-logs}"/><property name="APP_NAME" value="${spring.application.name:-app}"/><property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/><property name="LOG_LEVEL" value="INFO"/><!-- 控制臺輸出 --><appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"><encoder><pattern>${LOG_PATTERN}</pattern></encoder></appender><!-- 按天滾動的文件輸出 --><appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"><file>${LOG_HOME}/${APP_NAME}.log</file><rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><fileNamePattern>${LOG_HOME}/${APP_NAME}.%d{yyyy-MM-dd}.log</fileNamePattern><maxHistory>15</maxHistory></rollingPolicy><encoder><pattern>${LOG_PATTERN}</pattern></encoder></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><!-- Spring、MyBatis、SQL 等默認組件日志 --><logger name="org.springframework" level="WARN"additivity="false"><appender-ref ref="CONSOLE"/><appender-ref ref="ASYNC_FILE"/></logger><logger name="org.mybatis" level="WARN" additivity="false"><appender-ref ref="CONSOLE"/><appender-ref ref="ASYNC_FILE"/></logger><logger name="com.zaxxer.hikari" level="WARN" additivity="false"><appender-ref ref="CONSOLE"/><appender-ref ref="ASYNC_FILE"/></logger><!-- 控制層日志,只記錄 INFO 及以上,輸出到 CONSOLE 和 ASYNC_FILE --><logger name="com.example.project.controller" level="INFO" additivity="false"><appender-ref ref="CONSOLE"/><appender-ref ref="ASYNC_FILE"/></logger><!-- 服務層日志,記錄 DEBUG 及以上,輸出到 CONSOLE 和 ASYNC_FILE --><logger name="com.example.project.service" level="DEBUG" additivity="false"><appender-ref ref="CONSOLE"/><appender-ref ref="ASYNC_FILE"/></logger><!-- 持久層日志,記錄 DEBUG 及以上,輸出到 ASYNC_FILE --><logger name="com.example.project.repository" level="DEBUG" additivity="false"><appender-ref ref="ASYNC_FILE"/></logger><!-- 錯誤處理模塊日志,只記錄 ERROR,輸出到 CONSOLE 和 ASYNC_FILE --><logger name="com.example.project.error" level="ERROR" additivity="false"><appender-ref ref="CONSOLE"/><appender-ref ref="ASYNC_FILE"/></logger><!-- 定時任務模塊日志,記錄 INFO 及以上 --><logger name="com.example.project.job" level="INFO" additivity="false"><appender-ref ref="ASYNC_FILE"/></logger><!-- 系統集成、三方接口模塊日志 --><logger name="com.example.project.integration" level="INFO" additivity="false"><appender-ref ref="ASYNC_FILE"/></logger><!-- 根日志配置 --><root level="${LOG_LEVEL}"><appender-ref ref="CONSOLE"/><appender-ref ref="ASYNC_FILE"/></root>
</configuration>
8.3. ? 附加建議
8.3.1. 按環境區分日志配置(Spring Profiles)
<springProfile name="dev"><logger name="com.example.project" level="DEBUG"/>
</springProfile><springProfile name="prod"><logger name="com.example.project" level="INFO"/>
</springProfile>
8.3.2. 使用 MDC 實現鏈路追蹤(如 traceId)
在 filter 中設置:
MDC.put("traceId", UUID.randomUUID().toString());
在 logback pattern 中使用:
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level [%X{traceId}] %logger - %msg%n</pattern>
8.4. ? 總結
優秀實踐 | 說明 |
按模塊分 logger | 易于查找、屏蔽某一類日志 |
使用 AsyncAppender | 避免 I/O 阻塞,性能更好 |
使用 MDC + traceId | 日志鏈路追蹤 |
環境敏感日志級別 | 開發 debug,生產 info |
保留最近 N 天日志 | 利于問題追溯 |
不要用 | 統一日志管理 |
9. 【強制】生產環境禁止使用 System.out 或 System.err 輸出或使用 e.printStackTrace() 打印異常堆棧。
說明:標準日志輸出與標準錯誤輸出文件每次 Jboss 重啟時才滾動,如果大量輸出送往這兩個文件,容易造成文件大小超過操作系統大小限制。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;public class ExampleService {private static final Logger logger = LoggerFactory.getLogger(ExampleService.class);public void doSomething() {try {// 業務邏輯...} catch (Exception e) {// ? 錯誤做法:e.printStackTrace();// System.out.println("出現異常:" + e.getMessage());// ? 推薦做法:logger.error("業務處理失敗", e);}}
}
使用 logger.error("xxx", e)
輸出異常,有以下優勢:
- 自動打印完整堆棧;
- 日志等級明確(ERROR);
- 包含上下文信息;
- 可配置輸出到不同文件或集中式日志系統(如 ELK、Loki);
- 避免信息泄露(通過脫敏配置);
- 支持異步寫入提高性能
10. 【強制】異常信息應該包括兩類信息:案發現場信息和異常堆棧信息。如果不處理,那么通過關鍵字throws 往上拋出。
正例:logger.error("inputParams: {} and errorMessage: {}", 各類參數或者對象 toString(), e.getMessage(), e);
這是一個非常重要的日志輸出規范要求,旨在保證出現異常時,日志中不僅有錯誤堆棧信息(異常是什么),還包括上下文信息(發生異常時系統在做什么),以便于問題排查和復現。理解說明:“案發現場” + “異常堆棧” = 有價值的異常日志,兩類信息:
信息類型 | 說明 | 目的 |
案發現場信息 | 方法入參、操作用戶、請求來源、處理上下文等 | 定位是哪個請求或數據導致的 |
異常堆棧信息 |
| 定位代碼具體出錯位置 |
10.1. 正例解讀
logger.error("inputParams: {} and errorMessage: {}", request.toString(), e.getMessage(), e);
這行日志做到了:
- {} 第一個參數:打印
request
的內容(案發現場)。 - {} 第二個參數:打印異常提示信息(便于快速識別異常類型)。
- 最后的
e
:打印完整異常堆棧。
日志最終可能打印成:
ERROR com.example.UserService - inputParams: UserRequest{id=1, name='張三'} and errorMessage: java.lang.NullPointerException: xx
java.lang.NullPointerException
at com.example.UserService.getUser(UserService.java:45)
at ...
10.2. ? 示例:推薦做法
public void handleRequest(UserRequest request) {try {// 業務處理} catch (Exception e) {logger.error("處理請求失敗,請求參數: {}, 異常原因: {}", request, e.getMessage(), e);throw new BusinessException("用戶處理失敗", e); // 或者繼續往上拋}
}
10.3. ? 反例:不包含上下文
catch (Exception e) {logger.error("出錯了", e); // 缺少關鍵參數信息
}
無法知道是哪一個請求、哪個參數導致錯誤,排查困難。
10.4. ? 再進階:統一異常處理(推薦)
如果你用 Spring Boot,可以統一用 @ControllerAdvice
把這些信息收集起來打日志:
@RestControllerAdvice
public class GlobalExceptionHandler {private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);@ExceptionHandler(Exception.class)public ResponseEntity<String> handleException(HttpServletRequest request, Exception e) {logger.error("請求地址: {}, 請求參數: {}, 異常信息: {}", request.getRequestURI(),request.getQueryString(), e.getMessage(), e);return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("系統異常");}
}
11. 【強制】日志打印時禁止直接用 JSON 工具將對象轉換成 String。
說明: 如果對象里某些 get 方法被覆寫, 存在拋出異常的情況,則可能會因為打印日志而影響正常業務流程的執行。正例:打印日志時僅打印出業務相關屬性值或者調用其對象的 toString() 方法。這是一個非常重要的日志打印規范,防止因為“日志打印本身”而引發系統異常。
規范理解:不要直接使用 JSON 工具(如 ObjectMapper
, Gson
, FastJson
)將對象序列化為字符串用于日志打印。
11.1. 原因:
- 有些對象的
getXxx()
方法被重寫,里面可能拋異常(例如懶加載未初始化、連接已關閉等); - JSON 工具在序列化時會自動調用所有 getter,如果其中某個拋異常,會打斷日志打印,甚至影響主業務流程。
11.2. ? 反例(違背規范)
// 錯誤做法:直接用 JSON 工具打印整個對象
logger.info("用戶信息: {}", objectMapper.writeValueAsString(user));
潛在風險:
- 如果
user.getAccountBalance()
內部操作數據庫連接,而連接已關閉,日志打印時會報錯; - 程序可能在日志階段拋異常,導致主流程中斷。
11.3. ? 正例(推薦做法)
- 調用對象的
toString()
(前提是實現良好) - 只打印業務關鍵字段
// 推薦方式 1:對象已有良好的 toString 實現
logger.info("用戶信息: {}", user.toString());// 推薦方式 2:只打印關鍵屬性
logger.info("用戶信息: id={}, name={}", user.getId(), user.getName());
12. 不建議使用 JSON.toJSONString(obj)
等類似方法直接打印日志
12.1. 為什么 JSON.toJSONString()
不推薦用于日志打印?
因會調用對象的所有 getter 方法
JSON.toJSONString(user)
這會自動遍歷對象屬性并執行 getXxx()
方法,而這些方法中:
- 可能含有業務邏輯;
- 可能訪問數據庫(如懶加載字段);
- 有些重寫的 getter 甚至會拋異常;
結果就是:🔥 日志打印行為影響業務執行流程,甚至導致程序異常。
12.2. 推薦做法
12.2.1. ? 方案 1:只打印關鍵字段
logger.info("userId={}, userName={}", user.getId(), user.getName());
12.2.2. ? 方案 2:使用toString()
,前提是你確認安全
logger.info("user info: {}", user.toString());
?? 注意:不要在 toString()
里調用會拋異常的方法。
12.3. ?舉個反例
logger.info("用戶信息:{}", JSON.toJSONString(user)); // ? 可能觸發懶加載/空指針異常
如果 user.getBalance()
是懶加載字段,沒初始化,打印時就會拋 LazyInitializationException
,程序可能因此掛掉。
13. 【推薦】為了保護用戶隱私,日志文件中的用戶敏感信息需要進行脫敏處理。
不要在日志中輸出敏感信息:姓名、身份證號、手機號、銀行卡號、地址、登錄密碼、驗證碼等。這些數據如果未脫敏就出現在日志中,一旦日志泄露就會導致用戶隱私泄露、觸發法律風險。
13.1. 推薦做法:敏感信息日志中要脫敏處理
信息類型 | 脫敏規則示例 |
手機號 | 136****1234 |
身份證號 | 110***********1234 |
姓名 | 王** |
銀行卡號 | 6227********3456 |
13.2. 正例代碼示例
User user = getUser();// 脫敏處理
String maskedPhone = DesensitizationUtil.maskPhone(user.getPhone());
String maskedIdCard = DesensitizationUtil.maskIdCard(user.getIdCard());logger.info("用戶信息 - userId: {}, phone: {}, idCard: {}", user.getId(), maskedPhone, maskedIdCard);
推薦在日志中使用 userId
、orderId
、uuid
等非敏感的唯一標識進行問題定位。
13.3. ? 反例代碼(絕對禁止)
logger.info("用戶信息 - 姓名: {}, 身份證: {}, 手機號: {}", user.getName(), user.getIdCard(), user.getPhone());
// 泄露完整敏感信息,嚴重違規
13.4. ? 推薦脫敏工具類 DesensitizationUtil
public class DesensitizationUtil {public static String maskPhone(String phone) {if (phone == null || phone.length() != 11) return phone;return phone.substring(0, 3) + "****" + phone.substring(7);}public static String maskIdCard(String idCard) {if (idCard == null || idCard.length() < 8) return idCard;return idCard.substring(0, 3) + "***********" + idCard.substring(idCard.length() - 4);}public static String maskName(String name) {if (name == null || name.length() < 2) return "*";return name.charAt(0) + "*".repeat(name.length() - 1);}
}
13.5. ? 日志中推薦使用哪些字段定位問題?
userId
/accountId
orderId
uuid
transactionId
requestId
(可作為鏈路跟蹤標識)
這些字段 既不包含用戶隱私,又能唯一定位問題。