SLF4J門面日誌框架源碼探索

来源:https://www.cnblogs.com/jingdongkeji/archive/2023/06/21/17495528.html
-Advertisement-
Play Games

我們通過代碼入手,層層加碼,直觀感受SLF4J列印日誌,並跟蹤代碼追本溯源。主要瞭解,SLF4J是如何作為門面和其他日誌框架進行解耦。 ...


1 SLF4J介紹

SLF4J即Simple Logging Facade for Java,它提供了Java中所有日誌框架的簡單外觀或抽象。因此,它使用戶能夠使用單個依賴項處理任何日誌框架,例如:Log4j,Logback和JUL(java.util.logging)。通過在類路徑中插入適當的 jar 文件(綁定),可以在部署時插入所需的日誌框架。如果要更換日誌框架,僅僅替換依賴的slf4j bindings。比如,從java.util.logging替換為log4j,僅僅需要用slf4j-log4j12-1.7.28.jar替換slf4j-jdk14-1.7.28.jar。

2 SLF4J源碼分析

我們通過代碼入手,層層加碼,直觀感受SLF4J列印日誌,並跟蹤代碼追本溯源。主要瞭解,SLF4J是如何作為門面和其他日誌框架進行解耦。

2.1 pom只引用依賴slf4j-api,版本是1.7.30

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.30</version>
        </dependency>

2.1.1 執行一個Demo

public class HelloSlf4j {

    public static void main(String[] args) {
        Logger logger = LoggerFactory.getLogger(HelloSlf4j.class);
        logger.info("Hello World info");
    }
}

2.1.2 日誌提示信息

綁定org.slf4j.impl.StaticLoggerBinder失敗。如果在類路徑上沒有找到綁定,那麼 SL​​F4J 將預設為無操作實現

2.1.3 跟蹤源碼

點開方法getLogger(),可以直觀看到LoggerFactory使用靜態工廠創建Logger。通過以下方法,逐步點擊,報錯也很容易找到,可以在bind()方法看到列印的異常日誌信息。

org.slf4j.LoggerFactory#getLogger(java.lang.Class<?>)
org.slf4j.LoggerFactory#getLogger(java.lang.String)
org.slf4j.LoggerFactory#getILoggerFactory
org.slf4j.LoggerFactory#performInitialization
org.slf4j.LoggerFactory#bind

private final static void bind() {
        try {
            Set<URL> staticLoggerBinderPathSet = null;
            // skip check under android, see also
            // http://jira.qos.ch/browse/SLF4J-328
            if (!isAndroid()) {
                staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
                reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
            }
            // the next line does the binding
            StaticLoggerBinder.getSingleton();
            INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
            reportActualBinding(staticLoggerBinderPathSet);
        } catch (NoClassDefFoundError ncde) {
            String msg = ncde.getMessage();
            if (messageContainsOrgSlf4jImplStaticLoggerBinder(msg)) {
                INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
                Util.report("Failed to load class "org.slf4j.impl.StaticLoggerBinder".");
                Util.report("Defaulting to no-operation (NOP) logger implementation");
                Util.report("See " + NO_STATICLOGGERBINDER_URL + " for further details.");
            } else {
                failedBinding(ncde);
                throw ncde;
            }
        } catch (java.lang.NoSuchMethodError nsme) {
            String msg = nsme.getMessage();
            if (msg != null && msg.contains("org.slf4j.impl.StaticLoggerBinder.getSingleton()")) {
                INITIALIZATION_STATE = FAILED_INITIALIZATION;
                Util.report("slf4j-api 1.6.x (or later) is incompatible with this binding.");
                Util.report("Your binding is version 1.5.5 or earlier.");
                Util.report("Upgrade your binding to version 1.6.x.");
            }
            throw nsme;
        } catch (Exception e) {
            failedBinding(e);
            throw new IllegalStateException("Unexpected initialization failure", e);
        } finally {
            postBindCleanUp();
        }
    }

進一步分析綁定方法findPossibleStaticLoggerBinderPathSet(),可以發現在當前ClassPath下查詢了所有該路徑的資源“org/slf4j/impl/StaticLoggerBinder.class”,這裡可能沒有載入到任何文件,也可能綁定多個,對沒有綁定和綁定多個的場景進行了友好提示。這裡通過路徑載入資源的目的主要用來對載入的各種異常場景提示。

再往下代碼StaticLoggerBinder.getSingleton()才是實際的綁定,並且獲取StaticLoggerBinder的實例。這裡如果反編譯,你會發現根本沒有這個類StaticLoggerBinder。

如果沒有載入到文件,正如上邊demo執行的結果一樣,命中NoSuchMethodError異常,並列印沒有綁定場景的提示信息。

方法findPossibleStaticLoggerBinderPathSet()的源碼如下,可以發現類載入器通過路徑獲取URL資源。

            ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
            Enumeration<URL> paths;
            if (loggerFactoryClassLoader == null) {
                paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
            } else {
                paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
            }

2.2 pom引用依賴logback-classic

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

2.2.1 執行demo

可以看到正常的列印日誌信息,並且沒有任何異常

2.2.2 跟蹤源碼

這個時候如果再點擊進入方法StaticLoggerBinder.getSingleton(),發現類StaticLoggerBinder是由包logback-classic提供的,並且實現了SLF4J中的介面LoggerFactoryBinder。StaticLoggerBinder的創建用到了單例模式,該類主要目的返回一個創建Logger的工廠。這裡實際返回了ch.qos.logback.classic.LoggerContext的實例,再由該實例創建ch.qos.logback.classic.Logger。

UML類圖如下:

2.3 pom再引入log4j-slf4j-impl

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

2.3.1 執行demo

列印日誌如下,提示綁定了兩個StaticLoggerBinder.class,但最終實際綁定的是ch.qos.logback.classic.util.ContextSelectorStaticBinder。這裡邊也驗證了一旦一個類被載入之後,全局限定名相同的類就無法被載入了。這裡Jar包被載入的順序直接決定了類載入的順序。

SLF4J: Found binding in [jar:file:/D:/.m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/D:/.m2/repository/org/apache/logging/log4j/log4j-slf4j-impl/2.9.1/log4j-slf4j-impl-2.9.1.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [ch.qos.logback.classic.util.ContextSelectorStaticBinder]
18:19:43.521 [main] INFO com.cj.HelloSlf4j - Hello World info

2.4 log4j-slf4j-impl和logback-classic的引入位置變換

如果Pom文件先引入log4j-slf4j-impl,再引入logback-classic

2.4.1 執行demo

根據日誌列印結果,可以看到實際綁定的是org.apache.logging.slf4j.Log4jLoggerFactory;但是沒有正常列印出日誌,需要進行log4j2的日誌配置。說明實際綁定的是og4j-slf4j-impl包中的org/slf4j/impl/StaticLoggerBinder.class文件;這裡也驗證瞭如果有引入了多個橋接包,實際綁定的是先載入到的文件;

SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/D:/.m2/repository/org/apache/logging/log4j/log4j-slf4j-impl/2.9.1/log4j-slf4j-impl-2.9.1.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/D:/.m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.apache.logging.slf4j.Log4jLoggerFactory]
ERROR StatusLogger No log4j2 configuration file found. Using default configuration: logging only errors to the console. Set system property 'log4j2.debug' to show Log4j2 internal initialization logging.

2.5 類載入方式的變化

2.5.1 slf4j-api-1.7.30版本的打包技巧

反編譯看slf4j-api-1.7.30-sources.jar,發現壓根沒有這個類org.slf4j.impl.StaticLoggerBinder,他怎麼會編譯成功呢?猜想是不是打包的時候把這個類排除掉了呢?通過git下載源碼發現slf4j源碼其實是有這個文件的,org/slf4j/impl/StaticLoggerBinder.class;這裡使用了一個小技巧,打包的時候把實現類排除掉了,雖然不太優雅,但是思路很巧妙。

2.5.2 slf4j-api-2.0.0版本引入SPI(Service Provider Interface)

該版本通過使用SPI方式進行實現類的載入,感覺比之前的實現方式優雅了很多。橋接包只需要在這個位置:META-INF/services/,定義一個文件org.slf4j.spi.SLF4JServiceProvider(命名為SLFJ4提供的介面名),並且文件中指定實現類。只要引入這個橋接包,就可以適配到對應實現的日誌框架。

以下是SPI方式載入的源碼

private static List<SLF4JServiceProvider> findServiceProviders() {
        ServiceLoader<SLF4JServiceProvider> serviceLoader = ServiceLoader.load(SLF4JServiceProvider.class);
        List<SLF4JServiceProvider> providerList = new ArrayList();
        Iterator var2 = serviceLoader.iterator();

        while(var2.hasNext()) {
            SLF4JServiceProvider provider = (SLF4JServiceProvider)var2.next();
            providerList.add(provider);
        }

        return providerList;
     }

2.5.3 類載入方式對比

2.6 SLF4J官方已經實現綁定的日誌框架

slf4j已經提供了常用日誌框架的橋接包,以及詳細的文檔描述,使用起來非常簡單。
下圖是SLF4J官網中提供的,表示了各種日誌實現框架和SLF4J的關係:

2.7 總結

  • SLF4J API旨在一次綁定一個且僅一個底層日誌框架。而且引入SLF4J後,不管是否可以載入到StaticLoggerBinder,或者載入到多個StaticLoggerBinder,都進行友好提示,用戶體驗上考慮都很周到。如果類路徑上存在多個綁定,SLF4J 將發出警告,列出這些綁定的位置。當類路徑上有多個綁定可用時,應該選擇一個希望使用的綁定,然後刪除其他綁定。
  • 單純看SLF4J源碼,其實整體設計實現上都很簡單明確,定位非常清楚,就是做好門面。
  • 鑒於 SLF4J 介面及其部署模型的簡單性,新日誌框架的開發人員應該會發現編寫 SLF4J 綁定非常容易。
  • 對於目前比較主流的日誌框架都通過實現適配進行相容支持。只要用戶選擇了SLF4J,就可以確保以後變更日誌框架的自由。

3 SLF4J設計模式的使用

在slf4j中用到了一些經典的設計模式,比如門面模式、單例模式、靜態工廠模式等,我們來分析以下幾種設計模式。

3.1 門面模式(Facade Pattern)

1)解釋

門面模式,也叫外觀模式,要求一個子系統的外部與其內部的通信必須通過一個統一的對象進行。門面模式提供一個高層次的介面,使得子系統更易於使用。使用了門面模式,使客戶端調用變得更加簡單。

Slf4j制定了log日誌的使用標準,提供了高層次的介面, 我們編碼過程只需要依賴介面Logger和工廠類 LoggerFactory就可以實現日誌的列印,完全不用關心日誌內部的實現細節是logback實現的方式,還是log4j的實現方式。

2)圖解

        Logger logger = LoggerFactory.getLogger(HelloSlf4j.class);
        logger.info("Hello World info");

3)優點

解耦,減少系統的相互依賴。所有的依賴都是對門面對象的依賴,與子系統無關,業務層的開發不需要關心底層日誌框架的實現及細節,在編碼的時候也不需要考慮日後更換框架所帶來的成本。
介面和實現分離,屏蔽了底層的實現細節,面向介面編程。

3.2 單例模式(Singleton Pattern)

1)解釋

單例模式,確保一個類僅有一個實例,並提供一個訪問它的全局訪問點。
在SLF4J的適配包中都需要實現類StaticLoggerBinder,而類StaticLoggerBinder的實現就用了單例模式,而且是最簡單的實現方法,在靜態初始化器中直接new StaticLoggerBinder(),提供全局訪問方法獲取該實例。

2)UML圖

3)優點

在單例模式中,活動的單例只有一個實例,對單例類的所有實例化得到的都是相同的一個實例。這樣就防止其它對象對自己的實例化,確保所有的對象都訪問一個實例
單例模式具有一定的伸縮性,類自己來控制實例化進程,類就在改變實例化進程上有相應的伸縮性。

提供了對唯一實例的受控訪問。

在記憶體里只有一個實例,減少了記憶體的開銷,提高系統的性能。

4 啟示

  • 儘管SLF4J整體代碼短小但很精煉,可見門面模式運用好的威力。門面模式也為我們提供了對於多版本的實現如何統一定義介面以及相容提供了參考。
  • SLF4J定義和實現方案對用戶都很友好,同時又提供了各種橋接包,進行完善的文檔指導使用。總之各項用戶體驗都很棒,這也許也是SLF4J目前最受歡迎的原因之一吧。
  • 我們要多思考面向介面編程的思想,降低代碼耦合度,提高代碼擴展性。
  • 使用SPI的方式,優雅的載入擴展實現。
  • 好產品是設計出來的,更是優化迭代出來的。

5 參考資料

作者:京東物流 曹俊

來源:京東雲開發者社區


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 基於java的資源博客論壇系統設計與實現,可適用於java個人博客系統,個人資源博客管理系統,java博客系統,java論壇系統,類似於交友微博,新浪微博,發表動態,筆記博客,個人筆記系統。 ...
  • 引言 在日常使用的有些APP中,想什麼微信,百度地圖,可以可以搜尋附近的人,距離自己多遠,以及在地圖上我們可以搜索附近的某個地點,距離自己的位置。針對這種類似的功能,我們可以通過redis就能實現。 redis在3.2版本之後也提供了地理位置的能力,使用redis可以輕鬆實現查找附近的人 一:附近的 ...
  • # 概念 在Java中,`CountDownLatch`是一個線程同步的輔助類,用於等待其他線程完成操作。如果`CountDownLatch`實例被丟失或無法訪問,可能會導致無法正常使用該對象。這可能會導致等待線程永遠處於等待狀態,無法繼續執行。 如果意外丟失了`CountDownLatch`對象, ...
  • 毋庸諱言,密碼是極其偉大的發明,但拜病毒和黑客所賜,一旦密碼泄露,我們就得絞盡腦汁再想另外一個密碼,但記憶力並不是一個靠譜的東西,一旦遺忘密碼,也會造成嚴重的後果,2023年業界巨頭Google已經率先支持了Passkeys登錄方式,只須在設備上利用PIN碼解鎖、指紋或面部辨識等生物識別方式,即可驗 ...
  • `numpy`提供了簡單靈活的介面,用於優化數據數組的計算。 通用計算最大的優勢在於通過向量化操作,將迴圈推送至`numpy`之下的編譯層,從而取得更快的執行效率。 `numpy`的通用計算讓我們計算數組時就像計算單獨一個變數一樣, 不用寫迴圈去遍曆數組中的各個元素。 比如,對於一般的`python ...
  • 在SpringBoot中,EnableAutoConfiguration註解用於開啟自動裝配功能。 本文將詳細分析該註解的工作流程。 # EnableAutoConfiguration註解 啟用SpringBoot自動裝配功能,嘗試猜測和配置可能需要的組件Bean。 自動裝配類通常是根據類路徑和定義 ...
  • SpringAOP作為Spring最核心的能力之一,其重要性不言而喻。然後需要知道的是AOP並不只是Spring特有的功能,而是一種思想,一種通用的功能。而SpringAOP只是在AOP的基礎上將能力集成到SpringIOC中,使其作為bean的一種,從而我們能夠很方便的進行使用。 ...
  • 在初始類中,我們介紹瞭如何訪問類屬性,除了訪問類屬性外還有其他操作類屬性的情況,我們將在這裡做詳細介紹: # 1.給類屬性指定預設值 類中的每個屬性都必須有初始值,哪怕這個值是0或者空字元串。在有些情況下,如設置預設值時,在方法\_\_init\_\_方法內指定這種初始值是可行的,如果對某個屬性這樣 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...