slf4j中的橋接器是如何運作的?

2021-02-14 程序新視界

閱讀分析slf4j的日誌源碼,發現其中涵蓋了許多知識點和優秀的設計,關鍵它們都是活生生的實踐案例。寫成系列文章與大家分享,歡迎持續關注。第4篇。

前言

在日誌框架slf4j中有一組項目,除了核心的slf4j-api之外,還有slf4j-log4j12、slf4j-jdk14等項目。這一類項目統稱橋接器項目,針對不同的日誌框架有不同的橋接器項目。

在使用logback日誌框架時,並沒有針對的橋接器,這是因為logback與slf4j是一個作者所寫,在logback中直接實現了slf4j的SPI機制。

但如果使用其他日誌框架,那麼就必須要用到橋機器相關依賴。比如,當我們基於log4j使用slf4j時,除了需要引入log4j的jar包依賴,還需要引入slf4j的下面兩個依賴:

<dependency>  <groupId>org.slf4j</groupId>  <artifactId>slf4j-api</artifactId></dependency><dependency>  <groupId>org.slf4j</groupId>  <artifactId>slf4j-log4j12</artifactId></dependency>

slf4j-api為核心依賴,必須引入,而slf4j-log4j12就是橋接器用來在slf4j和log4j之間進行過渡和封裝。下面,我們就聊聊橋接器項目的核心實現。

slf4j-log4j12橋接器的價值

要了解橋接器的運作,首先需要回顧一下slf4j的SPI機制。在我們通過LoggerFactory.getLogger(Foo.class);時,slf4j會通過SPI機制尋找並初始化SLF4JServiceProvider的實現類。

然後,通過SLF4JServiceProvider的實現類來獲取日誌相關的具體工廠類對象,進而進行日誌功能的處理。先來看一下SLF4JServiceProvider的接口定義:

public interface SLF4JServiceProvider {
ILoggerFactory getLoggerFactory();
IMarkerFactory getMarkerFactory();
MDCAdapter getMDCAdapter();
String getRequesteApiVersion();
void initialize();}

SLF4JServiceProvider接口是在slf4j-api中定義的,具體的實現類由其他日誌框架來完成。但是像log4j(logback「敵對陣營」)是不會在框架內實現該接口的。那麼,怎麼辦?

針對此問題,slf4j提供了slf4j-log4j12這類橋接器的過渡項目。在其中實現SLF4JServiceProvider接口,並對Log4j日誌框架接口進行封裝,將Logger(slf4j)接收到的命令全部委託給Logger(log4j)去完成,在使用者無感知的情況下完成偷天換日。

slf4j-log4j12的核心實現類

理解了橋接器的存在價值及原理,下面就來看看slf4j-log4j12是如何實現這一功能的。

首先來看看核心實現類之一Log4j12ServiceProvider。它實現了SLF4JServiceProvider接口,主要功能就是完成接口中定義的相關工廠接口的實現。原始碼如下:

public class Log4j12ServiceProvider implements SLF4JServiceProvider {
public static String REQUESTED_API_VERSION = "1.8.99";
private ILoggerFactory loggerFactory; private IMarkerFactory markerFactory; private MDCAdapter mdcAdapter; public Log4j12ServiceProvider() { try { @SuppressWarnings("unused") Level level = Level.TRACE; } catch (NoSuchFieldError nsfe) { Util.report("This version of SLF4J requires log4j version 1.2.12 or later. See also http://www.slf4j.org/codes.html#log4j_version"); } }
@Override public void initialize() { loggerFactory = new Log4jLoggerFactory(); markerFactory = new BasicMarkerFactory(); mdcAdapter = new Log4jMDCAdapter(); } @Override public ILoggerFactory getLoggerFactory() { return loggerFactory; }
@Override public IMarkerFactory getMarkerFactory() { return markerFactory; }
@Override public MDCAdapter getMDCAdapter() { return mdcAdapter; }
@Override public String getRequesteApiVersion() { return REQUESTED_API_VERSION; }}

該類的實現看起來很簡單,構造方法中通過嘗試使用log4j的Level.TRACE調用來驗證log4j的版本是否符合要求。log4j1.2.12之前並沒有Level.TRACE,所以會拋出異常,並列印日誌信息。不得不讚嘆作者在此處檢查版本的巧妙用法。

而這裡對接口中返回的實現類主要通過initialize()方法來實現的。這裡我們重點看Log4jLoggerFactory類的實現。

public class Log4jLoggerFactory implements ILoggerFactory {
private static final String LOG4J_DELEGATION_LOOP_URL = "http://www.slf4j.org/codes.html#log4jDelegationLoop";
static { try { Class.forName("org.apache.log4j.Log4jLoggerFactory"); String part1 = "Detected both log4j-over-slf4j.jar AND bound slf4j-log4j12.jar on the class path, preempting StackOverflowError. "; String part2 = "See also " + LOG4J_DELEGATION_LOOP_URL + " for more details.";
Util.report(part1); Util.report(part2); throw new IllegalStateException(part1 + part2); } catch (ClassNotFoundException e) { } }
ConcurrentMap<String, Logger> loggerMap;
public Log4jLoggerFactory() { loggerMap = new ConcurrentHashMap<>(); org.apache.log4j.LogManager.getRootLogger(); }
@Override public Logger getLogger(String name) { Logger slf4jLogger = loggerMap.get(name); if(slf4jLogger != null) { return slf4jLogger; } else { org.apache.log4j.Logger log4jLogger; if(name.equalsIgnoreCase(Logger.ROOT_LOGGER_NAME)) { log4jLogger = LogManager.getRootLogger(); } else { log4jLogger = LogManager.getLogger(name); }
Logger newInstance = new Log4jLoggerAdapter(log4jLogger); Logger oldInstance = loggerMap.putIfAbsent(name, newInstance); return oldInstance == null ? newInstance : oldInstance; } }}

在Log4j12ServiceProvider中進行了Log4jLoggerFactory的實例化操作,也就直接new出來一個對象。我們知道,在new對象執行會先執行static代碼塊,本類的靜態代碼塊的核心工作就是檢查依賴文件中是否同時存在反向橋接器的依賴。

其中,org.apache.log4j.Log4jLoggerFactory是反向橋接器log4j-over-slf4j項目中的類,如果加裝到了,說明存在,則拋出異常,列印日誌信息。此處再次讚嘆作者運用的技巧的巧妙。

在Log4jLoggerFactory的構造方法中,做了兩件事:第一,初始化一個ConcurrentMap變量,用於存儲實例化的Logger;第二,強制初始化log4j的組件,其中強制初始化log4j的組件是通過getRootLogger方法,來初始化一些靜態的變量。

構造方法時初始化了ConcurrentMap變量,在Log4jLoggerFactory實現的getLogger方法中,先從Map中獲取一下是否存在對應的Logger,如果存在直接返回,如果不存在則進行構造。而構造的Log4jLoggerAdapter類很顯然使用了適配器模式,它內部持有了log4j的Logger對象,自身又實現了slf4j的Logger接口。

下面看一下Log4jLoggerAdapter的部分代碼實現:

public final class Log4jLoggerAdapter extends LegacyAbstractLogger implements LocationAwareLogger, Serializable {
final transient org.apache.log4j.Logger logger;
Log4jLoggerAdapter(org.apache.log4j.Logger logger) { this.logger = logger; this.name = logger.getName(); traceCapable = isTraceCapable(); }
@Override public boolean isDebugEnabled() { return logger.isDebugEnabled();    }
@Override public void log(Marker marker, String callerFQCN, int level, String msg, Object[] arguments, Throwable t) { Level log4jLevel = toLog4jLevel(level); NormalizedParameters np = NormalizedParameters.normalize(msg, arguments, t); String formattedMessage = MessageFormatter.basicArrayFormat(np.getMessage(), np.getArguments()); logger.log(callerFQCN, log4jLevel, formattedMessage, np.getThrowable()); }
public void log(LoggingEvent event) { Level log4jLevel = toLog4jLevel(event.getLevel().toInt()); if (!logger.isEnabledFor(log4jLevel)) return;
org.apache.log4j.spi.LoggingEvent log4jevent = toLog4jEvent(event, log4jLevel);        logger.callAppenders(log4jevent);    } }

源碼中,通過構造方法傳入log4j的Logger對象,而Log4jLoggerAdapter對外提供的方法,都是通過log4j的Logger進行具體實現。

總之,slf4j的Logger接口的方法通過Log4jLoggerAdapter進行包裝和轉換,交由log4j的Logger去執行,這就達到了連接slf4j-api和log4j的目的。而此時,slf4j-api不並關係日誌是如何實現記錄,對此也無感知。

小結

本文通過源碼跟蹤,逐步分析了slf4j項目中橋接器項目的運作機制,其中還涉及到了SPI機制、版本及依賴檢查小技巧、橋接器運作本質(適配器模式)等。其實,在slf4j項目中還有文中提到的反向橋接器,其實基本機制也是如此,感興趣的朋友可以閱讀一下log4j-over-slf4j中的源碼。


如果覺得文章有點用的話,請毫不留情地素質四連吧,分享、點讚、在看、留言,隨你便

相關焦點

  • 一文搞定slf4j、Java日誌那些事兒,我和面試官談笑風生
    翻譯成中文就可以這麼理解:slf4j為多種多樣的日誌框架(比如logging, logback and log4j)充當門面或抽象化的接口,本身並沒有具體實現如何進行日誌地列印以及日誌列印的格式這些細節。需要具體的日誌框架來實現並執行。
  • 飛利浦能否憑藉Hue橋接器走上統領智慧照明的寶座?
    其最大的特點就在於其橋接器,能夠通過 Zigbee技術連結最多50個燈泡,並且在連上網路後能夠通過手機App以及其他方式對這些燈光進行控制。 推出沒多久,市場上就出現了一批利用飛利浦Hue開發者套件開發出來的燈泡和應用生態產品。但開發者來源和實力的不均勻很快就導致了問題的出現,一些用戶反應第三方的燈泡使用中經常出現各種Bug,這也是飛利浦一開始選擇阻擋第三方智慧燈泡的原因。
  • Java 工程師快速入門深度學習,從 Deeplearning4j 開始
    隨著機器學習、深度學習為主要代表的人工智慧技術的逐漸成熟,越來越多的 AI 產品得到了真正的落地。
  • Spark與深度學習框架——H2O、deeplearning4j、SparkNet
    deeplearning4j項目準備了樣例存儲庫。要在Spark上使用deeplearning4j,dl4j-Spark-ml-examples是可參考的最佳示例(https:// github.com/deeplearning4j/dl4j-Spark-ml-examples)。下面列出如何下載及編譯這個代碼庫。
  • ...Java工程師轉型AI的秘密法寶——深度學習框架Deeplearning4j...
    目前研究人員使用的深度學習框架有 TensorFlow、Torch 、Caffe、Theano、Deeplearning4j等,而Deeplearning4j是為數不多以Java/JVM為基礎,能與Apache Spark無縫結合,支持CPU/GPU集群分布式計算的開源框架。本文就來為大家詳細介紹一下深度學習框架Deeplearning4j的重要組件,不同環境下的操作用法及實例分享。
  • Redis是如何實現點讚、取消點讚的?
    我是如何用redis做實時訂閱推送的?正文:本文基於 SpringCloud, 用戶發起點讚、取消點讚後先存入 Redis 中,再每隔兩小時從 Redis 讀取點讚數據寫入資料庫中做持久化存儲。由於需要記錄點讚人和被點讚人,還有點讚狀態(點讚、取消點讚),還要固定時間間隔取出 Redis 中所有點讚數據,分析了下 Redis 數據格式中 Hash 最合適。因為 Hash 裡的數據都是存在一個鍵裡,可以通過這個鍵很方便的把所有的點讚數據都取出。
  • Resilience4j v0.14.0 發布,面向 Java 8 和函數式編程的容錯庫
    Resilience4j v0.14.0 發布了,Resilience4j 是一個輕量級的容錯組件,其靈感來自於 Hystrix