Java開發經驗——阿里巴巴編碼規范實踐解析4

摘要

本文主要介紹了阿里巴巴編碼規范中關于日志處理的相關實踐解析。強調了使用日志框架(如 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. ? 解耦日志實現

  • 直接使用 Log4jLogback,代碼就“綁死”在某個實現上。
  • 如果以后想從 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 日志采集方案

  1. 使用 Filebeat 收集本地日志
  2. 發往 Logstash → Elasticsearch
  3. 設置 Elasticsearch 索引生命周期(ILM)策略,保留180天日志
  4. 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

應用名

mppserver

logType

日志類型(如 access / stats / monitor)

monitor

logName

日志內容描述(模塊或業務名稱)

timeZoneConvert

好處:

  • 文件名一看就知道日志內容,方便開發 & 運維;
  • 日志文件容易歸類,便于定向排查、自動告警等;
  • 可以設置不同的日志滾動策略與等級。

正例示例分析

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. ? 正確與錯誤用法對比

? 錯誤用法

? 正確用法

logger.info("User: " + username + " login success");

logger.info("User: {} login success", username);

logger.debug("Order total: " + total + ", discount: " + discount);

logger.debug("Order total: {}, discount: {}", total, discount);

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. 其他級別也適用

日志級別

判斷方法

適用場景

trace

logger.isTraceEnabled()

最低級別,性能敏感

debug

logger.isDebugEnabled()

開發調試時大量使用

info

一般不加判斷(輕量)

可省略

warn

通常不加判斷

可省略

error

不需要判斷

永遠輸出

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 ... additivity="false">

📌 功能說明

防止日志上傳父 logger,重復打印

🚫 如果不加

日志可能被打印多次,占磁盤、擾亂分析

? 推薦寫法

任何定義了 appender 的子 logger,都應顯式設置 additivity="false"

8. 優秀的Spring項目中日志分類應該是怎么樣?Logback配置文件應該是怎么樣設計?

在一個良好結構化的 Java Spring項目 中,日志分類和 Logback配置應當遵循可讀性、可維護性、按模塊分類、可定位問題、環境適配幾個核心原則。

8.1. ? 日志分類建議(按職責和層級)

通常可以按照以下分類方式命名 logger,并做等級管理:

類別/層

包路徑示例

log level 建議

說明

Controller 層

com.example.project.controller.*

INFO/WARN

記錄接口訪問、參數、響應耗時等

Service 層

com.example.project.service.*

INFO/DEBUG

業務核心邏輯,建議包含調用鏈信息

DAO 層

com.example.project.repository.*

DEBUG

數據庫操作,調試使用

異常處理層

com.example.project.error.*

ERROR

異常堆棧、關鍵異常處理

第三方調用層

com.example.project.integration.*

INFO/ERROR

外部服務接口日志

定時任務

com.example.project.job.*

INFO/DEBUG

定時調度相關日志

通用工具類

com.example.project.util.*

WARN/DEBUG

工具類、通用組件

框架組件日志

org.springframework.*

WARN

Spring 框架日志

數據源、MyBatis

com.zaxxer.hikari, 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 天日志

利于問題追溯

不要用 System.out.println()

統一日志管理

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);

這是一個非常重要的日志輸出規范要求,旨在保證出現異常時,日志中不僅有錯誤堆棧信息(異常是什么),還包括上下文信息(發生異常時系統在做什么),以便于問題排查和復現。理解說明:“案發現場” + “異常堆棧” = 有價值的異常日志,兩類信息:

信息類型

說明

目的

案發現場信息

方法入參、操作用戶、請求來源、處理上下文等

定位是哪個請求或數據導致的

異常堆棧信息

Exception 對象的堆棧

定位代碼具體出錯位置

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. ? 正例(推薦做法)

  1. 調用對象的 toString()(前提是實現良好)
  2. 只打印業務關鍵字段
// 推薦方式 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);

推薦在日志中使用 userIdorderIduuid 等非敏感的唯一標識進行問題定位。

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(可作為鏈路跟蹤標識)

這些字段 既不包含用戶隱私,又能唯一定位問題。

博文參考

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/82900.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/82900.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/82900.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

各個鏈接集合

golang學習&#xff5e;&#xff5e;_從數組中取一個相同大小的slice有成本嗎?-CSDN博客 框架 golang學習&#xff5e;&#xff5e;_從數組中取一個相同大小的slice有成本嗎?-CSDN博客 golang k8s學習_容器化部署和傳統部署區別-CSDN博客 K8S rabbitmq_rabbitmq 廣播-CSD…

Cesium 展示——獲取鼠標移動、點擊位置的幾種方法

文章目錄 需求分析:這里我們用到了幾種常見的鼠標事件1. 獲取鼠標移動的位置2. 獲取鼠標點擊的位置3. 添加面4. 示例代碼需求 獲取指定斷面的 label 分析:這里我們用到了幾種常見的鼠標事件 1. 獲取鼠標移動的位置 viewer.screenSpaceEventHandler.setInputAction((moveme…

技術分享 | Oracle SQL優化案例一則

本文為墨天輪數據庫管理服務團隊第70期技術分享&#xff0c;內容原創&#xff0c;作者為技術顧問馬奕璇&#xff0c;如需轉載請聯系小墨&#xff08;VX&#xff1a;modb666&#xff09;并注明來源。 一、問題概述 開發人員反映有條跑批語句在測試環境執行了很久都沒結束&…

$3 #12階段三小結Java se

$3 #12 階段三小結 Java se 基本沒有新學什么知識點 感覺 基礎語法 和高級語法 已經學完了 現在就是得學習 一些企業開發的框架 以及項目架構的思維 比如一個產品 從需求分析 到功能模塊設計 到接口文檔定義 數據庫建立 前端接口頁面設計 后端接口開發的步驟 然后現在比…

華為云Flexus+DeepSeek征文 | 初探華為云ModelArts Studio:部署DeepSeek-V3/R1商用服務的詳細步驟

華為云FlexusDeepSeek征文 | 初探華為云ModelArts Studio&#xff1a;部署DeepSeek-V3/R1商用服務的詳細步驟 前言一、華為云ModelArts Studio平臺介紹1.1 ModelArts Studio介紹1.2 ModelArts Studio主要特點1.3 ModelArts Studio使用場景1.4 ModelArts Studio產品架構 二、訪問…

易經六十四卦象解釋數據集分享!智能體知識庫收集~

今天給大家分享一個易經六十四卦象解釋數據集 &#xff0c;繼續來積累AI相關的資料。 六十四卦&#xff0c;記載于《易經》&#xff0c;每一卦的圖像均由兩個八卦上下組合而成&#xff0c;每一卦各有六個爻。南宋朱熹說&#xff0c;先畫八卦于內&#xff0c;后畫八卦于外&#…

1 μs = 10?? s

1 s 10? s 1 ms 10? s 1 s 10?? s 1 ns 10?? s 1 ps 10? s 1 fs 10?? s ?? 時間單位&#xff08;十進制&#xff09; 符號單位名稱10 的冪次s秒&#xff08;second&#xff09;10?ms毫秒&#xff08;millisecond&#xff09;10?s微秒&#xff08;microseco…

webrtc初了解

1. webrtc的簡介 一、WebRTC 是什么&#xff1f; Web Real-Time Communication&#xff08;網頁實時通信&#xff09;&#xff0c;是瀏覽器原生支持的實時音視頻通信技術&#xff0c;無需安裝插件或客戶端&#xff0c;可直接在瀏覽器之間實現點對點&#xff08;P2P&#xff09…

從數據持久化到網絡通信與OpenCV:Qt應用程序開發的深度探索與實戰

文章目錄 前言一、QSettings&#xff1a;輕量級數據持久化方案1.1 QSettings 主要特點1.2 QSettings 常用函數整理 二、數據庫2.1 連接SQLite數據庫2.2 建表2.3 增刪改 三、網絡編程3.1 網絡分層3.2 IP地址3.3 端口號3.4 基于TCP的Socket通信3.4 相關接口3.4.1核心類3.4.2 通信…

經典SQL查詢問題的練習第一天

首先有三張表&#xff0c;學生表、課程表、成績表 student:studentId,studentName; course:courseId&#xff0c;courseName,teacher; score:score,studentId,courseId; 接著有以下幾道題目&#xff1a; ①查詢課程編號為‘0006’的總成績&#xff1a; 首先總成績&#x…

企業級網絡管理實戰:Linux、云與容器的深度融合與優化

在數字化轉型浪潮下&#xff0c;企業網絡架構日益復雜&#xff0c;Linux系統、云計算與容器技術成為構建高效、靈活網絡的核心要素。本文將從技術原理、實踐方案、優化策略三個維度&#xff0c;深度解析企業級網絡管理中的關鍵技術&#xff0c;助力企業打造穩定、安全、可擴展的…

信號與系統速成-1.緒論

b站浙大教授雖然講的比較細&#xff0c;但是太慢了&#xff0c;不適合速成 祖師爺奧本海姆的MIT課程好像和我們教材的版本不太匹配&#xff0c;但是講的很不錯 慕課上也有很多資源&#xff0c;比如信號與系統 - 網易云課堂 同站博主籬笆外的xixi的文章也挺不錯 最終我還是選…

緩存架構方案:Caffeine + Redis 雙層緩存架構深度解析

在高并發、低延遲的現代互聯網系統中&#xff0c;緩存是提升系統性能和穩定性的重要手段。隨著業務復雜度的增長&#xff0c;單一緩存方案&#xff08;如僅使用Redis或僅使用本地緩存&#xff09;已難以滿足高性能與一致性需求。 本文將圍繞 Caffeine Redis 的雙層緩存架構展…

【Elasticsearch】track_total_hits

在 Elasticsearch 中&#xff0c;track_total_hits 是一個查詢參數&#xff0c;用于控制是否精確計算搜索結果的總命中數&#xff08;total hits&#xff09;。默認情況下&#xff0c;Elasticsearch 在某些情況下可能會對總命中數進行近似計算&#xff0c;以提高性能。track_to…

智能手機上用Termux安裝php+Nginx

Termux的官方網站&#xff1a;Termux | The main termux site and help pages. 以下是在 Termux 上安裝和配置 PHP Nginx 的完整流程總結&#xff0c;包含關鍵步驟和命令&#xff1a; 一、安裝依賴 pkg update && pkg upgrade # 更新包列表和系統pkg install nginx p…

電腦開機后出現bootmgr is conmpressed原因及解決方法

最近有網友問我為什么我電腦開機后出現BOOTMGR is compressed&#xff0c;這個提示意思是:意思是啟動管理器被壓縮了&#xff0c;即使重啟也無法正常進入系統。原因有很多&#xff0c;大部分是引導出現問題&#xff0c;或選錯了啟動硬盤所導致的&#xff0c;下面我們來詳細分析…

服務發現Nacos

目錄 Nacos server 安裝 注冊服務到Nacos server 接口訪問Nacos server中的已注冊服務 Nacos控制臺介紹 Nacos:一個更易于構建云原生應用的動態服務發現、配置管理和服務管理平臺。 在分布式服務應用中&#xff0c;各類服務需要統一的注冊、統一的管理&#xff0c;這個組件工具…

并查集 c++函數的值傳遞和引用傳遞 晴神問

目錄 學校的班級個數 手推7個班級&#xff0c;答案17&#xff1f;懷疑人生 破案了&#xff0c;應該是6個班。 破案了&#xff0c;原來寫的是 unionxy(a, b, father); c if兩個數同時為正或為負 簡潔寫法 可以用位運算&#xff1f; c可以這樣賦值嗎&#xff1f;ab2 典型…

Qt Creator快捷鍵合集

前言 QtCreator是一款跨平臺的IDE,專為Qt開發設計,支持C/C++/JS/Python編程,支持設備遠程調試,支持代碼高亮,集成幫助文檔,原生支持cmake和git,確實是一款樸實而又強大的集成開發環境,讓人有種愛不釋手的感覺 編輯 功能快捷鍵復制Ctrl + C粘貼Ctrl + V剪切Ctrl + X代…

docker網絡相關內容詳解

一、docker與k8s 一、Docker 核心解析 1. Docker 定義與架構 本質&#xff1a; 容器化平臺&#xff08;構建容器化應用&#xff09;、進程管理軟件&#xff08;守護進程管理容器生命周期&#xff09;。客戶端&#xff08;docker cli&#xff09;與服務端&#xff08;docker ser…