從源碼來理解slf4j的綁定,以及logback對配置文件的加載

前言

  項目中的日誌系統使用的是slf4j + logback。slf4j做爲一個簡單日誌門面,爲各類loging APIs(像java.util.logging, logback, log4j)提供一個簡單統一的接口,有利於維護和各個類的日誌處理方式統一。Logback做爲一個具體的日誌組件,完成具體的日誌操做。html

本博客旨在帶領你們理清楚slf4j的綁定(logback如何綁定到slf4j的),logback是什麼時候加載配置文件的。至於具體的配置則須要你們本身去查閱資料了。java

  路漫漫其修遠兮,吾將上下而求索!git

  github:https://github.com/youzhibinggithub

  碼雲(gitee):https://gitee.com/youzhibingspring

slf4j + logback的使用

  使用很是簡單,引入依賴的jar便可,以下圖apache

  pom.xmlapi

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.yzb</groupId>
    <artifactId>mylog</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>mylog</name>
    <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.7</maven.compiler.source>
        <maven.compiler.target>1.7</maven.compiler.target>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-api</artifactId>
                <version>1.7.7</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
        </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>
        
    </dependencies>
</project>
View Code

  測試代碼app

public class LogTest
{
    private static Logger LOGGER = LoggerFactory.getLogger(LogTest.class);
    
    public static void main(String[] args)
    {
        LOGGER.info("......info");
        LOGGER.debug("......debug");
        LOGGER.warn("......warn");
        LOGGER.error("......error");
        LOGGER.trace("......trace");
    }

}
View Code 

  控制檯輸出結果框架

15:24:48.840 [main] INFO com.huawei.log.LogTest - ......info
15:24:48.842 [main] DEBUG com.huawei.log.LogTest - ......debug
15:24:48.842 [main] WARN com.huawei.log.LogTest - ......warn
15:24:48.842 [main] ERROR com.huawei.log.LogTest - ......error

  使用真的簡單,也正是這種簡單讓我產生了一些疑問jvm

    問題1:你們對spring使用的比較多的話,就知道將某個實現類注給其接口的時候,都是須要明確指出的,不管是經過配置文件的方式仍是註解的方式。以下

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context" xmlns:task="http://www.springframework.org/schema/task"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context-3.1.xsd
    http://www.springframework.org/schema/task
    http://www.springframework.org/schema/task/spring-task-3.1.xsd">
    
    <!--
        applicationContext.xml實際是不會存在
                        配置文件會報錯,由於缺乏spring的jar包,這裏只是模擬spring的依賴注入 
                        更詳細代碼請看附件
     -->
    
    <bean id="daoImpl" class="com.yzb.dao.impl.DaoImpl" />
    
    <bean id="studentService" class="com.yzb.service.StudentService">
       <!-- dao對應private IDao dao; 將實現daoImpl綁定到接口dao -->
       <property name="dao" ref="daoImpl"/>  
    </bean>

</beans>

    可slf4j + logback沒有其餘任何的配置,工程就能跑起來,可以打印各類類型的日誌,這是怎麼實現的呢?

    問題2:咱們加上logback的配置文件,僅僅在src/main/resources(至關於classpath)下加logback.xml,發現生成了日誌文件(若沒有設置日誌文件路徑,那麼日誌文件生成在當前工程下),而且控制檯輸出結果以下:

2017-05-13 15:57:27|INFO|......info
2017-05-13 15:57:27|WARN|......warn
2017-05-13 15:57:27|ERROR|......error

    僅僅在src/main/resources下配置logback.xml,就能達到這種效果,logback.xml是何時加載的呢?

源碼解析

  從LogTest.java開始

public class LogTest
{
    private static Logger LOGGER = LoggerFactory.getLogger(LogTest.class);
    
    public static void main(String[] args)
    {
        // 下面5個方法至關於接口調用實現
        LOGGER.info("......info");
        LOGGER.debug("......debug");
        LOGGER.warn("......warn");
        LOGGER.error("......error");
        LOGGER.trace("......trace");
    }

}

  代碼很是簡單,很明顯咱們只須要看private static Logger LOGGER = LoggerFactory.getLogger(LogTest.class)的實現。

  跟進getLogger方法 2步,來到

    /**
     * Return a logger named according to the name parameter using the statically
     * bound {@link ILoggerFactory} instance.
     *
     * @param name The name of the logger.
     * @return logger
     */
    public static Logger getLogger(String name) {
      // 獲取日誌工廠
      ILoggerFactory iLoggerFactory = getILoggerFactory();
      // 返回日誌實例
      return iLoggerFactory.getLogger(name);
    }

  咱們跟進getILoggerFactory方法

  /**
   * Return the {@link ILoggerFactory} instance in use.
   * <p/>
   * <p/>
   * ILoggerFactory instance is bound with this class at compile time. // 編譯時綁定工廠實例
   *
   * @return the ILoggerFactory instance in use
   */
  public static ILoggerFactory getILoggerFactory() {
    if (INITIALIZATION_STATE == UNINITIALIZED) {
      INITIALIZATION_STATE = ONGOING_INITIALIZATION;
      
      // 執行初始化 
      performInitialization();
    }
    switch (INITIALIZATION_STATE) {
      case SUCCESSFUL_INITIALIZATION:
        // 若初始化成功,則返回日誌工廠
        return StaticLoggerBinder.getSingleton().getLoggerFactory();
      case NOP_FALLBACK_INITIALIZATION:
        return NOP_FALLBACK_FACTORY;
      case FAILED_INITIALIZATION:
        throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
      case ONGOING_INITIALIZATION:
        // support re-entrant behavior.
        // See also http://bugzilla.slf4j.org/show_bug.cgi?id=106
        return TEMP_FACTORY;
    }
    throw new IllegalStateException("Unreachable code");
  } 

  很顯然,接着跟進performInitialization方法

  private final static void performInitialization() {
    bind();
    if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
      versionSanityCheck();
    }
  }

  跟進bind方法

private final static void bind() {
    try {
      // 從classpath獲取可能的日誌綁定者,就是找出全部slf4j的實現,並將它們的資源路徑存放到staticLoggerBinderPathSet
      Set<URL> staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
      // 如有多個(多餘1個)綁定者,就是從classpath中找到了多個slf4j的實現,那麼就打印警告。這個方法就不跟進了,感興趣的本身跟進
      reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
      // the next line does the binding 真正的綁定,將具體的實現綁定到slf4j
      StaticLoggerBinder.getSingleton();
      // 修改初始化狀態爲初始化成功
      INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
      // 報告真實的綁定信息
      reportActualBinding(staticLoggerBinderPathSet);
      fixSubstitutedLoggers();
    } catch (NoClassDefFoundError ncde) {    // 如有多個綁定者,則會拋此異常,Java虛擬機在編譯時能找到合適的類,而在運行時不能找到合適的類致使的錯誤,jvm不知道用哪一個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.indexOf("org.slf4j.impl.StaticLoggerBinder.getSingleton()") != -1) {
        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);
    }
  }

  跟進findPossibleStaticLoggerBinderPathSet方法

  // We need to use the name of the StaticLoggerBinder class, but we can't reference
  // the class itself.
  private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";

  // 從classpath找出全部slf4j的實現,並記錄下它們的資源路徑
  private 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 用LinkedHashSet可以保證插入的順序
    Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
    try {
      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);
      }
      while (paths.hasMoreElements()) {
        // path的值 jar:file:/D:/repository/ch/qos/logback/logback-classic/1.1.7/logback-classic-1.1.7.jar!/org/slf4j/impl/StaticLoggerBinder.class
        URL path = (URL) paths.nextElement();
        staticLoggerBinderPathSet.add(path);
      }
    } catch (IOException ioe) {
      Util.report("Error getting resources from path", ioe);
    }
    return staticLoggerBinderPathSet;
  }

  至此,問題1的答案就很明顯了,slf4j會在classpath中找全部org/slf4j/impl/StaticLoggerBinder.class的資源路徑,通常而言只有一個,在本博客中就在logback的jar中,如圖

  

  那麼logback與slf4j就關聯起來了,接下來看logback對配置文件的加載。咱們回到bind方法,跟進StaticLoggerBinder.getSingleton(),方法很簡單

public static StaticLoggerBinder getSingleton() {
  return SINGLETON;
}

  很顯然,執行此方法以前,對配置文件的加載已經執行完了,也就是說在編譯器已經完成對配置文件的加載了。那麼咱們須要換目標跟進了,StaticLoggerBinder中只有一段靜態塊

    static {
        SINGLETON.init();
    }

  那麼咱們跟進init方法

    /**
     * Package access for testing purposes.
     */
    void init() {
        try {
            try {
                // 上下文初始化器
                new ContextInitializer(defaultLoggerContext).autoConfig();
            } catch (JoranException je) {
                Util.report("Failed to auto configure default logger context", je);
            }
            // logback-292
            if (!StatusUtil.contextHasStatusListener(defaultLoggerContext)) {
                StatusPrinter.printInCaseOfErrorsOrWarnings(defaultLoggerContext);
            }
            contextSelectorBinder.init(defaultLoggerContext, KEY);
            initialized = true;
        } catch (Throwable t) {
            // we should never get here
            Util.report("Failed to instantiate [" + LoggerContext.class.getName() + "]", t);
        }
    }

  接着跟進上下文初始化器的autoConfig方法

    public void autoConfig() throws JoranException {
        StatusListenerConfigHelper.installIfAsked(loggerContext);
        // 尋找默認配置文件
        URL url = findURLOfDefaultConfigurationFile(true);
        if (url != null) {
            configureByResource(url);
        } else {
            Configurator c = EnvUtil.loadFromServiceLoader(Configurator.class);
            if (c != null) {
                try {
                    c.setContext(loggerContext);
                    c.configure(loggerContext);
                } catch (Exception e) {
                    throw new LogbackException(String.format("Failed to initialize Configurator: %s using ServiceLoader", c != null ? c.getClass()
                                    .getCanonicalName() : "null"), e);
                }
            } else {
                // 沒有找到配置文件,則使用默認的配置器,那麼日誌只會打印在控制檯
                BasicConfigurator basicConfigurator = new BasicConfigurator();
                basicConfigurator.setContext(loggerContext);
                basicConfigurator.configure(loggerContext);
            }
        }
    }

  跟進findURLOfDefaultConfigurationFile方法

    public URL findURLOfDefaultConfigurationFile(boolean updateStatus) {
        // 獲取當前實例的類加載器,目的是在classpath下尋找配置文件
        ClassLoader myClassLoader = Loader.getClassLoaderOfObject(this);
        
        // 先找logback.configurationFile文件
        URL url = findConfigFileURLFromSystemProperties(myClassLoader, updateStatus);
        if (url != null) {
            return url;
        }
        
        // logback.configurationFile文件沒找到,再找logback.groovy
        url = getResource(GROOVY_AUTOCONFIG_FILE, myClassLoader, updateStatus);
        if (url != null) {
            return url;
        }
        
        // logback.groovy沒找到,再找logback-test.xml
        url = getResource(TEST_AUTOCONFIG_FILE, myClassLoader, updateStatus);
        if (url != null) {
            return url;
        }
        
        // logback-test.xml沒找到,最後找logback.xml
        return getResource(AUTOCONFIG_FILE, myClassLoader, updateStatus);
    } 

  自此,問題2的答案也清楚了,編譯期間logback就完成了對配置文件的加載

總結

  編譯期間,完成slf4j的綁定已經logback配置文件的加載。slf4j會在classpath中尋找org/slf4j/impl/StaticLoggerBinder.class(會在具體的日誌框架如log4j、logback等中存在),找到並完成綁定;同時,logback也會在classpath中尋找配置文件,先找logback.configurationFile、沒有則找logback.groovy,若logback.groovy也沒有,則找logback-test.xml,若logback-test.xml仍是沒有,則找logback.xml,若連logback.xml也沒有,那麼說明沒有配置logback的配置文件,那麼logback則會啓用默認的配置(日誌信息只會打印在控制檯)。

  slf4j只能綁定某一個特定的日誌框架,若沒有綁定,則會有以下警告,說明沒有找到合適的日誌框架

SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.

  若找到多個日誌框架,slf4j會發出警告,並在運行時拋出NoClassDefFoundError異常

  最後來一張圖

相關文章
相關標籤/搜索