Java紀錄檔框架的依賴設定備查(SLF4J, Log4j, Logback)

2023-08-26 18:01:26
example

前言

最近在看程式碼的過程中,發現身邊的許多人在使用Java紀錄檔框架時,對於應該引入何種依賴不甚瞭解,搜尋網路上的文章,常常也是互不一致。這篇文章可以看著是Java紀錄檔框架的入門使用和實踐建議,重點介紹不同組合方式下的依賴設定及其背後的邏輯,一方面給自己備查,另外也希望對小夥伴們有所幫助。

Java紀錄檔框架家族繁雜,出於實用的原則,這裡主要介紹主流的幾個專案:SLF4J、Logback、Log4j 2,以及它們之間各種搭配用法和使用建議。

另外,由於Log4j 1專案已經在2015-08-05正式宣佈死亡(最終版本停留在2012-05-13釋出的1.2.17),因此這裡也不再討論Log4j 1,下文所有提到Log4j的地方,都是指Log4j 2。



簡述

對於紀錄檔框架,可以按照「分層」的概念來理解:介面層、實現層。開發者在使用紀錄檔框架時,建議基於介面層而非實現層進行開發,這樣的好處是,避免專案與某一個具體紀錄檔框架耦合,這是一個常見的程式設計理念,應該比較容易理解。

例如,專案最初使用SLF4J作為介面層,使用Logback作為實現層,而你的專案程式碼中使用的也是介面層的類org.slf4j.Logger,這種情況下,當將來你想將實現層切換為Log4j時,你最需要改動依賴項,而不需要改動程式碼。

但是,如果你最初的專案程式碼中使用的並非是介面層的類,而是實現層(即Logback)的類ch.qos.logback.classic.Logger(這可能是因為手滑,畢竟類名都是一樣的)。這種情況下,想要切換實現層,就需要改動所有涉及使用到這個類的程式碼。



SLF4J + Logback

依賴設定

由於logback-classic中既有實現層,也包含了對介面層SLF4J的依賴,因此,最簡單的設定可以是這樣的:

<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.12</version>
</dependency>

不過,就像簡述裡說的,為了避免開發者不小心誤用實現類ch.qos.logback.classic.Logger,推薦使用如下的依賴設定,注意其中的scope設定:

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.32</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.12</version>
    <scope>runtime</scope>
    <exclusions>
        <exclusion>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
        </exclusion>
    </exclusions>
</dependency>

組態檔(logback.xml)

這裡給出一個最常見的組態檔,包含控制檯輸出、捲動檔案輸出:

<configuration>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <appender name="ROLLING_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>logs/app.log</file>

        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>logs/app-%d{yyyy-MM-dd-HH}-%i.log</fileNamePattern>
            <!-- 單個紀錄檔檔案超過10M,則進行卷動,對檔案進行遞增編號(即%i) -->
            <maxFileSize>10MB</maxFileSize>
            <!-- 所有紀錄檔檔案的大小限制,超出則刪除舊檔案 -->
            <totalSizeCap>5GB</totalSizeCap>
            <!-- 與fileNamePattern相結合,本例中由於時間粒度是小時,因此這裡表示儲存48個小時 -->
            <maxHistory>48</maxHistory>
        </rollingPolicy>

        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="CONSOLE" />
        <appender-ref ref="ROLLING_FILE" />
    </root>
</configuration>

程式碼範例

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
...
private static final Logger logger = LoggerFactory.getLogger(App.class);
...
logger.info("First name: {}, last name: {}", firstName, lastName);




SLF4J + Log4j

依賴設定

由於log4j-slf4j-impl中既有實現層,也包含了對介面層SLF4J的依賴,因此,最簡單的設定可以是這樣的:

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-slf4j-impl</artifactId>
    <version>2.20.0</version>
</dependency>

不過,基於與上一節同樣的邏輯,推薦使用下面的設定:

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.25</version>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-slf4j-impl</artifactId>
    <version>2.20.0</version>
    <scope>runtime</scope>
    <exclusions>
        <exclusion>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
        </exclusion>
    </exclusions>
</dependency>

組態檔(log4j2.xml)

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="INFO"> <!-- log4j internal log level -->
    <Appenders>
        <Console name="CONSOLE" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n"/>
        </Console>

        <RollingFile name="ROLLING_FILE"
                     fileName="logs/log4j2/roll-by-time-and-size/app.log"
                     filePattern="logs/log4j2/roll-by-time-and-size/app-%d{yyyy-MM-dd-HH}-%i.log"
                     ignoreExceptions="false">
            <PatternLayout>
                <Pattern>%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n</Pattern>
            </PatternLayout>
            <Policies>
                <!-- 啟動時,會刪除多餘的紀錄檔檔案 -->
                <OnStartupTriggeringPolicy/>
                <!-- 自動感知filePattern中的時間設定,本例中是按小時粒度進行卷動 -->
                <TimeBasedTriggeringPolicy/>
                <!-- 單個紀錄檔檔案超過10M,則進行卷動,遞增編號(即filePattern中的%i) -->
                <SizeBasedTriggeringPolicy size="10M"/>
            </Policies>
            <!-- max設定與上面的filePattern結合,由於本例中是按小時粒度進行卷動,因此這裡表示每小時內最多產生五個編號檔案,超出這回圈覆蓋,如不設定max,則預設為7 -->
            <DefaultRolloverStrategy max="5">
                <Delete basePath="logs" maxDepth="1">
                    <!-- 最近30天,最多5GB的紀錄檔 -->
                    <IfFileName glob="app-*.log">
                        <IfAny>
                            <IfLastModified age="30d"/>
                            <IfAccumulatedFileSize exceeds="5GB"/>
                        </IfAny>
                    </IfFileName>
                </Delete>
            </DefaultRolloverStrategy>
        </RollingFile>

    </Appenders>
    <Loggers>
        <Root level="warn">
            <AppenderRef ref="CONSOLE"/>
            <AppenderRef ref="ROLLING_FILE"/>
        </Root>
    </Loggers>
</Configuration>

程式碼範例

由於與上例相同,都是基於SLF4J介面層,因此使用方式相同:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
...
private static final Logger logger = LoggerFactory.getLogger(App.class);
...
logger.info("First name: {}, last name: {}", firstName, lastName);




單獨使用Log4j

一般我們會基於SLF4J介面層進行開發,但是如果你硬要單獨使用Log4j,也不是不可以。

依賴設定

最簡單的,我們可以使用以下設定:

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.20.0</version>
</dependency>

不過,由於Log4j自身也分了介面層和實現層,推薦使用如下設定:

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>2.20.0</version>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.20.0</version>
    <scope>runtime</scope>
    <exclusions>
        <exclusion>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
        </exclusion>
    </exclusions>
</dependency>

組態檔(log4j2.xml)

(同上)

程式碼範例

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
...
private static final Logger logger = LogManager.getLogger(App.class);
...
logger.info("First name: {}, last name: {}", firstName, lastName);

有人可能會說,Log4j自身拆成了介面層和實現層,是不是意味著,使用Log4j介面層的情況下,實現層還能使用別的紀錄檔系統?是的,例如可以使用「Log4j介面層 + Logback」的搭配:

<!-- 1) Log4j介面層 -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>2.20.0</version>
</dependency>
<!-- 2) Log4j專案提供的「橋接層」,將Log4j介面層橋接到SLF4J介面層,由於Logback是基於SLF4J,因此經過橋接之後,就可以使用Logback作為實現層 -->
<!-- 注:log4j-to-slf4j含有對log4j-api的依賴,因此上面可以不用單獨列出log4j-api依賴,不過,為了邏輯清晰,還是保留 -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-to-slf4j</artifactId>
    <version>2.20.0</version>
</dependency>
<!-- 3) Logback實現層 -->
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.12</version>
    <scope>runtime</scope>
</dependency>



總結

如果你在開發一個玩具專案,對於紀錄檔框架的選擇和使用當然可以比較隨意,但是,如果是開發一個正經的專案,尤其是你的專案將作為公眾可用的第三方庫時,遵循最佳實踐、保持靈活性則是非常必要的,因為你不知道使用方希望在他的專案中使用什麼紀錄檔框架。

另外,我曾經作為面試官的時候,也常常詢問面試者如何設定紀錄檔框架的依賴,這是一個很簡單的題目,不過,一樣可以考察對方几個知識點,包括紀錄檔框架、解耦、Maven中的scope設定等,總之,這是一個不錯的考察程式設計常識的點。