系統(tǒng)講解Java如何優(yōu)雅地打印日志
前言
“為什么生產(chǎn)環(huán)境的代碼還在用 System.out.println?”、“日志格式混亂,沒有時(shí)間戳,不知道是誰打印的”、“日志級別使用不當(dāng),錯(cuò)誤信息混在普通信息中”。
這些都是開發(fā)中常見的日志問題。本文將介紹如何優(yōu)雅地打印日志,告別 System.out.println,使用專業(yè)的日志框架(Logback/Log4j2)來管理日志,讓你的日志更專業(yè)、更規(guī)范、更易于排查問題。
一、為什么要使用日志框架
1.1 System.out.println 的問題
System.out.println 的主要問題:
1.性能問題:
- 同步輸出,會阻塞線程
- 無法關(guān)閉,即使生產(chǎn)環(huán)境也會輸出
- 無法按級別控制輸出
2.功能缺失:
- 沒有日志級別(DEBUG、INFO、WARN、ERROR)
- 沒有時(shí)間戳、線程信息、類名等關(guān)鍵信息
- 沒有上下文信息
3.管理困難:
- 無法按級別過濾日志
- 無法按日期分割日志文件
- 無法分類存儲不同類型的日志
- 無法遠(yuǎn)程傳輸日志
1.2 日志框架的優(yōu)勢
日志框架的優(yōu)勢:
1.性能優(yōu)化:
- 支持異步輸出,不阻塞主線程
- 支持日志級別控制,生產(chǎn)環(huán)境只輸出WARN和ERROR
- 支持按包路徑控制日志級別
2.功能豐富:
- 支持多種日志級別(DEBUG、INFO、WARN、ERROR)
- 支持自定義格式(時(shí)間、線程、類名、行號等)
- 支持MDC(Mapped Diagnostic Context)上下文信息
3.管理便捷:
- 支持按日期、大小分割日志文件
- 支持按類型分類存儲日志
- 支持異步寫入數(shù)據(jù)庫或消息隊(duì)列
- 支持結(jié)構(gòu)化日志(JSON格式)
二、日志級別
2.1 日志級別定義
/**
* 日志級別從低到高
* TRACE < DEBUG < INFO < WARN < ERROR
*/
// TRACE:追蹤信息,最詳細(xì)的日志信息
// 使用場景:調(diào)試時(shí)追蹤程序執(zhí)行流程
logger.trace("用戶登錄,用戶ID: {}", userId);
// DEBUG:調(diào)試信息,開發(fā)調(diào)試時(shí)使用
// 使用場景:開發(fā)階段,記錄變量值、方法調(diào)用等
logger.debug("查詢用戶,用戶ID: {}", userId);
// INFO:重要信息,記錄關(guān)鍵業(yè)務(wù)流程
// 使用場景:記錄重要操作、系統(tǒng)狀態(tài)等
logger.info("用戶創(chuàng)建成功,用戶ID: {}", userId);
// WARN:警告信息,潛在的問題
// 使用場景:參數(shù)校驗(yàn)失敗、配置警告等
logger.warn("用戶余額不足,用戶ID: {}, 余額: {}", userId, balance);
// ERROR:錯(cuò)誤信息,需要立即處理
// 使用場景:異常捕獲、系統(tǒng)錯(cuò)誤等
logger.error("訂單創(chuàng)建失敗,用戶ID: {}", userId, exception);
2.2 日志級別使用原則
使用建議:
| 級別 | 使用場景 | 生產(chǎn)環(huán)境 |
|---|---|---|
| TRACE | 追蹤詳細(xì)執(zhí)行流程 | 關(guān)閉 |
| DEBUG | 調(diào)試信息、變量值 | 關(guān)閉 |
| INFO | 重要操作、業(yè)務(wù)流程 | 開啟 |
| WARN | 警告、潛在問題 | 開啟 |
| ERROR | 錯(cuò)誤、異常 | 開啟 |
三、Logback 配置
3.1 Maven依賴
<!-- pom.xml -->
<dependencies>
<!-- Spring Boot Starter Web (已包含logback) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Logback Classic -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.14</version>
</dependency>
<!-- Logback Access -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-access</artifactId>
<version>1.4.14</version>
</dependency>
</dependencies>3.2 基礎(chǔ)配置
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 定義日志輸出格式 -->
<property name="LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"/>
<!-- 定義日志文件路徑 -->
<property name="LOG_PATH" value="logs"/>
<!-- 控制臺輸出 -->
<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.log</file>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<!-- 滾動(dòng)策略:按日期滾動(dòng) -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/app.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
</appender>
<!-- 錯(cuò)誤日志單獨(dú)輸出 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/error.log</file>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<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}/error.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
</appender>
<!-- 異步日志 -->
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="FILE"/>
<queueSize>512</queueSize>
<discardingThreshold>0</discardingThreshold>
</appender>
<!-- 根日志器 -->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC_FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
</configuration>3.3 完整配置(推薦)
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<!-- 定義常量 -->
<property name="LOG_HOME" value="logs"/>
<property name="APP_NAME" value="myapp"/>
<!-- 日志格式 -->
<property name="CONSOLE_LOG_PATTERN"
value="%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>
<property name="FILE_LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
<!-- 控制臺輸出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 文件輸出 - 所有日志 -->
<appender name="FILE_ALL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/${APP_NAME}-all.log</file>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/${APP_NAME}-all.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
</appender>
<!-- 文件輸出 - 錯(cuò)誤日志 -->
<appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/${APP_NAME}-error.log</file>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/${APP_NAME}-error.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>50MB</maxFileSize>
<maxHistory>60</maxHistory>
<totalSizeCap>5GB</totalSizeCap>
</rollingPolicy>
</appender>
<!-- 異步日志 - 提升性能 -->
<appender name="ASYNC_FILE_ALL" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="FILE_ALL"/>
<queueSize>1024</queueSize>
<discardingThreshold>0</discardingThreshold>
</appender>
<appender name="ASYNC_FILE_ERROR" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="FILE_ERROR"/>
<queueSize>512</queueSize>
<discardingThreshold>0</discardingThreshold>
</appender>
<!-- 開發(fā)環(huán)境 -->
<springProfile name="dev">
<logger name="com.example" level="DEBUG"/>
<root level="DEBUG">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC_FILE_ALL"/>
<appender-ref ref="ASYNC_FILE_ERROR"/>
</root>
</springProfile>
<!-- 測試環(huán)境 -->
<springProfile name="test">
<logger name="com.example" level="INFO"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC_FILE_ALL"/>
<appender-ref ref="ASYNC_FILE_ERROR"/>
</root>
</springProfile>
<!-- 生產(chǎn)環(huán)境 -->
<springProfile name="prod">
<logger name="com.example" level="INFO"/>
<root level="INFO">
<appender-ref ref="ASYNC_FILE_ALL"/>
<appender-ref ref="ASYNC_FILE_ERROR"/>
</root>
</springProfile>
</configuration>3.4 配置說明
# application.yml
logging:
config: classpath:logback-spring.xml
level:
root: INFO
com.example: DEBUG
org.springframework: INFO
org.hibernate.SQL: DEBUG
org.hibernate.type.descriptor.sql.BasicBinder: TRACE四、Log4j2 配置
4.1 Maven依賴
<!-- pom.xml -->
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<!-- 排除默認(rèn)的logback -->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Log4j2 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<!-- 異步日志支持 -->
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.4.4</version>
</dependency>
</dependencies>4.2 基礎(chǔ)配置
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<!-- 定義日志格式 -->
<Properties>
<Property name="LOG_PATTERN">
%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n
</Property>
<Property name="LOG_PATH">logs</Property>
</Properties>
<!-- 控制臺輸出 -->
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="${LOG_PATTERN}"/>
</Console>
<!-- 文件輸出 - 所有日志 -->
<RollingFile name="RollingFile" fileName="${LOG_PATH}/app.log"
filePattern="${LOG_PATH}/app.%d{yyyy-MM-dd}-%i.log">
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies>
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
<SizeBasedTriggeringPolicy size="100 MB"/>
</Policies>
<DefaultRolloverStrategy max="30"/>
</RollingFile>
<!-- 文件輸出 - 錯(cuò)誤日志 -->
<RollingFile name="ErrorFile" fileName="${LOG_PATH}/error.log"
filePattern="${LOG_PATH}/error.%d{yyyy-MM-dd}-%i.log">
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies>
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
<SizeBasedTriggeringPolicy size="50 MB"/>
</Policies>
<DefaultRolloverStrategy max="60"/>
</RollingFile>
<!-- 異步日志 -->
<AsyncLogger name="AsyncRollingFile" level="info" additivity="false">
<AppenderRef ref="RollingFile"/>
</AsyncLogger>
<AsyncLogger name="AsyncErrorFile" level="error" additivity="false">
<AppenderRef ref="ErrorFile"/>
</AsyncLogger>
</Appenders>
<!-- 日志器配置 -->
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
<AppenderRef ref="AsyncRollingFile"/>
<AppenderRef ref="AsyncErrorFile"/>
</Root>
</Loggers>
</Configuration>4.3 完整配置(推薦)
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" monitorInterval="30">
<!-- 定義變量 -->
<Properties>
<Property name="LOG_HOME">logs</Property>
<Property name="APP_NAME">myapp</Property>
<Property name="LOG_PATTERN">
%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{50} - %msg%n%ex
</Property>
</Properties>
<!-- Appenders -->
<Appenders>
<!-- 控制臺輸出 -->
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="${LOG_PATTERN}"/>
</Console>
<!-- 文件輸出 - 所有日志 -->
<RollingRandomAccessFile name="RollingFile"
fileName="${LOG_HOME}/${APP_NAME}.log"
filePattern="${LOG_HOME}/${APP_NAME}-%d{yyyy-MM-dd}-%i.log"
immediateFlush="false"
append="true">
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies>
<OnStartupTriggeringPolicy/>
<SizeBasedTriggeringPolicy size="100 MB"/>
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
</Policies>
<DefaultRolloverStrategy max="30"/>
</RollingRandomAccessFile>
<!-- 文件輸出 - 錯(cuò)誤日志 -->
<RollingRandomAccessFile name="ErrorFile"
fileName="${LOG_HOME}/${APP_NAME}-error.log"
filePattern="${LOG_HOME}/${APP_NAME}-error-%d{yyyy-MM-dd}-%i.log"
immediateFlush="false"
append="true">
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies>
<OnStartupTriggeringPolicy/>
<SizeBasedTriggeringPolicy size="50 MB"/>
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
</Policies>
<DefaultRolloverStrategy max="60"/>
</RollingRandomAccessFile>
<!-- 異步日志 -->
<AsyncLogger name="AsyncRollingFile" level="info" additivity="false">
<AppenderRef ref="RollingFile"/>
</AsyncLogger>
<AsyncLogger name="AsyncErrorFile" level="error" additivity="false">
<AppenderRef ref="ErrorFile"/>
</AsyncLogger>
</Appenders>
<!-- Loggers -->
<Loggers>
<!-- Spring框架日志級別 -->
<Logger name="org.springframework" level="INFO"/>
<Logger name="org.hibernate" level="INFO"/>
<Logger name="org.apache" level="INFO"/>
<!-- 應(yīng)用日志級別 -->
<Logger name="com.example" level="DEBUG"/>
<!-- Root Logger -->
<Root level="INFO">
<AppenderRef ref="Console"/>
<AppenderRef ref="AsyncRollingFile"/>
<AppenderRef ref="AsyncErrorFile"/>
</Root>
</Loggers>
</Configuration>五、代碼中使用日志
5.1 基本使用
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 日志使用示例
*/
public class UserService {
// 獲取Logger實(shí)例
private static final Logger logger = LoggerFactory.getLogger(UserService.class);
/**
* 創(chuàng)建用戶
*/
public void createUser(User user) {
// TRACE:追蹤信息
logger.trace("開始創(chuàng)建用戶,用戶名: {}", user.getUsername());
try {
// DEBUG:調(diào)試信息
logger.debug("用戶信息: {}", user);
// 業(yè)務(wù)邏輯
saveUser(user);
// INFO:重要信息
logger.info("用戶創(chuàng)建成功,用戶ID: {}", user.getId());
} catch (Exception e) {
// ERROR:錯(cuò)誤信息(包含異常堆棧)
logger.error("用戶創(chuàng)建失敗,用戶名: {}", user.getUsername(), e);
}
}
/**
* 參數(shù)校驗(yàn)失敗
*/
public void validateUser(User user) {
if (user.getUsername() == null || user.getUsername().isEmpty()) {
// WARN:警告信息
logger.warn("用戶名為空,用戶ID: {}", user.getId());
}
}
}
5.2 使用占位符
/**
* 日志占位符使用示例
*/
public class OrderService {
private static final Logger logger = LoggerFactory.getLogger(OrderService.class);
/**
* 創(chuàng)建訂單
*/
public void createOrder(Order order) {
// 使用占位符{},避免字符串拼接
// 錯(cuò)誤做法:logger.info("訂單創(chuàng)建成功,訂單ID: " + order.getId());
// 正確做法:
logger.info("訂單創(chuàng)建成功,訂單ID: {}", order.getId());
// 多個(gè)占位符
logger.info("訂單信息: ID={}, 用戶ID={}, 金額={}",
order.getId(), order.getUserId(), order.getAmount());
// 占位符數(shù)量不匹配時(shí),會使用toString()
logger.info("訂單創(chuàng)建: {}", order);
}
}
5.3 異常日志
/**
* 異常日志示例
*/
public class PaymentService {
private static final Logger logger = LoggerFactory.getLogger(PaymentService.class);
/**
* 支付處理
*/
public void processPayment(Payment payment) {
try {
// 業(yè)務(wù)邏輯
process(payment);
} catch (InsufficientBalanceException e) {
// 帶異常的日志
logger.error("余額不足,用戶ID: {}, 訂單ID: {}",
payment.getUserId(), payment.getOrderId(), e);
} catch (PaymentException e) {
// 錯(cuò)誤日志
logger.error("支付處理失敗,訂單ID: {}", payment.getOrderId(), e);
} catch (Exception e) {
// 未預(yù)期的異常
logger.error("系統(tǒng)異常,訂單ID: {}", payment.getOrderId(), e);
}
}
}
5.4 MDC 上下文
import org.slf4j.MDC;
/**
* MDC(Mapped Diagnostic Context)使用示例
*/
public class RequestContext {
private static final Logger logger = LoggerFactory.getLogger(RequestContext.class);
/**
* 設(shè)置請求上下文
*/
public static void setContext(String traceId, String userId) {
// 設(shè)置MDC
MDC.put("traceId", traceId);
MDC.put("userId", userId);
MDC.put("requestTime", String.valueOf(System.currentTimeMillis()));
logger.info("請求上下文已設(shè)置");
}
/**
* 清除請求上下文
*/
public static void clearContext() {
MDC.clear();
logger.info("請求上下文已清除");
}
/**
* 使用示例
*/
public void handleRequest(String traceId, String userId) {
try {
setContext(traceId, userId);
// 日志會自動(dòng)包含MDC信息
logger.info("處理請求");
// 業(yè)務(wù)邏輯
processRequest();
} finally {
clearContext();
}
}
}
logback-spring.xml 中配置 MDC:
<pattern>
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] [%X{userId}] %-5level %logger{36} - %msg%n
</pattern>六、日志格式說明
6.1 常用格式化符號
/**
* Logback 格式化符號
*/
%logger{36} // Logger名稱,最多36個(gè)字符
%date // 日期和時(shí)間
%d{yyyy-MM-dd} // 日期
%d{HH:mm:ss.SSS} // 時(shí)間
%p / %-5level // 日志級別(5個(gè)字符寬度,左對齊)
%thread // 線程名稱
%m / %msg // 日志消息
%n // 換行符
%ex // 異常堆棧
%caller // 調(diào)用者信息
%line // 行號
%file // 文件名
%M // 方法名
/**
* Log4j2 格式化符號
*/
%d // 日期和時(shí)間
%p / %-5p // 日志級別
%t // 線程名稱
%c / %-36c // Logger名稱
%m // 日志消息
%n // 換行符
%ex // 異常堆棧
%F // 文件名
%L // 行號
%M // 方法名
%r // 毫秒數(shù)
6.2 推薦的日志格式
<!-- 控制臺格式(帶顏色) -->
<pattern>
%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint}
%clr(%5p)
%clr([%15.15t]){faint}
%clr(%-40.40logger{39}){cyan}
%clr(:){faint}
%m%n
</pattern>
<!-- 文件格式 -->
<pattern>
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n%ex
</pattern>
<!-- 包含MDC的格式 -->
<pattern>
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] [%X{userId}] %-5level %logger{36} - %msg%n
</pattern>七、日志最佳實(shí)踐
7.1 最佳實(shí)踐示例
/**
* 日志最佳實(shí)踐
*/
public class OrderService {
private static final Logger logger = LoggerFactory.getLogger(OrderService.class);
/**
* 最佳實(shí)踐1:合理使用日志級別
*/
public void createOrder(Order order) {
logger.trace("開始創(chuàng)建訂單"); // 追蹤信息
logger.debug("訂單詳情: {}", order); // 調(diào)試信息
// 重要操作
logger.info("訂單創(chuàng)建成功,訂單ID: {}, 用戶ID: {}",
order.getId(), order.getUserId());
}
/**
* 最佳實(shí)踐2:使用占位符而不是字符串拼接
*/
public void updateOrder(Order order) {
// 錯(cuò)誤做法
// logger.info("訂單更新: ID=" + order.getId() + ", 狀態(tài)=" + order.getStatus());
// 正確做法
logger.info("訂單更新: ID={}, 狀態(tài)={}", order.getId(), order.getStatus());
}
/**
* 最佳實(shí)踐3:異常日志包含關(guān)鍵信息
*/
public void processOrder(Long orderId) {
try {
process(orderId);
} catch (OrderNotFoundException e) {
// 包含關(guān)鍵業(yè)務(wù)信息
logger.error("訂單不存在,訂單ID: {}", orderId, e);
} catch (Exception e) {
// 包含異常堆棧
logger.error("訂單處理失敗,訂單ID: {}", orderId, e);
}
}
/**
* 最佳實(shí)踐4:避免在循環(huán)中打印日志
*/
public void processBatch(List<Long> orderIds) {
// 錯(cuò)誤做法:在循環(huán)中打印日志
// for (Long orderId : orderIds) {
// logger.info("處理訂單: {}", orderId);
// }
// 正確做法:批量打印
logger.info("批量處理訂單,數(shù)量: {}, 訂單ID列表: {}",
orderIds.size(), orderIds);
// 或者只打印摘要
logger.info("批量處理訂單,數(shù)量: {}", orderIds.size());
}
/**
* 最佳實(shí)踐5:使用MDC記錄請求上下文
*/
public void handleRequest(String traceId, Long userId) {
MDC.put("traceId", traceId);
MDC.put("userId", String.valueOf(userId));
try {
logger.info("開始處理請求");
// 業(yè)務(wù)邏輯
} finally {
MDC.clear();
}
}
}
7.2 性能優(yōu)化建議
/**
* 日志性能優(yōu)化
*/
public class PerformanceOptimization {
/**
* 優(yōu)化1:判斷日志級別
*/
public void expensiveOperation() {
// 錯(cuò)誤做法:即使日志級別不滿足,也會執(zhí)行字符串拼接
// logger.debug("用戶列表: " + getUsers());
// 正確做法:先判斷日志級別
if (logger.isDebugEnabled()) {
logger.debug("用戶列表: {}", getUsers());
}
}
/**
* 優(yōu)化2:使用異步日志
*/
// 在配置文件中配置異步Appender
/**
* 優(yōu)化3:避免記錄敏感信息
*/
public void logPayment(Payment payment) {
// 錯(cuò)誤做法:記錄敏感信息
// logger.info("支付信息: {}", payment);
// 正確做法:脫敏處理
logger.info("支付信息: ID={}, 金額={}, 卡號={}",
payment.getId(),
payment.getAmount(),
maskCardNumber(payment.getCardNumber()));
}
/**
* 卡號脫敏
*/
private String maskCardNumber(String cardNumber) {
if (cardNumber == null || cardNumber.length() < 16) {
return "****";
}
return cardNumber.substring(0, 4) + "****" + cardNumber.substring(12);
}
}
八、日志分析工具
8.1 常用日志分析工具
/**
* 日志分析工具推薦
*/
public class LogAnalysisTools {
/**
* 1. ELK Stack
* - Elasticsearch: 日志存儲和搜索
* - Logstash: 日志收集和處理
* - Kibana: 日志可視化
*/
/**
* 2. Splunk
* - 商業(yè)日志分析平臺
* - 強(qiáng)大的搜索和分析功能
*/
/**
* 3. Graylog
* - 開源日志管理平臺
* - 基于Elasticsearch和MongoDB
*/
/**
* 4. Loki
* - Grafana出品
* - 輕量級日志聚合系統(tǒng)
*/
/**
* 5. Sentry
* - 錯(cuò)誤監(jiān)控和日志收集
* - 支持實(shí)時(shí)告警
*/
}
8.2 日志查詢示例
# 使用grep查詢?nèi)罩? grep "ERROR" app.log grep "用戶創(chuàng)建成功" app.log # 查找特定時(shí)間段的日志 grep "2024-01-01" app.log # 查找包含特定關(guān)鍵詞的日志 grep -E "(ERROR|WARN)" app.log # 統(tǒng)計(jì)錯(cuò)誤數(shù)量 grep "ERROR" app.log | wc -l # 查找特定用戶的日志 grep "userId=123456" app.log
九、Spring Boot 集成
9.1 配置文件配置
# application.yml
logging:
config: classpath:logback-spring.xml
level:
root: INFO
com.example: DEBUG
org.springframework: INFO
org.hibernate.SQL: DEBUG
org.hibernate.type.descriptor.sql.BasicBinder: TRACE
file:
name: logs/app.log
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"9.2 不同環(huán)境配置
# application-dev.yml(開發(fā)環(huán)境)
logging:
level:
root: DEBUG
com.example: DEBUG
# application-test.yml(測試環(huán)境)
logging:
level:
root: INFO
com.example: INFO
# application-prod.yml(生產(chǎn)環(huán)境)
logging:
level:
root: WARN
com.example: INFO
file:
name: /var/log/myapp/app.log十、總結(jié)
10.1 最佳實(shí)踐清單
配置管理:
- 使用日志框架(Logback/Log4j2)
- 配置合理的日志格式
- 配置日志文件滾動(dòng)策略
- 配置不同環(huán)境的日志級別
代碼使用:
- 使用占位符而不是字符串拼接
- 合理使用日志級別
- 異常日志包含關(guān)鍵信息
- 使用MDC記錄請求上下文
性能優(yōu)化:
- 使用異步日志
- 判斷日志級別后再記錄
- 避免在循環(huán)中打印日志
- 避免記錄敏感信息
日志管理:
- 使用日志分析工具
- 定期清理舊日志
- 配置日志告警
- 建立日志規(guī)范
10.2 避坑指南
常見錯(cuò)誤:
- ? 使用 System.out.println
- ? 字符串拼接日志消息
- ? 所有日志都用INFO級別
- ? 異常日志不包含堆棧
- ? 在循環(huán)中打印大量日志
- ? 記錄敏感信息
正確做法:
- ? 使用日志框架
- ? 使用占位符{}
- ? 合理使用日志級別
- ? 異常日志包含堆棧和關(guān)鍵信息
- ? 批量記錄或記錄摘要
- ? 脫敏處理敏感信息
記?。?strong>日志是系統(tǒng)的重要組成部分,良好的日志記錄能夠幫助你快速定位問題、分析系統(tǒng)狀態(tài)。選擇合適的日志框架,合理配置日志級別和格式,遵循最佳實(shí)踐,讓你的日志更有價(jià)值!
以上就是系統(tǒng)講解Java如何優(yōu)雅地打印日志的詳細(xì)內(nèi)容,更多關(guān)于Java打印日志的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java里得到00:00:00格式的時(shí)分秒的Timestamp
Java里如何得到00:00:00格式的時(shí)分秒的Timestamp ,下面是具體的實(shí)現(xiàn)代碼,需要的朋友可以參考下。2009-09-09
SpringBoot用配置影響B(tài)ean加載@ConditionalOnProperty
這篇文章主要為大家介紹了SpringBoot用配置影響B(tài)ean加載@ConditionalOnProperty示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04
java使用Jsoup連接網(wǎng)站超時(shí)的解決方法
jsoup是一個(gè)非常好的解析網(wǎng)頁的包,用java開發(fā)的,提供了類似DOM,CSS選擇器的方式來查找和提取文檔中的內(nèi)容,提取文檔內(nèi)容時(shí)會出現(xiàn)超時(shí)的情況,解決方法可看下文2013-11-11
SpringBoot實(shí)現(xiàn)數(shù)據(jù)轉(zhuǎn)換的四種對象映射方案
在項(xiàng)目開發(fā)中,對象之間的相互轉(zhuǎn)換是一個(gè)高頻操作,尤其在分層架構(gòu)的系統(tǒng)中,數(shù)據(jù)在實(shí)體對象(Entity)、數(shù)據(jù)傳輸對象(DTO)、值對象(VO)之間的轉(zhuǎn)換尤為常見,選擇一個(gè)高效、可靠的對象映射方案對提高系統(tǒng)性能和開發(fā)效率至關(guān)重要,本文將介紹4種對象映射方案2025-06-06
Java中final關(guān)鍵字和final的四種用法實(shí)例
final關(guān)鍵字代表最終的、不可改變的,下面這篇文章主要給大家介紹了關(guān)于Java中final關(guān)鍵字和final的四種用法實(shí)例,文中通過圖文以及實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-02-02
MyBatis復(fù)雜Sql查詢實(shí)現(xiàn)示例介紹
在利用mybatis做查詢的時(shí)候,一般返回結(jié)果用resulttype,這種情況必須是查詢的結(jié)果在對應(yīng) 的pojo類中有對應(yīng)的,一般都是單表查詢,但是對于一些復(fù)雜的情況,比如需要用到多表查詢的時(shí)候,resultType不再適用,此時(shí)一般用resultMap來表示返回的結(jié)果2022-12-12

