列印 Logger 紀錄檔時,需不需要再封裝一下工具類?

2022-09-26 12:01:06

在開發過程中,列印紀錄檔是必不可少的,因為紀錄檔關乎於應用的問題排查、應用監控等。現在列印紀錄檔一般都是使用 slf4j,因為使用紀錄檔門面,有助於列印方式統一,即使後面更換紀錄檔框架,也非常方便。在 《Java 開發手冊》中也有相關的規約。

所以在開發中,一般使用下面這種方式來列印紀錄檔。

LOGGER.info("print: {}", "this is the log");

不過有的應用會將 LOGGER 再封裝一下,最終寫成:

LoggerUtil.info(LOGGER, "print: {}", "this is the log");

本文的主要內容是討論為什麼要封裝,有沒有必要封裝,以及怎樣封裝,如果小夥伴有更好的建議,可以提出,進行互相學習。

為什麼要封裝

很多人覺得 slf4j 本來就是紀錄檔門面,已經封裝的很好了,為什麼要多此一舉,再額外封裝一個 LoggerUtil 呢?

其實這塊也是在開發規範中有說明的:

如果不進行封裝,則會寫成下面這種:

if (LOGGER.isInfoEnabled()) {
    LOGGER.info("print: {}", "this is the log");
}

所以,一般封裝是將 if 判斷這塊邏輯統一封裝為一個工具類。

可能到這裡還有小夥伴不是很理解為什麼要加 if 判斷,可以看下下面這段程式碼:

可以看出轉換邏輯這塊相對比較複雜、耗時,在這裡只是模擬的場景,實際使用可能會有其他情況,比如列印方法的出參入參、計算耗時等:

LOGGER.info("xxx 方法請求引數為:{}", JSON.toJSONString(req));
LOGGER.info("xxx 執行耗時:{}ms", System.currentTimeMillis() - startTime);

在某些場景下為了提高效能,需要關閉紀錄檔,比如大促,秒殺等等。

說到這裡相信小夥伴已經看出問題了,因為這樣寫的話,當我關閉紀錄檔列印時,只是關閉了磁碟輸出,但是耗時邏輯依然會繼續執行。

# 紀錄檔級別調整到 error
logging.level.com.liuzhihang=error

這也是為什麼在開發規範中建議大家手寫判斷,雖然紀錄檔框架中幫我們進行了判斷,那只是避免了列印輸出紀錄檔,實際上像組裝紀錄檔,序列化範例物件等等還是會被執行的。

當然如果當前應用只有個位數的 tps 或者 tpm 那完全沒必要考慮這些,也沒必要因噎廢食,正常使用就行。

該怎樣封裝

為了避免每次都要 if 判斷的問題,會將 if 模組封裝為工具類:

上面的封裝,有效避免了每次都需要進行判斷,只需要將程式碼中的列印紀錄檔換成 LogUtil 即可:

但是這種情況只能避免列印既有引數時的 if 判斷,對方法型別的沒有作用,這裡就需要使用 Supplier

實際使用效果:

以上僅為一種封裝方式,其他的封裝可以自行考慮,比如整個紀錄檔框架都封裝。

其他使用

這部分封裝在 log4j-api-2.17.2.jar 中也有所體現,只不過 slf4j 裡面並沒有封裝 Supplier 支援,詳細實現可以自行閱讀原始碼。

那為什麼 slf4j 不支援,其實也是有討論的,可以看 issue #70,裡面進行了一系列討論。

最終結果是在 2.0 支援了 Fluent Logging API 語法。

slf4j 2.0 使用

<!-- slf4j 2.0 依賴 -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>2.0.1</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-simple</artifactId>
    <version>2.0.0</version>
</dependency>

按照官方檔案的使用案例直接使用即可:

logger.atDebug()
    .setMessage("Temperature set to {}. Old value was {}.")
    .addArgument(() -> t16()).addArgument(oldT)
    .log();

為什麼要這樣寫,只能說是人家的 API 設計就是如此,當然也有其他的考慮,可以看看 github issue。具體使用哪種,用不用封裝等等,這些都是根據自己的實際情況來使用。