大家好,我是曉凡
一、日誌概念
日誌的重要性不用我多說了,日誌,簡單來說就是記錄。
用來記錄程式執行時發生的事情。比如,程式啟動了、執行了某個操作、遇到了問題等等,這些都可以透過日誌記錄下來。
想象一下,你開了一家店,每天的營業額、顧客的反饋、商品的進出、庫存等等,你都會記錄下來。這就像是程式的日誌。比如:
- 電商網站:記錄使用者的登入、瀏覽、購買行為,監控交易過程,及時發現異常交易;透過日誌分析你的瀏覽記錄,實現精準推送等等
- 伺服器:記錄伺服器的啟動、執行、關閉狀態,以及發生的各種錯誤,幫助管理員及時發現並解決問題。
1.1 日誌的作用
- 除錯幫助:當程式出現問題時,透過檢視日誌,可以快速定位問題發生的地方和原因。
- 監控執行狀態:透過日誌可以瞭解程式的執行狀態,比如使用者的操作、系統的效能等。
- 安全審計:在需要記錄使用者行為或系統操作的場合,日誌可以作為審計的依據。
1.2 具體示例
public class SimpleApp {
public static void main(String[] args) {
System.out.println("程式啟動");
// 假設這裡是使用者輸入資料
String userInput = "Hello, World!";
System.out.println("使用者輸入了: " + userInput);
// 處理資料
String result = processInput(userInput);
System.out.println("處理結果: " + result);
try {
//可能異常的邏輯程式碼
}catch(Exception e){
e.printStackTrace()
}
// 程式結束
System.out.println("程式結束");
}
private static String processInput(String input) {
// 這裡是處理邏輯
return "Processed: " + input;
}
}
上面的程式碼我們不陌生了吧,我們使用System.out.println
來列印程式的執行狀態,使用e.printStackTrace()
來列印資訊和錯誤
這就是沒有日誌框架時,最簡單直接的日誌列印方式
這種方式簡單直接,但也有一些缺點:
- 靈活性差:不能方便地控制日誌的輸出格式、級別等。
- 效能問題:大量日誌輸出可能會影響程式效能。
- 不易管理:日誌資訊混在標準輸出中,不易於查詢和分析。
所以我們要引入各種功能強大的日誌框架進行日誌管理
二、主流日誌框架
日誌框架由日誌門面和日誌實現構成,具體如下圖所示
2.1 日誌門面
顧名思義,日誌門面,就像是一個團隊的領導者一樣,只負責制定規則,安排任務,而具體幹活的則交給苦逼的打工人(日誌具體實現)即可。
日誌門面提供了一套標準的日誌記錄介面,而具體的日誌記錄工作則由不同的日誌框架來完成。
這樣做的好處是,可以在不修改程式碼的情況下,透過配置來切換不同的日誌框架。
正如職場中,一個打工人跑路了,在不需要太多成本,不用做太多改變的情況下,新招一個更便宜的打工人也可完成同樣的任務實現快速切換,好像有點扯遠了
主流的日誌門面框架主要有:
SLF4J
:這是一個非常流行的日誌門面,它提供了一套簡單的日誌記錄介面,並且可以與多種日誌框架(如Log4j、Logback等)配合使用。JCL
:這是早期的一個日誌門面
2.2 日誌實現
透過是實現日誌門面介面來完成日誌記錄,實實在在的打工人無疑了
主流的日誌實現框架有:
-
JUL
Java
自帶的日誌框架 ,功能相對基礎,效能一般,但對於簡單的日誌需求來說足夠用了。 -
Log4j
個非常老牌的日誌框架,功能非常強大,可以自定義很多日誌的細節,比如日誌級別、輸出格式、輸出目的地等。現由Apache軟體基金會維護
Log4j2
也是Apache軟體基金會開發,相比Log4j
,Log4j2
在效能上有顯著提升,同時保持了豐富的功能,支援非同步日誌處理,適合高效能需求的場景
Logback
由Log4j
的原開發者之一主導開發,Spring Boot
預設日誌,輕量級,效能優秀,功能也比較全面
三、JUL日誌框架
3.1 主要元件
- Logger:日誌記錄器,是日誌系統的核心,用來生成日誌記錄。
- Handler:日誌處理器,負責將日誌資訊輸出到不同的目的地,比如控制檯、檔案等。可以為每個Logger配置一個或多個
Handler
- Formatter:日誌格式化器,負責定義日誌的輸出格式。比如時間戳、日誌級別、訊息等。
- Level:設定日誌級別,常見的級別有
SEVERE
、WARNING
、INFO
、CONFIG
、FINE
、FINER
、FINEST
等。 - Filter: 這個元件用來過濾日誌記錄。你可以設定一些規則,只有滿足這些規則的日誌才會被記錄。
- Log Record:這是日誌記錄本身,包含了日誌的所有資訊,比如時間、日誌級別、訊息等
3.2 使用步驟
- 獲取
Logger
例項。 - 新增
Handler
- 為上一步新增的
Handler
設定日誌級別(Level
)和格式輸出(Formatter
) - 建立
Filter
過濾器 - 為
Logger
例項新增日誌處理器(Handler
)和日誌過濾器(Filter
) - 記錄日誌。
3.3 入門案例
public class LogQuickTest {
@Test
public void testLogQuick(){
//建立日誌記錄物件
Logger logger = Logger.getLogger("com.xiezhr");
//日誌記錄輸出
logger.info("這是一個info日誌");
logger.log(Level.INFO,"這是一個info日誌");
String name="程式設計師曉凡";
Integer age=18;
logger.log(Level.INFO,"姓名:{0},年齡:{1}",new Object[]{name,age});
}
}
3.4 日誌級別
日誌級別系統,用來區分日誌的重要性
3.4.1 日誌級別
- SEVERE(嚴重):這是最高階別的日誌,用來記錄嚴重錯誤,比如系統崩潰、資料丟失等。這類日誌通常需要立即關注和處理。
- WARNING(警告):用來記錄可能不會立即影響系統執行,但可能表明潛在問題的資訊。比如,某個操作沒有達到預期效果,或者系統資源接近耗盡。
- INFO(資訊):用來記錄一般性的資訊,比如程式執行的狀態、重要的操作步驟等。這類資訊對於瞭解程式的執行情況很有幫助,但通常不需要立即處理。
- CONFIG(配置):用來記錄配置資訊,比如程式啟動時載入的配置檔案、初始化的引數等。這類日誌有助於除錯和驗證程式的配置是否正確。
- FINE(詳細):用來記錄更詳細的資訊,比如程式內部的執行細節、變數的值等。這類日誌對於開發者在除錯程式時瞭解程式的內部狀態非常有用。
- FINER(更詳細):比FINE級別更細的日誌,記錄更深入的執行細節。通常用於深入分析程式的執行情況。
- FINEST(最詳細):這是最低階別的日誌,記錄最詳細的資訊,包括程式的每一步執行細節。這類日誌可能會產生大量的輸出,通常只在需要非常詳細的除錯資訊時使用。
3.4.2 級別關係
SEVERE > WARNING > INFO > CONFIG > FINE > FINER > FINEST
日誌級別越高,記錄的資訊越重要。當你設定一個日誌級別時,比如INFO,那麼INFO級別以及以上的日誌(SEVERE和WARNING)都會被記錄,而FINE、FINER和FINEST級別的日誌則會被忽略
3.5 詳細使用案例(硬編碼)
這裡我們按照上面的步驟建立一個日誌記錄器,將日誌檔案分別輸出到控制檯和檔案中
public class LoggingExampleTest {
@Test
public void testLogging() {
// 獲取日誌記錄器
Logger logger = Logger.getLogger("LoggingExample");
// 設定日誌級別為INFO,這意味著INFO級別及以上的日誌會被記錄
logger.setLevel(Level.INFO);
// 建立控制檯Handler 將日誌輸出到控制檯
// 並設定其日誌級別和Formatter
ConsoleHandler consoleHandler = new ConsoleHandler();
consoleHandler.setLevel(Level.WARNING); // 控制檯只輸出WARNING及以上級別的日誌
consoleHandler.setFormatter(new SimpleFormatter() {
@Override
public synchronized String format(LogRecord record) {
// 自定義日誌格式
return String.format("%1$tF %1$tT [%2$s] %3$s %n", record.getMillis(), record.getLevel(), record.getMessage());
}
});
logger.addHandler(consoleHandler);
// 建立檔案Handler 將日誌輸出到檔案
// 並設定其日誌級別和Formatter
try {
FileHandler fileHandler = new FileHandler("app.log", true);
fileHandler.setLevel(Level.ALL); // 檔案將記錄所有級別的日誌
fileHandler.setFormatter(new SimpleFormatter() {
@Override
public synchronized String format(LogRecord record) {
// 自定義日誌格式
return String.format("%1$tF %1$tT [%2$s] %3$s %n", record.getMillis(), record.getLevel(), record.getMessage());
}
});
logger.addHandler(fileHandler);
} catch (IOException e) {
e.printStackTrace();
}
// 建立並設定Filter
Filter filter = new Filter() {
@Override
public boolean isLoggable(LogRecord record) {
// 這裡可以新增過濾邏輯,例如只記錄包含特定字串的日誌
return record.getMessage().contains("important");
}
};
// 將Filter應用到Logger
//logger.setFilter(filter);
// 記錄不同級別的日誌
logger.severe("嚴重錯誤資訊 - 應記錄到控制檯和檔案");
logger.warning("警告資訊 - 應記錄到控制檯和檔案");
logger.info("常規資訊 - 只記錄到檔案");
logger.config("配置資訊 - 只記錄到檔案");
logger.fine("詳細日誌 - 只記錄到檔案");
// 這條日誌將被Filter過濾掉,不會記錄
logger.info("這條資訊不重要,將被過濾");
// 這條日誌將被記錄,因為訊息中包含"important"
logger.info("這條資訊很重要,將被記錄到控制檯和檔案");
}
}
① 控制檯日誌輸出
②日誌檔案輸出 app.log
內容
程式碼解釋:
- Logger獲取:首先獲取一個名為
LoggingExample
的Logger
例項。 - 設定日誌級別:將Logger的日誌級別設定為
INFO
,這意味著INFO及以上級別的日誌將被記錄。 - 控制檯Handler:建立一個
ConsoleHandler
例項,設定其日誌級別為WARNING
,並且自定義了日誌的輸出格式。 - 檔案Handler:嘗試建立一個
FileHandler
例項,將日誌寫入到app.log
檔案中,並設定其日誌級別為ALL
,意味著所有級別的日誌都將被記錄到檔案。 - 自定義Formatter:為Handler建立自定義的
SimpleFormatter
,用於定義日誌的輸出格式。 - Filter設定:建立一個實現了
Filter
介面的匿名內部類,並重寫isLoggable
方法,實現過濾邏輯,這裡只記錄訊息中包含"important"字串的日誌。 - 應用Filter:將建立的Filter應用到Logger上。
- 記錄日誌:記錄不同級別的日誌,展示不同級別的日誌如何被Handler和Filter處理。
- 日誌記錄:一些日誌將根據設定的日誌級別、Handler和Filter的規則被記錄到控制檯或檔案,或者被忽略。
3.6 日誌配置檔案
以上3.4小節透過硬編碼的方式列印輸出日誌,這樣的方式很不利於後期的管理與維護,這小節我們將使用配置檔案的方式進行日誌輸出
① 在resources下面新建logconfig.properties
檔案,內容如下
# 指定日誌處理器為:ConsoleHandler,FileHandler 表示同時使用控制檯和檔案處理器
handlers= java.util.logging.ConsoleHandler,java.util.logging.FileHandler
#設定預設的日誌級別為:ALL
.level= ALL
# 配置自定義 Logger
com.xiezhr.handlers = com.xiezhr.DefConsoleHandler
com.xiezhr.level = CONFIG
# 如果想要使用自定義配置,需要關閉預設配置
com.xiezhr.useParentHanlders =true
# 向日志檔案輸出的 handler 物件
# 指定日誌檔案路徑 當檔案數為1時 日誌為/logs/java0.log
java.util.logging.FileHandler.pattern = /logs/java%u.log
# 指定日誌檔案內容大小,下面配置表示日誌檔案達到 50000 位元組時,自動建立新的日誌檔案
java.util.logging.FileHandler.limit = 50000
# 指定日誌檔案數量,下面配置表示只保留 1 個日誌檔案
java.util.logging.FileHandler.count = 1
# 指定 handler 物件日誌訊息格式物件
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter
# 指定 handler 物件的字符集為 UTF-8 ,防止出現亂碼
java.util.logging.FileHandler.encoding = UTF-8
# 指定向檔案中寫入日誌訊息時,是否追加到檔案末尾,true 表示追加,false 表示覆蓋
java.util.logging.FileHandler.append = true
# 向控制檯輸出的 handler 物件
# 指定 handler 物件的日誌級別
java.util.logging.ConsoleHandler.level =WARNING
# 指定 handler 物件的日誌訊息格式物件
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
# 指定 handler 物件的字符集
java.util.logging.ConsoleHandler.encoding = UTF-8
# 指定日誌訊息格式
java.util.logging.SimpleFormatter.format = [%1$tF %1$tT] %4$s: %5$s %n
注意: 設定日誌訊息格式中(後面一小節會詳細講解)
%1$tF
:這個佔位符表示日誌記錄的時間,格式為yyyy-MM-dd
,其中1$
表示這是第一個引數tF
是日期的格式化程式碼%1$tT
:這個佔位符表示日誌記錄的時間,格式為HH:mm:ss.SSS
,即小時:分鐘:秒.毫秒1$
表示這是第一個引數,tT
是時間的格式化程式碼%4$s
: 表示日誌級別,level =WARNING
輸出警告 level =INFO 輸出訊息%5$s
: 表示日誌訊息%n
:這個佔位符表示換行符,每條日誌記錄之後會有一個換行,以便在檢視日誌時能夠清晰地區分每條記錄。
② 日誌測試
@Test
public void testLogProperties()throws Exception{
// 1、讀取配置檔案,透過類載入器
InputStream ins = LoggingExampleTest.class.getClassLoader().getResourceAsStream("logconfig.properties");
// 2、建立LogManager
LogManager logManager = LogManager.getLogManager();
// 3、透過LogManager載入配置檔案
logManager.readConfiguration(ins);
// 4、建立日誌記錄器
Logger logger = Logger.getLogger("com.xiezhr");
// 5、記錄不同級別的日誌
logger.severe("這是一條severe級別資訊");
logger.warning("這是一條warning級別資訊");
}
執行上面程式碼後
控制檯輸出:
java0.log檔案輸出:
3.7 日誌格式化
上面兩個小節中,不管是透過編碼或者配置檔案 都對日誌進行了格式化
① 編碼設定日誌格式
fileHandler.setFormatter(new SimpleFormatter() {
@Override
public synchronized String format(LogRecord record) {
// 自定義日誌格式
return String.format("%1$tF %1$tT [%2$s] %3$s %n", record.getMillis(), record.getLevel(), record.getMessage());
}
});
② 配置檔案指定日誌格式
# 指定日誌訊息格式
java.util.logging.SimpleFormatter.format = [%1$tF %1$tT] %4$s: %5$s %n
上面設定的日誌格式設定你看懂了麼?
不管是哪種方式設定日誌格式,我們看原始碼最終都是透過String.format
函式來實現的,所有我們有必要學一學String
類提供的format
這個方法的使用
3.7.1 String
的format
方法
String
的format
方法用來格式化字串。
format
方法就像是一個模板,你可以在這個模板裡插入你想要的資料,然後它就會幫你生成一個格式化好的字串。
我們先來看看下面這個簡單例子
@Test
public void testStringFormatter()throws Exception{
String name = "曉凡";
Integer age = 18;
// 使用String.format()方法格式化字串
String xiaofan = String.format("%s今年%d歲", name, age);
System.out.println(xiaofan);
}
//輸出
曉凡今年18歲
3.7.2 常用佔位符
%s
和%d
為佔位符,不同型別需要不同佔位符,那麼還有哪些常用轉換符呢?
佔位符 | 詳細說明 | 示例 |
---|---|---|
%s |
字串型別**** | “喜歡曉凡請關注” |
%c |
字元型別 | ‘x’ |
%b |
布林型別 | true |
%d |
整數型別(十進位制) | 666 |
%x |
整數型別(十六進位制) | FF |
%o |
整數型別(八進位制) | 77 |
%f |
浮點型別 | 8.88 |
%a |
十六進位制浮點型別 | FF.34 |
%e |
指數型別 | 1.28e+5 |
%n |
換行符 | |
%tx |
日期和時間型別(x代表不同的日期與時間轉換符) |
3.7.3 特殊符號搭配使用
符號 | 說明 | 示例 | 結果 |
---|---|---|---|
0 | 指定數字、字元前面補0,用於對齊 | ("%04d",6) | 0006 |
空格 | 指定數字、字元前面補空格,用於對齊 | ("[% 4s]",x) | [ x] |
, | 以“,”對數字分組顯示(常用於金額) | ("%,f,666666.66") | 666,666.6600 |
注意: 預設情況下,可變引數是按照順序依次替換,但是我們可以透過“數字$”來重複利用可變引數
@Test
public void testStringFormatter()throws Exception{
String name = "曉凡";
Integer age = 18;
// 使用String.format()方法格式化字串
String xiaofan = String.format("%s今年%d歲", name, age);
System.out.println(xiaofan);
//
String xiaofan1 = String.format("%s今年%d歲,%1$s的公眾號是:程式設計師曉凡", name, age);
System.out.println(xiaofan1);
}
//輸出
曉凡今年18歲
曉凡今年18歲,曉凡的公眾號是:程式設計師曉凡
上面例子中我們透過%1$s
重複使用第一個引數name
3.7.4 日期格式化
上面我們說到%tx,x代表日期轉換符,其具體含義如下
符號 | 描述 | 示例 |
---|---|---|
c |
包含全部日期和時間資訊 | 週六 8月 03 17:16:37 CST 2024 |
F |
"年-月-日" 格式 | 2024-08-03 |
D |
"月/日/年"格式 | 08/03/24 |
d |
日 | 03 |
r |
“HH:MM:SS PM ”格式(12小時制) |
05:16:37 下午 |
R |
“HH:MM ”格式(24小時制) |
17:16 |
T |
“HH:MM:SS ”格式(24小時制) |
17:16:37 |
b |
月份本地化 | 8月 |
y |
兩位年 | 24 |
Y |
四位年 | 2024 |
m |
月 | 08 |
H |
時(24小時制) | 17 |
I |
時(12小時制) | 05 |
M |
分 | 16 |
S |
秒 | 37 |
s |
秒為單位的時間戳 | 1722677530 |
p |
上午還是下午 | 下午 |
四、Log4j日誌框架
Log4j 是Apache軟體基金組織旗下的一款開源日誌框架,是一款比較老的日誌框架,目前已出log4j2,它在log4j上做了很大改動,效能提升了不少。但是有些老專案還會在使用,所以我們也來說一說
官網:https://logging.apache.org/log4j/1.x/
注意: 從官網,我們可以看到專案管理委員會宣佈Log4j 1. x
已終止使用。建議使用者升級到 Log4j 2
4.1 快速入門
4.1.1 新增依賴
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<!--為了方便測試,我們引入junit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
4.1.2 log4j入門程式碼
@Test
public void testLog4jQuick(){
//初始化日誌配置資訊,不需要配置檔案
BasicConfigurator.configure();
//獲取日誌記錄器
Logger logger = Logger.getLogger(Log4jTest.class);
//透過各種日誌級別列印日誌
logger.fatal("這是一條致命的資訊"); // 嚴重錯誤,一般會造成系統崩潰
logger.error("這是一條錯誤的資訊"); // 出現錯誤時,比如出錯了但是不影響系統繼續執行
logger.warn("這是一條警告的資訊"); // 警告級別,比如要告警的時候
logger.info("這是一條普通的資訊"); // 一般資訊,比如記錄普通的方法執行
logger.debug("這是一條除錯的資訊"); // 除錯資訊,比如除錯的時候列印的資訊
logger.trace("這是一條追蹤的資訊"); // 追蹤資訊,比如追蹤程式執行路徑
}
//輸出
0 [main] FATAL Log4jTest - 這是一條致命的資訊
0 [main] ERROR Log4jTest - 這是一條錯誤的資訊
0 [main] WARN Log4jTest - 這是一條警告的資訊
0 [main] INFO Log4jTest - 這是一條普通的資訊
0 [main] DEBUG Log4jTest - 這是一條除錯的資訊
注意: BasicConfigurator.configure();
為log4j在不新增配置檔案的情況下初始化預設日誌配置資訊,如果既沒有預設配置資訊,也沒有配置檔案
會報下面錯誤
4.2 日誌級別
日誌級別,就好比是日記本里的不同標記,用來區分資訊的重要性。在log4j中,日誌級別從低到高分為以下幾種:
- TRACE:追蹤級別,通常用來記錄程式執行的詳細軌跡,比如方法呼叫的順序等。這個級別非常詳細,一般在開發階段或者除錯時用得比較多。
- DEBUG:除錯級別,用來記錄程式的執行狀態,比如變數的值、程式的流程等。當你需要深入瞭解程式的內部工作時,DEBUG級別就非常有用。
- INFO:資訊級別,用來記錄程式的正常執行狀態,比如程式啟動、配置資訊、正常結束等。INFO級別的日誌對使用者和開發者瞭解程式的執行情況很有幫助。
- WARN:警告級別,用來記錄一些可能引起問題的情況,但程式仍然可以繼續執行。比如,程式遇到了一個不常見的情況,或者某個操作失敗了但不影響大局。
- ERROR:錯誤級別,用來記錄程式執行中的錯誤,這些錯誤通常會影響程式的正常功能,但程式可能還能繼續執行。
- FATAL:致命級別,用來記錄非常嚴重的錯誤,這些錯誤會導致程式完全無法繼續執行。比如,程式的某個關鍵部分失敗了,整個應用可能需要重啟。
出了上面的,還有以下兩個特殊級別
1. **OFF**: 用來關閉日誌記錄
1. **ALL**: 啟用所有訊息的日誌記錄
4.3 Log4j元件
Logger
:這個元件就像是日誌的大腦,負責記錄日誌資訊。你可以想象它是一個日記本的主人,決定哪些事情值得記錄,哪些事情可以忽略。Appender
:Appender就像是日記本的筆,它決定了日誌資訊要寫到哪裡。可以是控制檯、檔案、資料庫,甚至是透過網路傳送到遠端伺服器。每種Appender都有不同的用途和特點。Layout
:Layout決定了日誌的外觀,也就是日誌的格式。比如,你可以選擇日誌中包含時間、日誌級別、發生日誌的類名和方法名,以及日誌的具體內容等。Layout就像是給日記本設計外觀樣式。
4.3.1 Logger
Log4j
中有一個特殊的logger
叫做root
,它是logger
的根,其他的logger
都會直接或者間接的繼承自root
。
入門示例中,我們透過Logger.getLogger(Log4jTest.class);
獲取的就是root logger
name為org.apache.commons
的logger會繼承name為org.apache
的logger
4.3.2 Appender
用來指定日誌記錄到哪兒,主要有以下幾種
Appender型別 | 作用 |
---|---|
ConsoleAppender |
將日誌輸出到控制檯 |
FileAppender |
將日誌輸出到檔案中 |
DailyRollingFileAppender |
將日誌輸出到檔案中,並且每天輸出到一個日誌檔案中 |
RollingFileAppender |
將日誌輸出到檔案中,並且指定檔案的大小,當檔案大於指定大小,會生成一個新的日誌檔案 |
JDBCAppender |
將日誌儲存到資料庫中 |
4.3.3 Layout
用於控制日誌內容輸出格式,Log4j常用的有以下幾種輸出格式
日誌格式器 | 說明 |
---|---|
HTMLLayout |
將日誌以html表格形式輸出 |
SimpleLayout |
簡單的日誌格式輸出,例如(info-message) |
PatternLayout |
最強大的格式化器,也是我們使用最多的一種,我們可以自定義輸出格式 |
示例:下面我們透過PatternLayout
格式化日誌
@Test
public void testLog4jLayout(){
//初始化日誌配置資訊,不需要配置檔案
BasicConfigurator.configure();
//獲取日誌記錄器
Logger logger = Logger.getLogger(Log4jTest.class);
Layout patternLayout = new PatternLayout("%d{yyyy-MM-dd HH:mm:ss} [%p] - %l - %m%n");// 將自定義的Layout應用到控制檯Appender上
ConsoleAppender consoleAppender = new ConsoleAppender(patternLayout);
logger.addAppender(consoleAppender);
// 記錄日誌
logger.info("這是一條自定義格式的日誌資訊");
}
//輸出
2024-08-04 13:55:35 [INFO] - Log4jTest.testLog4jLayout(Log4jTest.java:44) - 這是一條自定義格式的日誌資訊
佔位符 | 說明 |
---|---|
%m |
輸出程式碼中指定的日誌資訊 |
%p |
輸出優先順序 |
%n |
換行符 |
%r |
輸出自應用啟用到輸出log資訊消耗的毫秒數 |
%c |
輸出語句所屬的類全名 |
%t |
輸出執行緒全名 |
%d |
輸出伺服器當前時間,%d |
%l |
輸出日誌時間發生的位置,包括類名、執行緒、及在程式碼中的函式 例如: Log4jTest.testLog4jLayout(Log4jTest.java:44) |
%F |
輸出日誌訊息產生時所在的資料夾名稱 |
%L |
輸出程式碼中的行號 |
%5c |
category名稱不足5位時,左邊補充空格,即右對齊 |
%-5c |
category名稱不足5位時,右邊補充空格,即左對齊 |
.5c |
category名稱大於5位時,會將左邊多出的字元擷取掉,小於5位時,以空格補充 |
4.4 透過配置檔案配置日誌
BasicConfigurator.configure();
上面程式碼中透過這段程式碼初始化日誌配置資訊,這一小節,我們透過配置檔案來配置
透過看LogManager
日誌管理器原始碼,我們知道可以預設載入如下幾種格式的配置檔案(其中log4j.xml
和log4j.properties
是我們最常用的)
-
log4j.properties
-
log4j.xml
-
og4j.configuration
等等
# 指定RootLogger頂級父元素預設配置資訊
# 指定日誌級別位INFO,使用的appender 位Console
log4j.rootLogger=INFO,Console
# 指定控制檯日誌輸出appender
log4j.appender.Console = org.apache.log4j.ConsoleAppender
# 指定訊息格式器 layout
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
# 指定訊息內容格式
log4j.appender.Console.layout.conversionPattern =%d{yyyy-MM-dd HH:mm:ss} [%p] - %l - %m%n
或者
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%p] - %l - %m%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="INFO">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
4.5 各種日誌輸出示例
上面小節中已經說了控制檯輸出配置,由於篇幅原因,這裡不再贅述
① 檔案輸出配置
# 指定RootLogger頂級父元素預設配置資訊
# 指定日誌級別位INFO,使用的appender 位Console
log4j.rootLogger=INFO,File
# 指定檔案日誌輸出appender
log4j.appender.File = org.apache.log4j.FileAppender
# 指定日誌檔名
log4j.appender.File.File=D:/logs/testxiezhr.log
# 指定是否在原有日誌的基礎新增新日誌
log4j.appender.File.Append=true
# 指定訊息格式器 layout
log4j.appender.File.layout=org.apache.log4j.PatternLayout
# 指定訊息內容格式
log4j.appender.File.layout.conversionPattern =%d{yyyy-MM-dd HH:mm:ss} [%p] - %l - %m%n
# 指定日誌檔案編碼格式
log4j.appender.File.encoding=UTF-8
②日誌檔案根據大小分割輸出
# 指定RootLogger頂級父元素預設配置資訊
# 指定日誌級別位INFO,使用的appender 位Console
log4j.rootLogger=INFO,RollingFile
# 指定檔案日誌根據大小分割輸出appender
log4j.appender.RollingFile = org.apache.log4j.RollingFileAppender
# 指定日誌檔名
log4j.appender.RollingFile.File=D:/logs/testxiezhr.log
# 設定是否在重新啟動服務時,在原有日誌的基礎新增新日誌
log4j.appender.RollingFile.Append=true
# 設定最多儲存的日誌檔案個數
log4j.appender.RollingFile.MaxBackupIndex=5
# 設定檔案大小,超過這個值,就會再產生一個檔案
log4j.appender.RollingFile.maximumFileSize=1
# 指定訊息格式器 layout
log4j.appender.RollingFile.layout=org.apache.log4j.PatternLayout
# 指定訊息內容格式
log4j.appender.RollingFile.layout.conversionPattern =%d{yyyy-MM-dd HH:mm:ss} [%p] - %l - %m%n
# 指定日誌檔案編碼格式
log4j.appender.RollingFile.encoding=UTF-8
最終生成日誌效果如下所示
③ 日誌檔案根據日期分割
# 指定RootLogger頂級父元素預設配置資訊
# 指定日誌級別位INFO,使用的appender 位Console
log4j.rootLogger=INFO,DailyRollingFile
# 指定檔案日誌根據日期分割輸出appender
log4j.appender.DailyRollingFile = org.apache.log4j.DailyRollingFileAppender
# 指定日誌檔名
log4j.appender.DailyRollingFile.File=D:/logs/testxiezhr.log
# 設定是否在重新啟動服務時,在原有日誌的基礎新增新日誌
log4j.appender.DailyRollingFile.Append=true
# 指定訊息格式器 layout
log4j.appender.DailyRollingFile.layout=org.apache.log4j.PatternLayout
# 指定訊息內容格式
log4j.appender.DailyRollingFile.layout.conversionPattern =%d{yyyy-MM-dd HH:mm:ss} [%p] - %l - %m%n
# 指定日誌檔案編碼格式
log4j.appender.DailyRollingFile.encoding=UTF-8
最終生成日誌效果如下所示
④ 自定義日誌配置
當我們想定義自己的日誌配置時,可以按照如下配置新增.例如:新增
com.xiezhr
,它也是繼承自rootLogger
,所以我們必須要新增
log4j.additivity.com.xiezhr=false
避免日誌列印重複
# 指定RootLogger頂級父元素預設配置資訊
# 指定日誌級別位INFO,使用的appender 位Console
log4j.rootLogger=INFO,DailyRollingFile
# 自定義日誌配置
log4j.logger.com.xiezhr=DEBUG,Console
# 設定日誌疊加,這一句配置一定要新增,否則日誌會重複輸出
log4j.additivity.com.xiezhr=false
⑤ 將日誌資訊存入資料庫
首先,我們新建一個testlog資料庫,並在資料庫下新建log日誌表
CREATE TABLE `log` (
`log_id` int(11) NOT NULL AUTO_INCREMENT,
`project_name` varchar(255) DEFAULT NULL COMMENT '目項名',
`create_date` varchar(255) DEFAULT NULL COMMENT '建立時間',
`level` varchar(255) DEFAULT NULL COMMENT '優先順序',
`category` varchar(255) DEFAULT NULL COMMENT '所在類的全名',
`file_name` varchar(255) DEFAULT NULL COMMENT '輸出日誌訊息產生時所在的檔名稱 ',
`thread_name` varchar(255) DEFAULT NULL COMMENT '日誌事件的執行緒名',
`line` varchar(255) DEFAULT NULL COMMENT '號行',
`all_category` varchar(255) DEFAULT NULL COMMENT '日誌事件的發生位置',
`message` varchar(4000) DEFAULT NULL COMMENT '輸出程式碼中指定的訊息',
PRIMARY KEY (`log_id`)
);
其次,新建JDBCAppender
,並且為JDBCAppender
設定資料庫連線資訊,具體程式碼如下
@Test
public void testLog4j2db(){
//初始化日誌配置資訊,不需要配置檔案
BasicConfigurator.configure();
//獲取日誌記錄器
Logger logger = Logger.getLogger(Log4jTest.class);
// 新建JDBCAppender
JDBCAppender jdbcAppender = new JDBCAppender();
jdbcAppender.setDriver("com.mysql.cj.jdbc.Driver");
jdbcAppender.setURL("jdbc:mysql://localhost:3308/testlog?useSSL=false&serverTimezone=UTC");
jdbcAppender.setUser("root");
jdbcAppender.setPassword("123456");
jdbcAppender.setSql("INSERT INTO log(project_name,create_date,level,category,file_name,thread_name,line,all_category,message) values('曉凡日誌測試','%d{yyyy-MM-dd HH:mm:ss}','%p','%c','%F','%t','%L','%l','%m')");
logger.addAppender(jdbcAppender);
// 記錄日誌
logger.info("這是一條自定義格式的日誌資訊");
logger.error("這是一條自定義格式的錯誤日誌資訊");
}
最後,執行程式碼,來看一下效果
五、JCL日誌門面
何為日誌門面,我們在第二小節中已經介紹過了,這裡就不多說了。
日誌門面的引入,使得我們可以面向介面開發,不再依賴具體的實現類,減小程式碼耦合。
JCL
全稱Jakarta Commons Logging
是Apache提供的一個通用日誌API
,JCL
中自帶一個日誌實現simplelog
,不過這個功能非常簡單
5.1 JCL快速入門
① LCL的兩個抽象類
- Log: 基本日誌記錄器
- LogFactory: 負責建立Log具體例項,如果時log4j,則建立log4j的例項,如果時jul則建立jul例項
② 示例程式碼
引入依賴
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
基本程式碼
我們沒有匯入任何日誌實現,所以這裡預設使用jdk自帶
JUL
來實現日誌
@Test
public void test(){
Log log = LogFactory.getLog(JclTest.class);
log.error("這是一條error");
log.warn("這是一條warn");
log.info("這是一條info");
log.debug("這是一條debug");
log.trace("這是一條trace");
}
5.2 快速切換Log4j日誌框架
① 匯入log4j
日誌依賴
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
② 新增log4j.properties
配置檔案
# 指定RootLogger頂級父元素預設配置資訊
# 指定日誌級別位INFO,使用的appender 位Console
log4j.rootLogger=INFO,Console
# 指定控制檯日誌輸出appender
log4j.appender.Console = org.apache.log4j.ConsoleAppender
# 指定訊息格式器 layout
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
# 指定訊息內容格式
log4j.appender.Console.layout.conversionPattern =%d{yyyy-MM-dd HH:mm:ss} [%p] - %l - %m%n
③ 測試日誌輸出
@Test
public void testJclLog4j(){
Log log = LogFactory.getLog(JclLog4jTest.class);
log.error("這是一條error");
log.warn("這是一條warn");
log.info("這是一條info");
log.debug("這是一條debug");
log.trace("這是一條trace");
}
日誌輸出如下:
我們可以看到,使用了JCL
日誌門面之後,我們從simplelog
日誌框架切換到log4j
日誌框架,沒有改過程式碼。
六、SLF4j日誌門面
SLF4j
全稱是Simple Logging Facade For Java
Java簡單的日誌門面 和上一小節說到的JCL
乾的一樣的活。
在現目前的大多數Java專案中,日誌框架基本上會選擇slf4j-api
作為門面,配上具體實現框架logback
、log4j
等使用
SLF4j
是目前市面上最流行的日誌門面,主要提供了以下兩個功能
- 日誌框架的繫結
- 日誌框架的橋接
6.1 快速入門
① 新增依賴
<!--新增日誌門面sl4j-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.13</version>
</dependency>
<!--新增slf4j 自帶的簡單日誌實現-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.13</version>
</dependency>
②日誌輸出
//申明日誌物件
public final static Logger logger = LoggerFactory.getLogger(Slf4jTest.class);
@Test
public void testSlf4j(){
//列印日誌
logger.error("這是error日誌");
logger.warn("這是warn日誌");
logger.info("這是info日誌");
logger.debug("這是debug日誌");
logger.trace("這是trace日誌");
//使用佔位符輸出日誌資訊
String name = "曉凡";
Integer age = 18;
logger.info("{},今年{}歲", name, age);
//將系統異常寫入日誌
try {
int i = 1/0;
}catch (Exception e){
logger.error("執行出錯", e);
}
}
上面程式碼輸出日誌如下
6.2 SLF4j 日誌繫結功能
6.2.1 日誌繫結原理
下圖是從官網薅下來的
slf4j
日誌繫結圖,對了,官網在這https://www.slf4j.org/
小夥伴看到上圖可能會有點懵,全是英文,看不懂。
於是乎,曉凡簡單翻譯了一下,如下如所示
- 只匯入日誌門面,沒匯入日誌實現,不會進行日誌輸出
logback
、simplelog
、no-operation
框架遵循SLF4j
規範 匯入jar包即可使用log4j
、JUL
屬於比較古老日誌框架,不遵循SLF4j
規範,需要引入介面卡才能使用- 當我們匯入
slf4j-nop
後將不會使用任何日誌框架
6.2.2 繫結logback日誌框架
① 引入logback依賴
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.14</version>
</dependency>
② 日誌輸出
快速入門中程式碼不變,執行後,採用logback日誌框架輸入日誌如下所示
6.2.3 繫結slf4j-nop
① 引入依賴
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>2.0.13</version>
</dependency>
② 此時控制檯將不會輸出任何日誌
6.2.4 使用介面卡繫結log4j
日誌框架
① 匯入依賴
<!--log4j介面卡-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>2.0.13</version>
</dependency>
<!--log4j日誌框架依賴-->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
② 新增log4j.properties配置檔案
# 指定RootLogger頂級父元素預設配置資訊
# 指定日誌級別位INFO,使用的appender 位Console
log4j.rootLogger=INFO,Console
# 指定控制檯日誌輸出appender
log4j.appender.Console = org.apache.log4j.ConsoleAppender
# 指定訊息格式器 layout
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
# 指定訊息內容格式
log4j.appender.Console.layout.conversionPattern =%d{yyyy-MM-dd HH:mm:ss} [%p] - %l - %m%n
③ 程式碼不變,日誌輸出如下
6.2.5 使用介面卡繫結JUL日誌框架
① 引入依賴
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>2.0.13</version>
</dependency>
② 程式碼不變,日誌輸出如下
6.3 SLF4j日誌橋接
6.3.1 使用場景
如果你的專案中已經使用了Log4j 1.x
等老的日誌框架,但你想遷移到使用SLF4J
的API
,這時候你可以使用SLF4J
的Log4j 1.x
橋接器來平滑過渡
6.3.2 橋接原理
上圖為SLF4j官網提供的橋接原理圖,從圖中,我們可以看到,只需要引入不同的橋接器log4j-over-slf4j
、jul-to-slf4j
、jcl-over-slf4j
就可以實現在不改變原有程式碼的情況下,將日誌從log4j
、jul
、jcl
遷移到slf4j
+logback
日誌組合
6.3.3 橋接步驟
下面以
Log4j 1.x
遷移到slf4j
+logback
日誌組合為例
- 去除老的日誌框架
Log4j 1.x
依賴
-
新增
SLF4J
提供的橋接元件 -
為專案新增
SLF4J
的具體實現
七、Logback日誌框架
官網:https://logback.qos.ch/index.html
7.1 快速入門
① 新增依賴
<!--新增日誌門面SLF4j依賴-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.13</version>
</dependency>
<!--新增Logback日誌實現依賴-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.14</version>
</dependency>
② 列印日誌程式碼
public class LogbackTest {
private static final Logger logger = LoggerFactory.getLogger(LogbackTest.class);
@Test
public void testLogbackQuick(){
logger.error("這是一個錯誤日誌");
logger.warn("這是一個警告日誌");
logger.info("這是一個資訊日誌");
logger.debug("這是一個除錯日誌");
logger.trace("這是一個跟蹤日誌");
}
}
7.2 Logback配置
Logback可以透過程式設計式配置(新增配置類的方式),也可以透過配置檔案配置。
配置檔案是日常開發中最常用的,我們這裡就以這種方式配置,如果對配置檔案感興趣的小夥伴可自行到官網檢視
7.2.1 Logback 包含哪些元件?
Logger
:日誌記錄器,用來記錄不同級別的日誌資訊,比如錯誤、警告、資訊、除錯和追蹤。Appender
:指定日誌資訊輸出到不同的地方。比如,你可以設定一個Appender將日誌輸出到控制檯,另一個Appender將日誌寫入檔案,或者傳送到遠端伺服器。Encoder
:如果你使用的是檔案Appender,Encoder就是用來定義日誌檔案內容格式的。比如,你可以選擇日誌的格式是簡單文字還是XML。Layout
:老版本的Logback
中用來定義日誌格式的元件。在新版本中,Encoder已經取代了Layout的功能。Filter
:指定特定的規則來過濾日誌資訊,比如只記錄錯誤以上的日誌,或者只記錄包含特定關鍵字的日誌。Configuration
:用來配置Logback
的設定,比如設定日誌級別、Appender的型別和引數等。配置可以透過XML
、JSON
或者Groovy
指令碼來完成。
7.2.2 可以有哪些檔案格式進行配置?
Logback會依次讀取以下型別配置檔案
-
logback.groovy
-
logback-test.xml
-
logback.xml
(最常用的)如果均不存在會採用預設配置
7.2.3 新增一個ConsoleAppender
控制檯日誌輸出配置
配置檔案
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!--新增一個名字為pattern的屬性 用來設定日誌輸出可是-->
<!--格式化輸出:%d表示日期,%thread表示執行緒名,%-5level:級別從左顯示5個字元寬度%msg:日誌訊息,%n是換行符-->
<property name="pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %c [%thread]%-5level %msg%n" />
<!--輸出到控制檯-->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!--引用上面配置好的pattern屬性-->
<pattern>${pattern}</pattern>
</encoder>
</appender>
<!--設定日誌級別-->
<root level="ALL">
<!--引用上面配置好的consoleAppender將日誌輸出到控制檯-->
<appender-ref ref="console" />
</root>
</configuration>
日誌輸入如下
日誌輸出格式:在前面幾個日誌框架中我們已經介紹過,大同小異。這裡簡單說下常用的幾種
符號 | 含義 |
---|---|
%d{pattern} |
格式化日期 |
%m或者%msg |
日誌資訊 |
%M |
method(方法) |
%L |
行號 |
%c |
完整類名稱 |
%thread |
執行緒名稱 |
%n |
換行 |
%-5level |
日誌級別,並且左對齊 |
7.2.4 新增一個FileAppender
將日誌輸出到檔案
配置檔案
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!--新增一個名字為pattern的屬性 用來設定日誌輸出可是-->
<!--格式化輸出:%d表示日期,%thread表示執行緒名,%-5level:級別從左顯示5個字元寬度%msg:日誌訊息,%n是換行符-->
<property name="pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %c [%thread]%-5level %msg%n" />
<!--設定日誌檔案存放路徑-->
<property name="log_file" value="d:/logs"></property>
<!--輸出到檔案-->
<appender name="file" class="ch.qos.logback.core.FileAppender">
<encoder>
<!--引用上面配置好的pattern屬性-->
<pattern>${pattern}</pattern>
</encoder>
<!--被寫入的檔名,可以是相對目錄,也可以是絕對目錄,如果上級目錄不存在會自動建立,沒有預設值。-->
<file>${log_file}/logback.log</file>
</appender>
<!--設定日誌級別-->
<root level="ALL">
<!--引用上面配置好的FileAppender將日誌輸出到檔案-->
<appender-ref ref="file" />
</root>
</configuration>
日誌輸出如下
7.2.5 生成html格式appender物件
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!--新增一個名字為pattern的屬性 用來設定日誌輸出可是-->
<!--格式化輸出:%d表示日期,%thread表示執行緒名,%-5level:級別從左顯示5個字元寬度%msg:日誌訊息,%n是換行符-->
<property name="pattern" value="%-5level%d{yyyy-MM-dd HH:mm:ss}%c%M%L%thread%m"/>
<!--設定日誌檔案存放路徑-->
<property name="log_file" value="d:/logs"></property>
<!--輸出到檔案-->
<appender name="htmlFile" class="ch.qos.logback.core.FileAppender">
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="ch.qos.logback.classic.html.HTMLLayout">
<!--引用上面配置好的pattern屬性-->
<pattern>${pattern}</pattern>
</layout>
</encoder>
<!--被寫入的檔名,可以是相對目錄,也可以是絕對目錄,如果上級目錄不存在會自動建立,沒有預設值。-->
<file>${log_file}/logback.html</file>
</appender>
<!--設定日誌級別-->
<root level="ALL">
<!--引用上面配置好的FileAppender將日誌輸出到檔案-->
<appender-ref ref="htmlFile" />
</root>
</configuration>
日誌輸出: 在d:/logs
目錄下生成一個logback.html
檔案
7.3 Logback 日誌拆分壓縮 ⭐
在生產環境中對日誌進行按時間、日誌大小拆分 且壓縮日誌非常非常重要,所以單獨拿出來說一說
配置檔案
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!--新增一個名字為pattern的屬性 用來設定日誌輸出可是-->
<!--格式化輸出:%d表示日期,%thread表示執行緒名,%-5level:級別從左顯示5個字元寬度%msg:日誌訊息,%n是換行符-->
<property name="pattern" value="[%-5level] %d{yyyy-MM-dd HH:mm:ss} %c %M %L [%thread] %m %n" />
<!--設定日誌檔案存放路徑-->
<property name="log_file" value="d:/logs"></property>
<!--輸出到檔案-->
<appender name="rollFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--引用上面配置好的pattern屬性-->
<pattern>${pattern}</pattern>
</encoder>
<!--被寫入的檔名,可以是相對目錄,也可以是絕對目錄,如果上級目錄不存在會自動建立,沒有預設值。-->
<file>${log_file}/roll_logback.log</file>
<!--滾動記錄檔案:根據時間來制定滾動策略-->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!--日誌檔案輸出的檔名-->
<fileNamePattern>${log_file}/roll_logback.%d{yyyy-MM-dd}.log%i.gz</fileNamePattern>
<!--指定檔案拆分大小-->
<maxFileSize>1MB</maxFileSize>
<!--日誌檔案保留天數-->
<MaxHistory>3</MaxHistory>
</rollingPolicy>
</appender>
<!--設定日誌級別-->
<root level="ALL">
<!--引用上面配置好的FileAppender將日誌輸出到檔案-->
<appender-ref ref="rollFile" />
</root>
</configuration>
日誌滾動輸出: 按照日期和檔案大小進行拆分
7.4 非同步日誌
我們先來解釋下什麼是非同步日誌?
我們將日誌輸出到檔案中,這樣會涉及到大量io
操作,非常耗時,如果需要輸出大量的日誌,就可能影響正常的主執行緒業務邏輯。
為了解決這問題,非同步日誌就出現了。日誌資訊不是直接寫入到日誌檔案或者控制檯,而是先傳送到一個佇列裡,
然後由一個專門的執行緒去處理這些日誌資訊的寫入工作。
這樣做的好處是可以減少日誌記錄對主程式執行的影響,提高程式的效率。
7.4.1 不加非同步日誌
private static final Logger logger = LoggerFactory.getLogger(LogbackTest.class);
@Test
public void testLogbackQuick(){
//日誌輸出
logger.error("這是一個錯誤日誌");
logger.warn("這是一個警告日誌");
logger.info("這是一個資訊日誌");
logger.debug("這是一個除錯日誌");
logger.trace("這是一個跟蹤日誌");
//這裡模擬業務邏輯
System.out.println("曉凡今年18歲了");
System.out.println("曉凡的個人部落格是:www.xiezhrspace.cn");
System.out.println("曉凡的個人公眾號是:程式設計師曉凡");
System.out.println("曉凡的個人微信是:xie_zhr");
System.out.println("歡迎關注曉凡,持續輸出乾貨!!!!!");
}
輸出結果:
從上面控制檯輸出看,只有當日志輸出完成之後我們的業務邏輯程式碼才被執行。如果日誌耗時比較長,非常影響效率
7.4.2 新增非同步日誌
我們只需在原來的配置檔案中新增如下關鍵配置
<!--新增非同步日誌配置-->
<appender name="async" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="console" />
</appender>
<root level="ALL">
<!--引用上面配置好的consoleAppender將日誌輸出到控制檯-->
<appender-ref ref="console" />
<!--引用上面配置好的asyncAppender將日誌輸出到控制檯-->
<appender-ref ref="async" />
</root>
日誌輸出效果:
從上面日誌日誌輸出看,不再是日誌輸出完再進行業務邏輯程式碼執行,而是非同步執行了
八、Log4j2日誌框架
官網:https://logging.apache.org/log4j/2.x/
Log4j2
是Log4j
的升級版,參考了Logback
的一些優秀設計,修復了一些bug,效能和功能都帶來了極大提升
主要體現在以下幾個方面
-
效能提升:
Log4j2
在多執行緒環境下表現出更高的吞吐量,比Log4j 1.x
和Logback
高出10倍 -
非同步日誌:
Log4j2
支援非同步日誌記錄,可以透過AsyncAppender
或AsyncLogger
實現。非同步日誌可以減少日誌記錄對主程式效能的影響,尤其是在高併發場景下 -
自動過載配置:
Log4j2
支援動態修改日誌級別而不需要重啟應用,這是借鑑了Logback
的設計 -
無垃圾機制:
Log4j2
大部分情況下使用無垃圾機制,避免因頻繁的日誌收集導致的JVM GC2
。 -
異常處理:
Log4j2
提供了異常處理機制,Appender 中的異常可以被應用感知到,而Logback
中的異常不會被應用感知
Log4j2
有這麼多優勢,所以在未來SLF4j
+Log4j2
組合
8.1 快速入門
Log4j2不僅僅是日誌實現,同時也是日誌門面。在快速入門中,我們就使用Log4j2作為日誌門面和日誌實現來快速入門
8.1.1 新增依賴
<!--新增log4j2日誌門面API-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.23.1</version>
</dependency>
<!--新增log4j2日誌實現-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.23.1</version>
</dependency>
8.1.2 新增日誌實現程式碼
public class Log4j2Test {
private static final Logger logger = LogManager.getLogger(Log4j2Test.class);
@Test
public void Log4j2Test(){
logger.fatal("這是一條致命資訊");
logger.error("這是一條錯誤資訊");
logger.warn("這是一條警告資訊");
logger.info("這是一條一般資訊");
logger.debug("這是一條除錯資訊");
logger.trace("這是一條追蹤資訊");
}
}
日誌輸出結果如下
8.2 使用slf4j+log4j2組合
前面我們提到
SLF4j
+Log4j2
組合會是未來日誌發展的大趨勢,所以接下來我們就使用這個組合來輸出日誌
匯入依賴
<!--新增log4j2日誌門面API-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.23.1</version>
</dependency>
<!--新增log4j2日誌實現-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.23.1</version>
</dependency>
<!--新增slf4j作為日誌門面,使用log4j2作為日誌實現-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.13</version>
</dependency>
<!--新增log4j2與slf4j的橋接器-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.23.1</version>
</dependency>
日誌輸出程式碼
public class Log4j2Test {
//這裡我們換成了slf4j的門面介面
private static final Logger logger = LoggerFactory.getLogger(Log4j2Test.class);
@Test
public void Log4j2Test(){
logger.error("這是一條錯誤資訊");
logger.warn("這是一條警告資訊");
logger.info("這是一條一般資訊");
logger.debug("這是一條除錯資訊");
logger.trace("這是一條追蹤資訊");
}
}
日誌輸出效果
8.3 Log4j2配置
log4j2 預設載入classpath 下的 log4j2.xml 檔案中的配置。
下面透過log4j2.xml 配置檔案進行測試,配置大同小異,這裡就不一一說明了,給出完整的配置
<?xml version="1.0" encoding="UTF-8" ?>
<!--status="warn" 日誌框架本身的輸出日誌級別,可以修改為debug monitorInterval="5" 自動載入配置檔案的間隔時間,不低於 5秒;生產環境中修改配置檔案,是熱更新,無需重啟應用 -->
<configuration status="warn" monitorInterval="5">
<!--集中配置屬性進行管理 使用時透過:${name} -->
<properties>
<property name="LOG_HOME">D:/logs</property>
</properties>
<!--日誌處理 -->
<Appenders>
<!--控制檯輸出 appender,SYSTEM_OUT輸出黑色,SYSTEM_ERR輸出紅色 -->
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] [%-5level] %c{36}:%L --- %m%n" />
</Console>
<!--日誌檔案輸出 appender -->
<File name="file" fileName="${LOG_HOME}/file.log">
<PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l %c{36} - %m%n" />
</File>
<!-- 使用隨機讀寫流的日誌檔案輸出 appender,效能提高 -->
<RandomAccessFile name="accessFile" fileName="${LOG_HOME}/access.log">
<PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l %c{36} - %m%n" />
</RandomAccessFile>
<!--按照一定規則拆分的日誌檔案的appender -->
<!-- 拆分後的檔案 -->
<!-- 拆分後的日誌檔案命名規則:log-debug.log、log-info.log、log-error.log -->
<RollingFile name="rollingFile" fileName="${LOG_HOME}/rolling.log"
filePattern="${LOG_HOME}/$${date:yyyy-MM-dd}/rolling-%d{yyyy-MM-dd}-%i.log.gz">
<!-- 日誌級別過濾器 -->
<ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY" />
<!-- 日誌訊息格式 -->
<PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l %c{36} - %msg%n" />
<Policies>
<!-- 在系統啟動時,出發拆分規則,生產一個新的日誌檔案 -->
<OnStartupTriggeringPolicy />
<!-- 按照檔案大小拆分,1MB -->
<SizeBasedTriggeringPolicy size="1MB" />
<!--按照時間節點拆分,規則根據filePattern定義的 -->
<TimeBasedTriggeringPolicy />
</Policies>
<!-- 在同一個目錄下,檔案的個限定為 10個,超過進行覆蓋 -->
<DefaultRolloverStrategy max="10" />
</RollingFile>
</Appenders>
<!-- logger 定義 -->
<Loggers>
<!--使用 rootLogger 配置 日誌級別 level="trace" -->
<Root level="trace">
<!--指定日誌使用的處理器 -->
<AppenderRef ref="Console" />
<!-- <AppenderRef ref="file"/>-->
<AppenderRef ref="rollingFile" />
<AppenderRef ref="accessFile" />
</Root>
</Loggers>
</configuration>
日誌輸出如下
下面的截圖為2024-08-11的日誌按日誌檔案大小1MB拆分成10個並進行壓縮,拆分滿10個檔案後新日誌會覆蓋舊日誌,其他天的類似
8.4 Log4j2 非同步日誌
Log4j2
最大的特點就是非同步日誌,就因為非同步日誌的存在,將效能提升了好多。
下圖是官網給的效能對比圖,從圖中我們可以看出在全域性非同步模式(Loggers all async) 和混合非同步模式(Loggers mixed sync/async)
效能簡直將Logback
和Log4j
日誌框架甩了一條街。
至於什麼時全域性非同步模式和混合非同步模式?我們會在後面詳細說明
8.4.1 陌生名詞解釋
-
同步日誌:想象一下你手裡有一堆信件要寫,每寫一封信你都得親自動手,寫完後才能去做別的事情。在這個過程中,你得一封一封地寫,不能同時幹其他事,這就類似於同步日誌。在程式中,同步日誌意味著每次記錄日誌時,程式都得停下來,等待日誌寫完了才能繼續執行其他任務。這樣做的好處是不會丟信(日誌),但壞處是寫信(記錄日誌)這個過程如果太慢,就會耽誤你做其他事情(程式執行)
-
非同步日誌:如果你特別忙,你可能會找個助手來幫你寫信。你只需要告訴他要寫什麼,然後就可以繼續忙自己的事情,而助手會幫你把信寫好並寄出去。這個過程就像是非同步日誌。在程式中,非同步日誌意味著程式可以把要記錄的日誌資訊交給一個專門的“助手”(通常是另外的執行緒或程序),然後程式就可以繼續執行其他任務,而不需要等待日誌寫完。這樣做的好處是可以更快地處理任務,不會耽誤正事兒,但偶爾可能會有一兩封信(日誌)因為意外情況沒有寄出去。
-
全域性非同步: 所有日誌記錄都採用非同步的方式記錄
-
混合非同步:以在應用中同時使用同步日誌和非同步日誌,這使得日誌配置更加靈活
8.4.2 同步日誌與非同步日誌
- 同步日誌流程
2、非同步日誌流程
8.5 非同步日誌配置
非同步日誌的實現一共有兩種方式
-
AsyncAppender
[生產上幾乎不使用,因為效能低下] -
AsyncLogger
[生產上用得多,因為效能高]- 全域性非同步
- 混合非同步
第一種方式因為用的不多效能也不夠好,所以這裡就不說了,我們以第二種配置來具體說一說
不管採用哪種方式,首先都要引入非同步依賴
<!--非同步日誌依賴-->
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.4.4</version>
</dependency>
① 全域性非同步
只需在resources
下新增log4j2.component.properties
,具體內容如下
Log4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
日誌輸出結果
② 混合非同步配置
首先,我們需要關閉全域性非同步配置,將上面新增的log4j2.component.properties
內容註釋即可
log4j2.xml
配置
<?xml version="1.0" encoding="UTF-8" ?>
<!--status="warn" 日誌框架本身的輸出日誌級別,可以修改為debug monitorInterval="5" 自動載入配置檔案的間隔時間,不低於 5秒;生產環境中修改配置檔案,是熱更新,無需重啟應用 -->
<configuration status="debug" monitorInterval="5">
<!--集中配置屬性進行管理 使用時透過:${name} -->
<properties>
<property name="LOG_HOME">D:/logs</property>
</properties>
<!--日誌處理 -->
<Appenders>
<!--控制檯輸出 appender,SYSTEM_OUT輸出黑色,SYSTEM_ERR輸出紅色 -->
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] [%-5level] %c{36}:%L --- %m%n" />
</Console>
<!--日誌檔案輸出 appender -->
<File name="file" fileName="${LOG_HOME}/file.log">
<!-- <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l %c{36} - %m%n" />-->
<PatternLayout pattern="%d %p %c{1.} [%t] %m%n" />
</File>
<Async name="Async">
<AppenderRef ref="file" />
</Async>
</Appenders>
<!--logger 定義 -->
<Loggers>
<!--自定義 logger 物件 includeLocation="false" 關閉日誌記錄的行號資訊,開啟的話會嚴重影響非同步輸出的效能 additivity="false" 不再繼承 rootlogger物件 -->
<AsyncLogger name="com.xiezhr" level="trace" includeLocation="false" additivity="false">
<AppenderRef ref="Console" />
</AsyncLogger>
<!-- 使用 rootLogger 配置 日誌級別 level="trace" -->
<Root level="trace">
<!-- 指定日誌使用的處理器 -->
<AppenderRef ref="Console" />
<!-- 使用非同步 appender -->
<AppenderRef ref="Async" />
</Root>
</Loggers>
</configuration>
輸出結果:
注意事項:
- 上面配置
AsyncAppender
、全域性配置、混合配置 不能同時出現,否則將影響日誌效能 includeLocation="false"
關閉日誌記錄的行號資訊 配置一定要加上,否則會降低日誌效能
九、阿里巴巴日誌規約
透過上面八小節我們對Java日誌框架應該非常熟悉了,並且也知道怎麼使用了。但在日誌開發中,使用日誌還是有寫規約需要我們去遵守。
下面式阿里巴巴Java開發手冊中的日誌規約
❶【強制】應用中不可直接使用日誌系統(Log4j
、Logback
)中的 API
,而應依賴使用日誌框架(SLF4J
、JCL--Jakarta Commons Logging
)中的 API
,使用門面模式的日誌框架,有利於維護和各個類的日誌處理方式統一。
說明:日誌框架(SLF4J、JCL--Jakarta Commons Logging)的使用方式(推薦使用 SLF4J)
1)使用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);
❷【強制】所有日誌檔案至少儲存 15 天,因為有些異常具備以“周”為頻次發生的特點。對於當天日誌,以“應用名.log
”來儲存,
儲存在”/home/admin/應用名/logs/
“目錄下,過往日誌格式為: {logname}.log.{儲存日期}
,日期格式:yyyy-MM-dd
正例:以 aap 應用為例,日誌儲存在/home/admin/aapserver/logs/aap.log
,歷史日誌名稱為aap.log.2021-03-23
❸【強制】根據國家法律,網路執行狀態、網路安全事件、個人敏感資訊操作等相關記錄,留存的日誌不少於六個月,並且進行網路多機備份。
❹【強制】應用中的擴充套件日誌(如打點、臨時監控、訪問日誌等)命名方式:appName_logType_logName.log
。
-
logType
:日誌型別,如stats/monitor/access
等; -
logName
:日誌描述。這種命名的好處:透過檔名就可知道日誌檔案屬於什麼應用,什麼型別,什麼目的,也有利於歸類查詢。
說明:推薦對日誌進行分類,如將錯誤日誌和業務日誌分開存放,便於開發人員檢視,也便於透過日誌對系統進行及時監控。
正例:mppserver
應用中單獨監控時區轉換異常,如:
mppserver_monitor_timeZoneConvert.log
❺ 【強制】在日誌輸出時,字串變數之間的拼接使用佔位符的方式。
說明:因為 String 字串的拼接會使用
StringBuilder
的append()
方式,有一定的效能損耗。使用佔位符僅是替換動作,可以有效提升效能。
正例:
logger.debug("Processing trade with id: {} and symbol: {}", id, symbol);
❻【強制】對於 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());
}
❼【強制】避免重複列印日誌,浪費磁碟空間,務必在日誌配置檔案中設定 additivity=false
。
正例:
<logger name="com.taobao.dubbo.config" additivity="false">
❽ 【強制】生產環境禁止直接使用 System.out
或 System.err
輸出日誌或使用e.printStackTrace()
列印異常堆疊 。
說明:標準日誌輸出與標準錯誤輸出檔案每次
Jboss
重啟時才滾動,如果大量輸出送往這兩個檔案,容易造成檔案大小超過作業系統大小限制。
❾ 【強制】異常資訊應該包括兩類資訊:案發現場資訊和異常堆疊資訊。如果不處理,那麼透過關鍵字 throws
往上丟擲。
正例:
logger.error("inputParams:{} and errorMessage:{}", 各類引數或者物件 toString(), e.getMessage(), e);
❿ 【強制】日誌列印時禁止直接用 JSON 工具將物件轉換成 String
。
說明:如果物件裡某些
get
方法被覆寫,存在丟擲異常的情況,則可能會因為列印日誌而影響正常業務流程的執行。
正例:
列印日誌時僅列印出業務相關屬性值或者呼叫其物件的 toString()
方法。
⓫ 【推薦】謹慎地記錄日誌。生產環境禁止輸出 debug
日誌;有選擇地輸出 info
日誌;
如果使用 warn
來記錄剛上線時的業務行為資訊,一定要注意日誌輸出量的問題,避免把伺服器磁碟撐爆,並記得及時刪除這些觀察日誌。
說明:大量地輸出無效日誌,不利於系統效能提升,也不利於快速定位錯誤點。記錄日誌時請思考:這些日誌真的有人看嗎?看到這條日誌你能做什麼?能不能給問題排查帶來好處?
⓬ 【推薦】可以使用 warn
日誌級別來記錄使用者輸入引數錯誤的情況,避免使用者投訴時,無所適從。
說明:如非必要,請不要在此場景打出
error
級別,避免頻繁報警。 注意日誌輸出的級別,error
級別只記錄系統邏輯出錯、異常或者重要的錯誤資訊。
⓭ 【推薦】儘量用英文來描述日誌錯誤資訊,如果日誌中的錯誤資訊用英文描述不清楚的話使用中文描述即可,否則容易產生歧義。
說明:國際化團隊或海外部署的伺服器由於字符集問題,使用全英文來註釋和描述日誌錯誤資訊。
本期內容到這兒就結束了 ★,°:.☆( ̄▽ ̄)/$:.°★ 。 希望對您有所幫助
我們下期再見 ヾ(•ω•`)o (●'◡'●)