源碼解讀SLF4J綁定日志實現的原理


一、導讀

我們使用log4j框架時,經常會用slf4j-api。在運行時,經常會遇到如下的錯誤提示:
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/Users/abc/maven-repository/org/slf4j/slf4j-simple/1.7.26/slf4j-simple-1.7.26.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/Users/abc/maven-repository/org/apache/logging/log4j/log4j-slf4j-impl/2.7/log4j-slf4j-impl-2.7.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.slf4j.impl.SimpleLoggerFactory]
也即 classpath下有多個日志實現,slf4j-api在綁定時出現了沖突。那么這時slf4j-api到底會綁定哪一個具體的實現呢?
 
官方文檔有如下說明:

 

也就是說,當有多個日志實現時,SLF4J會在編譯期隨機選擇其中的一個實現。那么,它到底是如何隨機選擇的呢?下面我們通過源碼來分析。

請尊重作者勞動成果,轉載請標明原文鏈接: https://www.cnblogs.com/waterystone/p/11329645.html
 

二、源碼分析

2.1 LoggerFactory.getLogger(this.getClass())

我們通過LoggerFactory.getLogger(this.getClass())拿到Logger對象,其代碼如下:
public static Logger getLogger(Class<?> clazz) {
    Logger logger = getLogger(clazz.getName());
    if (DETECT_LOGGER_NAME_MISMATCH) {
        Class<?> autoComputedCallingClass = Util.getCallingClass();
        if (autoComputedCallingClass != null && nonMatchingClasses(clazz, autoComputedCallingClass)) {
            Util.report(String.format("Detected logger name mismatch. Given name: \"%s\"; computed name: \"%s\".", logger.getName(),
                            autoComputedCallingClass.getName()));
            Util.report("See " + LOGGER_NAME_MISMATCH_URL + " for an explanation");
        }
    }
    return logger;
}

  

2.2 LoggerFactory.bind()

getLogger() -> getILoggerFactory()-> performInitialization() -> bind() ,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(); //在classpath下查找有多少個日志實現
            reportMultipleBindingAmbiguity(staticLoggerBinderPathSet); //如果有多個日志實現,打印出來
        }
        // the next line does the binding。classpath下每個日志實現jar都會有org.slf4j.impl.StaticLoggerBinder類,這里會隨機加載其中的一個。
        StaticLoggerBinder.getSingleton();
        INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
        reportActualBinding(staticLoggerBinderPathSet);
        fixSubstituteLoggers();
        replayEvents();
        // release all resources in SUBST_FACTORY
        SUBST_FACTORY.clear();
    } catch (NoClassDefFoundError ncde) { //如果在classpath下沒有找到一個org.slf4j.impl.StaticLoggerBinder
        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);
    }
}

  

2.3 LoggerFactory.findPossibleStaticLoggerBinderPathSet()

接下來,我們再看看findPossibleStaticLoggerBinderPathSet()如何在classpath下查找有多少個日志實現的源碼:
static Set<URL> findPossibleStaticLoggerBinderPathSet() {
    // use Set instead of list in order to deal with bug #138
    // LinkedHashSet appropriate here because it preserves insertion order
    // during iteration
    Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
    try {
        ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
        Enumeration<URL> paths;
        if (loggerFactoryClassLoader == null) { //用ClassLoader去查找classpath下有多少個org.slf4j.impl.StaticLoggerBinder類
            paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
        } else {
            paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
        }
        while (paths.hasMoreElements()) {
            URL path = paths.nextElement();
            staticLoggerBinderPathSet.add(path);
        }
    } catch (IOException ioe) {
        Util.report("Error getting resources from path", ioe);
    }
    return staticLoggerBinderPathSet;
}

  

通過源碼我們可以看到, findPossibleStaticLoggerBinderPathSet()會用ClassLoader查找當前classpath下有多少個org.slf4j.impl.StaticLoggerBinder類,每個path的URL能定位到其具體jar包位置。 每個日志實現jar包都會有個org.slf4j.impl.StaticLoggerBinder類,反過來,那么classpath下有多少個StaticLoggerBinder類,就會有多少個相應的jar包,也即有多少個日志實現。所以findPossibleStaticLoggerBinderPathSet()通過掃描classpath下的org.slf4j.impl.StaticLoggerBinder類就能找到有多少個日志實現
 
接下來, 我們再看看是不是每個日志實現jar包都會有個org.slf4j.impl.StaticLoggerBinder類呢?答案是YES
 

三、具體日志實現

3.1 slf4j-simple

 

3.2 slf4j-nop

 

3.3 logback-classic

 

3.4 log4j-slf4j-impl

 

四、總結

  • 每個日志實現jar都會有org.slf4j.impl.StaticLoggerBinder類的實現;
  • SLF4J會通過ClassLoad掃描當前classpath下有多少個org.slf4j.impl.StaticLoggerBinder類,也就找到了有多少個日志實現(通過這樣,我們只需在項目中加入日志實現的jar包,編譯時即可自動加載,業務代碼無須顯式依賴,實現解耦);
  • 如果有多個org.slf4j.impl.StaticLoggerBinder類,SLF4J會在LoggerFactory.bind()里調用StaticLoggerBinder.getSingleton()隨機加載一個日志實現jar的StaticLoggerBinder。
  • 發現一個有趣的現象,在本地idea上運行時,是加載pom.xml里聲明的第一個日志實現;但是如果打包好后通過java -jar啟動時,其加載的日志實現確實是隨機的(是在編譯打包時隨機加載一個日志實現,所以一旦編譯打包好后其加載的那個日志實現就會固定不變)。所以,我們在具體使用時一定要通過排除依賴的方式來確定日志實現,不要由於日志實現的不確定性引入難以排查、不必要的坑


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM