萬字長文帶你瞭解Java日誌框架使用Java日誌框架

程序员晓凡發表於2024-08-14

大家好,我是曉凡

一、日誌概念

日誌的重要性不用我多說了,日誌,簡單來說就是記錄。

用來記錄程式執行時發生的事情。比如,程式啟動了、執行了某個操作、遇到了問題等等,這些都可以透過日誌記錄下來。

想象一下,你開了一家店,每天的營業額、顧客的反饋、商品的進出、庫存等等,你都會記錄下來。這就像是程式的日誌。比如:

  • 電商網站:記錄使用者的登入、瀏覽、購買行為,監控交易過程,及時發現異常交易;透過日誌分析你的瀏覽記錄,實現精準推送等等
  • 伺服器:記錄伺服器的啟動、執行、關閉狀態,以及發生的各種錯誤,幫助管理員及時發現並解決問題。

1.1 日誌的作用

  1. 除錯幫助:當程式出現問題時,透過檢視日誌,可以快速定位問題發生的地方和原因。
  2. 監控執行狀態:透過日誌可以瞭解程式的執行狀態,比如使用者的操作、系統的效能等。
  3. 安全審計:在需要記錄使用者行為或系統操作的場合,日誌可以作為審計的依據。

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軟體基金會開發,相比Log4jLog4j2在效能上有顯著提升,同時保持了豐富的功能,支援非同步日誌處理,適合高效能需求的場景

  • Logback

​ 由Log4j的原開發者之一主導開發,Spring Boot預設日誌,輕量級,效能優秀,功能也比較全面

三、JUL日誌框架

3.1 主要元件

  1. Logger:日誌記錄器,是日誌系統的核心,用來生成日誌記錄。
  2. Handler:日誌處理器,負責將日誌資訊輸出到不同的目的地,比如控制檯、檔案等。可以為每個Logger配置一個或多個Handler
  3. Formatter:日誌格式化器,負責定義日誌的輸出格式。比如時間戳、日誌級別、訊息等。
  4. Level:設定日誌級別,常見的級別有SEVEREWARNINGINFOCONFIGFINEFINERFINEST等。
  5. Filter: 這個元件用來過濾日誌記錄。你可以設定一些規則,只有滿足這些規則的日誌才會被記錄。
  6. Log Record:這是日誌記錄本身,包含了日誌的所有資訊,比如時間、日誌級別、訊息等

3.2 使用步驟

  1. 獲取Logger例項。
  2. 新增Handler
  3. 為上一步新增的Handler 設定日誌級別(Level)和格式輸出(Formatter
  4. 建立Filter過濾器
  5. Logger例項新增日誌處理器(Handler)和日誌過濾器(Filter
  6. 記錄日誌。

jul使用步驟

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

    }
}

JUT入門案例

3.4 日誌級別

日誌級別系統,用來區分日誌的重要性

3.4.1 日誌級別
  1. SEVERE(嚴重):這是最高階別的日誌,用來記錄嚴重錯誤,比如系統崩潰、資料丟失等。這類日誌通常需要立即關注和處理。
  2. WARNING(警告):用來記錄可能不會立即影響系統執行,但可能表明潛在問題的資訊。比如,某個操作沒有達到預期效果,或者系統資源接近耗盡。
  3. INFO(資訊):用來記錄一般性的資訊,比如程式執行的狀態、重要的操作步驟等。這類資訊對於瞭解程式的執行情況很有幫助,但通常不需要立即處理。
  4. CONFIG(配置):用來記錄配置資訊,比如程式啟動時載入的配置檔案、初始化的引數等。這類日誌有助於除錯和驗證程式的配置是否正確。
  5. FINE(詳細):用來記錄更詳細的資訊,比如程式內部的執行細節、變數的值等。這類日誌對於開發者在除錯程式時瞭解程式的內部狀態非常有用。
  6. FINER(更詳細):比FINE級別更細的日誌,記錄更深入的執行細節。通常用於深入分析程式的執行情況。
  7. 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("這條資訊很重要,將被記錄到控制檯和檔案");
    }
}       

① 控制檯日誌輸出

1、控制檯輸出結果

②日誌檔案輸出 app.log內容

2、檔案中輸出日誌

程式碼解釋

  1. Logger獲取:首先獲取一個名為LoggingExampleLogger例項。
  2. 設定日誌級別:將Logger的日誌級別設定為INFO,這意味著INFO及以上級別的日誌將被記錄。
  3. 控制檯Handler:建立一個ConsoleHandler例項,設定其日誌級別為WARNING,並且自定義了日誌的輸出格式。
  4. 檔案Handler:嘗試建立一個FileHandler例項,將日誌寫入到app.log檔案中,並設定其日誌級別為ALL,意味著所有級別的日誌都將被記錄到檔案。
  5. 自定義Formatter:為Handler建立自定義的SimpleFormatter,用於定義日誌的輸出格式。
  6. Filter設定:建立一個實現了Filter介面的匿名內部類,並重寫isLoggable方法,實現過濾邏輯,這裡只記錄訊息中包含"important"字串的日誌。
  7. 應用Filter:將建立的Filter應用到Logger上。
  8. 記錄日誌:記錄不同級別的日誌,展示不同級別的日誌如何被Handler和Filter處理。
  9. 日誌記錄:一些日誌將根據設定的日誌級別、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檔案輸出:

ava0.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這個方法的使用

new SimpleFormatter

3.7.1 Stringformat方法

Stringformat方法用來格式化字串。

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

log4j官網

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中,日誌級別從低到高分為以下幾種:

  1. TRACE:追蹤級別,通常用來記錄程式執行的詳細軌跡,比如方法呼叫的順序等。這個級別非常詳細,一般在開發階段或者除錯時用得比較多。
  2. DEBUG:除錯級別,用來記錄程式的執行狀態,比如變數的值、程式的流程等。當你需要深入瞭解程式的內部工作時,DEBUG級別就非常有用。
  3. INFO:資訊級別,用來記錄程式的正常執行狀態,比如程式啟動、配置資訊、正常結束等。INFO級別的日誌對使用者和開發者瞭解程式的執行情況很有幫助。
  4. WARN:警告級別,用來記錄一些可能引起問題的情況,但程式仍然可以繼續執行。比如,程式遇到了一個不常見的情況,或者某個操作失敗了但不影響大局。
  5. ERROR:錯誤級別,用來記錄程式執行中的錯誤,這些錯誤通常會影響程式的正常功能,但程式可能還能繼續執行。
  6. FATAL:致命級別,用來記錄非常嚴重的錯誤,這些錯誤會導致程式完全無法繼續執行。比如,程式的某個關鍵部分失敗了,整個應用可能需要重啟。

出了上面的,還有以下兩個特殊級別

1. **OFF**: 用來關閉日誌記錄
1. **ALL**: 啟用所有訊息的日誌記錄

4.3 Log4j元件

  1. Logger:這個元件就像是日誌的大腦,負責記錄日誌資訊。你可以想象它是一個日記本的主人,決定哪些事情值得記錄,哪些事情可以忽略。
  2. Appender:Appender就像是日記本的筆,它決定了日誌資訊要寫到哪裡。可以是控制檯、檔案、資料庫,甚至是透過網路傳送到遠端伺服器。每種Appender都有不同的用途和特點。
  3. 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,不過這個功能非常簡單

jcl實現圖

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

日誌輸出如下:

log4j日誌輸出

我們可以看到,使用了JCL日誌門面之後,我們從simplelog日誌框架切換到log4j日誌框架,沒有改過程式碼。

六、SLF4j日誌門面

SLF4j 全稱是Simple Logging Facade For JavaJava簡單的日誌門面 和上一小節說到的JCL乾的一樣的活。

在現目前的大多數Java專案中,日誌框架基本上會選擇slf4j-api 作為門面,配上具體實現框架logbacklog4j 等使用

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

小夥伴看到上圖可能會有點懵,全是英文,看不懂。

一臉懵逼

於是乎,曉凡簡單翻譯了一下,如下如所示

slf4j實現原理圖

  • 只匯入日誌門面,沒匯入日誌實現,不會進行日誌輸出
  • logbacksimplelogno-operation 框架遵循SLF4j規範 匯入jar包即可使用
  • log4jJUL 屬於比較古老日誌框架,不遵循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日誌框架輸入日誌如下所示

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

③ 程式碼不變,日誌輸出如下

log4j日誌輸出

6.2.5 使用介面卡繫結JUL日誌框架

① 引入依賴

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-jdk14</artifactId>
    <version>2.0.13</version>
</dependency>

② 程式碼不變,日誌輸出如下

jul日誌輸出結果

6.3 SLF4j日誌橋接

6.3.1 使用場景

如果你的專案中已經使用了Log4j 1.x等老的日誌框架,但你想遷移到使用SLF4JAPI,這時候你可以使用SLF4JLog4j 1.x橋接器來平滑過渡

6.3.2 橋接原理

橋接器原理

上圖為SLF4j官網提供的橋接原理圖,從圖中,我們可以看到,只需要引入不同的橋接器log4j-over-slf4jjul-to-slf4jjcl-over-slf4j

就可以實現在不改變原有程式碼的情況下,將日誌從log4jjuljcl遷移到slf4j+logback日誌組合

6.3.3 橋接步驟

下面以Log4j 1.x遷移到slf4j+logback日誌組合為例

  1. 去除老的日誌框架Log4j 1.x依賴

去除老專案中的日誌依賴

  1. 新增SLF4J提供的橋接元件

    引入橋接器

  2. 為專案新增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("這是一個跟蹤日誌");
    }
}

Logback列印日誌

7.2 Logback配置

Logback可以透過程式設計式配置(新增配置類的方式),也可以透過配置檔案配置。

配置檔案是日常開發中最常用的,我們這裡就以這種方式配置,如果對配置檔案感興趣的小夥伴可自行到官網檢視

7.2.1 Logback 包含哪些元件?
  1. Logger:日誌記錄器,用來記錄不同級別的日誌資訊,比如錯誤、警告、資訊、除錯和追蹤。
  2. Appender:指定日誌資訊輸出到不同的地方。比如,你可以設定一個Appender將日誌輸出到控制檯,另一個Appender將日誌寫入檔案,或者傳送到遠端伺服器。
  3. Encoder:如果你使用的是檔案Appender,Encoder就是用來定義日誌檔案內容格式的。比如,你可以選擇日誌的格式是簡單文字還是XML。
  4. Layout:老版本的Logback中用來定義日誌格式的元件。在新版本中,Encoder已經取代了Layout的功能。
  5. Filter:指定特定的規則來過濾日誌資訊,比如只記錄錯誤以上的日誌,或者只記錄包含特定關鍵字的日誌。
  6. Configuration:用來配置Logback的設定,比如設定日誌級別、Appender的型別和引數等。配置可以透過XMLJSON或者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 檔案

image-20240810171050844

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/

Log4j2Log4j的升級版,參考了Logback的一些優秀設計,修復了一些bug,效能和功能都帶來了極大提升

主要體現在以下幾個方面

  • 效能提升: Log4j2 在多執行緒環境下表現出更高的吞吐量,比 Log4j 1.xLogback 高出10倍

  • 非同步日誌Log4j2 支援非同步日誌記錄,可以透過 AsyncAppenderAsyncLogger 實現。非同步日誌可以減少日誌記錄對主程式效能的影響,尤其是在高併發場景下

  • 自動過載配置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("這是一條追蹤資訊");

    }
}

日誌輸出結果如下

log4j2 日誌輸出結果

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("這是一條追蹤資訊");

    }
}

日誌輸出效果

slf4j+log4j2組合日誌輸出

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)

效能簡直將LogbackLog4j日誌框架甩了一條街。

至於什麼時全域性非同步模式和混合非同步模式?我們會在後面詳細說明

日誌框架效能比較

8.4.1 陌生名詞解釋
  • 同步日誌:想象一下你手裡有一堆信件要寫,每寫一封信你都得親自動手,寫完後才能去做別的事情。在這個過程中,你得一封一封地寫,不能同時幹其他事,這就類似於同步日誌。在程式中,同步日誌意味著每次記錄日誌時,程式都得停下來,等待日誌寫完了才能繼續執行其他任務。這樣做的好處是不會丟信(日誌),但壞處是寫信(記錄日誌)這個過程如果太慢,就會耽誤你做其他事情(程式執行)

  • 非同步日誌:如果你特別忙,你可能會找個助手來幫你寫信。你只需要告訴他要寫什麼,然後就可以繼續忙自己的事情,而助手會幫你把信寫好並寄出去。這個過程就像是非同步日誌。在程式中,非同步日誌意味著程式可以把要記錄的日誌資訊交給一個專門的“助手”(通常是另外的執行緒或程序),然後程式就可以繼續執行其他任務,而不需要等待日誌寫完。這樣做的好處是可以更快地處理任務,不會耽誤正事兒,但偶爾可能會有一兩封信(日誌)因為意外情況沒有寄出去。

  • 全域性非同步: 所有日誌記錄都採用非同步的方式記錄

  • 混合非同步:以在應用中同時使用同步日誌和非同步日誌,這使得日誌配置更加靈活

8.4.2 同步日誌與非同步日誌
  1. 同步日誌流程

同步日誌流程

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開發手冊中的日誌規約

❶【強制】應用中不可直接使用日誌系統(Log4jLogback)中的 API,而應依賴使用日誌框架(SLF4JJCL--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);
  1. 使用 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 字串的拼接會使用 StringBuilderappend()方式,有一定的效能損耗。使用佔位符僅是替換動作,可以有效提升效能。

正例:

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.outSystem.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 (●'◡'●)

相關文章