基於slf4j和common-logging的日誌框架

前言

用了這麼長時間的日誌框架,有時候用得很順利,有的時候又很迷惑,比如在項目中引入一個新的jar包,會出現日誌框架衝突的時候,要不就是日誌莫名的打印不出來,要不就是項目啓動異常。今天花點時間來整理,梳理一下這些日誌框架之間的關係。

日誌框架概覽

目前使用比較廣泛的日誌接口是common-logging和slf4j,common-logging是apache提供的一個通用的日誌接口,slf4j是log4j的作者創作的日誌接口。

小故事:apache說服log4j以及其他的日誌來按照commons-logging的標準編寫,但是由於commons-logging的類加載有點問題,log4j的作者就創作了slf4j。至於到底使用哪個,由用戶來決定。

所爲日誌接口,就是common-logging或者slf4j只定義了日誌的接口,而日誌輸出的具體實現,交給實現了日誌接口的框架,比如log4j、logback等。

slf4j日誌接口

slf4j全稱爲Simple Logging Facade for JAVA,java簡單日誌門面,這是從其他的博主文章中摘錄出來的,我借用過來記錄一下:
在這裏插入圖片描述
從這張圖中就能看的很清楚,不同的日誌框架(包括:log4j、logback等)的基礎是slf4j,也就是說slf4j是一個日誌門面,slf4j只是提供了一套標準的日誌接口,沒有具體的實現,它只提供了一個slf4j-api-*.jar包,負責日誌實現的工作交給了各個日誌框架。各個日誌框架在實現的時候,又有點區別,一個明顯的區別就是是否需要引入一個連接層jar包,比如log4j日誌框架,需要引入一個連接層slf4j-log412.jar,而像logback日誌框架,就不需要引入一個連接層jar,引入和不引入連接層實質上的區別是什麼呢?我們後面在分析。
參照這個圖,我們就清楚的知道,如果在項目中使用log4j作爲日誌框架,就需要引入3個jar包

<dependency>
	<groupId>log4j</groupId>
	<artifactId>log4j</artifactId>
	<version>1.2.17</version>
</dependency>
<dependency>
	<groupId>org.slf4j</groupId>
	<artifactId>slf4j-api</artifactId>
	<version>1.7.25</version>
</dependency>
<dependency>
	<groupId>org.slf4j</groupId>
	<artifactId>slf4j-log4j12</artifactId>
	<version>1.7.25</version>
</dependency>

如果是log4j2日誌框架,就回引入不同的jar,畢竟log4j2和log4j架構還是有區別的。
如果是使用logback作爲日誌框架,需要引入下面的jar包,沒有什麼中間的連接層jar包。

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.7</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-core</artifactId>
    <version>1.1.7</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.1.7</version>
</dependency>

使用slf4j的常見代碼

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class TestLog {
	private Logger logger = LoggerFactory.getLogger(this.getClass());
	public static void main(String[] s){
		logger.info("test");
	}
}

slf4j查找日誌實現框架的過程,以logback日誌框架爲例進行簡單的分析:
slf4j正式加載日誌框架的動作是在類實例化完成的最後階段,也就是給下面這條語句賦值的時候:

private Logger logger = LoggerFactory.getLogger(this.getClass());

LoggerFactory是slf4j-api 包中的類,在這個LoggerFactory裏有幾處關鍵的地方:

package org.slf4j;
public final class LoggerFactory {
    private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";
    
	public static Logger getLogger(String name) {
		//③創建Logger工廠
        ILoggerFactory iLoggerFactory = getILoggerFactory();
        //⑤創建Logger
        return iLoggerFactory.getLogger(name);
    }
    private static Set findPossibleStaticLoggerBinderPathSet() {
        LinkedHashSet staticLoggerBinderPathSet = new LinkedHashSet();
        try {
            ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
            Enumeration paths;
            if (loggerFactoryClassLoader == null) {
                paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
            } else {
            	//①加載指定的binder
                paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
            }
            //省略代碼
        } catch (IOException var4) {
            Util.report("Error getting resources from path", var4);
        }
        return staticLoggerBinderPathSet;
    }

    public static ILoggerFactory getILoggerFactory() {
        if (INITIALIZATION_STATE == 0) {
            INITIALIZATION_STATE = 1;
            performInitialization();
        }
        switch(INITIALIZATION_STATE) {
        case 1:
            return TEMP_FACTORY;
        case 2:
            throw new IllegalStateException("org.slf4j.LoggerFactory could not be successfully initialized. See also http://www.slf4j.org/codes.html#unsuccessfulInit");
        case 3:
        	//②加載日誌工廠類
            return StaticLoggerBinder.getSingleton().getLoggerFactory();
        case 4:
            return NOP_FALLBACK_FACTORY;
        default:
            throw new IllegalStateException("Unreachable code");
        }
    }
}

上面3個方法的調用順序是getLogger()->getILoggerFactory()->findPossibleStaticLoggerBinderPathSet()
先看看①處代碼,加載了一個指定的Binder類StaticLoggerBinder.class,這個類有兩個特點,

  • 第一個特點是,這個類顯然是寫死在代碼裏面的,
  • 第二個特點是,這個類不是slf4j-api包裏面的類,

那這個類在哪兒呢?這就得在logback的jar包中去發現了,看看下面這張截圖
在這裏插入圖片描述
這就很明顯了,這個StaticLoggerBinder類成了slf4j和logback日誌框架的一個橋樑,這個Binder類做了什麼事情,不是現在我們關注的重點,
接着看②處代碼,StaticLoggerBinder.getSingleton()獲取了StaticLoggerBinder的一個實現單例,getLoggerFactory做了什麼事情,看看代碼

package org.slf4j.impl;

public class StaticLoggerBinder implements LoggerFactoryBinder {
    private LoggerContext defaultLoggerContext = new LoggerContext();
	//④返回Logger工廠類
    public ILoggerFactory getLoggerFactory() {
        if (!this.initialized) {
            return this.defaultLoggerContext;
        } else if (this.contextSelectorBinder.getContextSelector() == null) {
            throw new IllegalStateException("contextSelector cannot be null. See also http://logback.qos.ch/codes.html#null_CS");
        } else {
            return this.contextSelectorBinder.getContextSelector().getLoggerContext();
        }
    }
    void init() {
        try {
            try {
            	//⑥加載日誌的配置文件
                (new ContextInitializer(this.defaultLoggerContext)).autoConfig();
            } catch (JoranException var2) {
                Util.report("Failed to auto configure default logger context", var2);
            }
            //省略代碼
        } catch (Throwable var3) {
            Util.report("Failed to instantiate [" + LoggerContext.class.getName() + "]", var3);
        }

    }
}

④代碼說明:StaticLoggerBinder類中getLoggerFactory方法,就是返回了一個LoggerContext,這個是什麼東西,接着看代碼

package ch.qos.logback.classic;
import org.slf4j.ILoggerFactory;
public class LoggerContext extends ContextBase implements ILoggerFactory, LifeCycle {
}

看到這個類的定義就知道了,LoggerContext是處於logback-classic.jar包中的一個ILoggerFactory的實現類,ILoggerFactory是slf4j-api中定義的接口
在這裏插入圖片描述
回頭看看上面③處的代碼,LoggerFactory就是這樣被創建出來的,⑤處的代碼就是調用ch.qos.logback.classic.LoggerContext.getLogger()方法了,這就是logback要完成的事情了。

順便說一下日誌配置文件logback.xml在哪兒被加載的?代碼⑥處,也是在StaticLoggerBinder類中完成的,ContextInitializer().autoConfig()加載配置文件logback.xml

從上面的分析看得出StaticLoggerBinder至少做了兩件重要的事情:

  1. 創建ILoggerFactory的實現類LoggerContext
  2. 加載日誌框架的配置文件logback.xml

SLF4J 會在編譯時會綁定org.slf4j.impl.StaticLoggerBinder,該類裏面實現對具體日誌方案的綁定接入。任何一種基於slf4j 的實現都要有一個這個類。
log4j日誌框架也是一樣的道理,它也實現了這個StaticLoggerBinder類,從上面的我們講到log4j需要引入的jar包就知道,引入了slf4j-log4j12這個包,這個包是幹什麼的呢?我們不在此處分析,看圖:
在這裏插入圖片描述
雖然不知道slf4j-log4j12這個包是幹什麼的,但是現在至少分析出來可以得出結論,沒有這個包,StaticLoggerBinder類就缺失,log4j就不能和slf4j集成。

common-logging日誌接口

使用common-logging的常見代碼

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
 
public class Test {
	private Log logger = LogFactory.getLog(this.getClass());
	public static void main(String[] s){
		logger.info("test");
	}
}

動態查找原理:
Log 是一個接口聲明。LogFactory 的內部會去裝載具體的日誌系統,並獲得實現該Log 接口的實現類。LogFactory 內部裝載日誌系統的流程如下:

  • 首先,尋找org.apache.commons.logging.LogFactory 屬性配置。
  • 否則,利用JDK1.3 開始提供的service 發現機制,會掃描classpah 下的META-INF/services/org.apache.commons.logging.LogFactory文件,若找到則裝載裏面的配置,使用裏面的配置。
  • 否則,從Classpath 裏尋找commons-logging.properties ,找到則根據裏面的配置加載。
  • 否則,使用默認的配置:如果能找到Log4j 則默認使用log4j 實現,如果沒有則使用JDK14Logger 實現,再沒有則使用commons-logging 內部提供的SimpleLog 實現。

從上述加載流程來看,只要引入了log4j 並在classpath 配置了log4j.xml ,則commons-logging 就會使log4j 使用正常,而代碼裏不需要依賴任何log4j 的代碼。

slf4j與common-logging比較

  1. common-logging通過動態查找的機制,在程序運行時自動找出真正使用的日誌庫。由於它使用了ClassLoader尋找和載入底層的日誌庫, 導致了象OSGI這樣的框架無法正常工作,因爲OSGI的不同的插件使用自己的ClassLoader。 OSGI的這種機制保證了插件互相獨立,然而卻使Apache Common-Logging無法工作。
  2. slf4j在編譯時靜態綁定真正的Log庫,因此可以再OSGI中使用。另外,SLF4J 支持參數化的log字符串,避免了之前爲了減少字符串拼接的性能損耗而不得不寫的if(logger.isDebugEnable()),現在你可以直接寫:logger.debug(“current user is: {}”, user)。拼裝消息被推遲到了它能夠確定是不是要顯示這條消息的時候,但是獲取參數的代價並沒有倖免。

日誌框架切換

現實情況,假如我們現在的項目,正在使用的是slf4j日誌接口,日誌實現框架是logback,運行一段時間之後,引入了一個其他組件,這個組件使用的是common-logging作爲日誌接口,日誌實現框架是log4j,一個項目中不能使用兩套日誌框架,必須得保留一個,logback作爲項目目前使用的框架,優先考慮保留,自然就得把log4j排除掉,這個時候怎麼實現呢?如果強行把common-logging的jar包和log4j的jar包排除,程序啓動肯定是會報錯的。

解決的方案需要引入一種橋接模式,關鍵在於一個jar包jcl-over-slf4j.jar,就是commons-logging通過jcl-over-slf4j來選擇slf4j作爲底層的日誌輸出對象,而slf4j又選擇logback來作爲底層的日誌輸出對象。幾個關鍵步驟:

  1. 引入jcl-over-slf4j的jar包
  2. 去掉commons-logging的jar包,使用jcl-over-slf4j將commons-logging的底層日誌輸出切換到slf4j
  3. 去掉log4j的jar包
    jcl-over-slf4j.jar裏面到底做了哪些事情,首先看看包裏面有哪些類?
    在這裏插入圖片描述
  • 第一個需要注意的地方,路徑org.apache.commons.logging.LogFactory,org.apache.commons.logging.Log和和common-logging.jar包的路徑是一樣的。這就是爲什麼我們去掉了commons-logging的jar包之後,程序不會報錯的原因。程序依然是可以正常編譯的,對應程序來說,我們刪除了commons-logging.jar包是無感的。
  • 第二個需要注意的地方,Log的實現類換成了SLF4JLog,SLF4JLog implements Log,SLF4JLogFactory繼承了LogFactory

看看整體的類圖

在這裏插入圖片描述 在這裏插入圖片描述

再次回顧一下我們的日誌使用程序,使用common-logging的常見代碼

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
 
public class LogTest {
	private Log logger = LogFactory.getLog(this.getClass());
	public static void main(String[] s){
		logger.info("test");
	}
}

LogFactory.getLog()實質上是調用到了SLF4JLogFactory.getInstance(),getInstance方法返回的是Log的實現類SLF4JLog,所以引用變量logger實際指向的實例是SLF4JLog,當我們執行logger.info(“test”)的時候,被SLF4JLog轉發到了logback日誌框架去輸出日誌。
整個大致類圖:
在這裏插入圖片描述
其他的日誌框架的裝換,用到了相似的方法,不再一一分析。別的日誌接口,轉成slf4j的方法,這張圖總結得很好,借用過來記錄一下。
在這裏插入圖片描述

參考:
Java日誌框架SLF4J和log4j以及logback的聯繫和區別
java日誌組件介紹
SLF4j 和 common-logging

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章