commons-logging + log4j原始碼分析

青山丶發表於2019-08-07

分析之前先理清楚幾個概念

Log4J = Log For Java

SLF4J = Simple Logging Facade for Java

看到Facade首先想到的就是設計模式中的門面(Facade)模式,實際上SLF4J 就是一個裝"門面"的java日誌框架,它只提供一層抽象且通用的日誌API供呼叫方寫日誌使用,而真正實現寫日誌功能的則是Log4J、logback等框架和從jdk1.4之後開始提供的java.util.logging包,而具體要使用誰就要看SLF4J中設定的策略,整體來看的話也確實是使用了門面模式

關於這幾個日誌框架的誕生和關係推薦看下這篇部落格:https://blog.csdn.net/qq_32625839/article/details/80893550

 

Apache的commons-logging和SLF4J 一樣,也是一個抽象的日誌框架,使用得更廣泛,下面通過幾段原始碼分析下其內部的門面模式是怎樣實現的

一般寫日誌之前都要先用下面的方法獲取到log物件

Log log = LogFactory.getLog(clz.getName());

進入getLog方法
    public static Log getLog(String name) throws LogConfigurationException {
        return getFactory().getInstance(name);
    }

看來是先獲取到log工廠物件再獲取到log物件,進入getFactory方法

//獲取到上下文中的類載入器
ClassLoader contextClassLoader = getContextClassLoaderInternal();
//先嚐試從快取中獲取LogFactory
LogFactory factory = getCachedFactory(contextClassLoader); if (factory != null) {   return factory; }

  //如果存在commons-logging.properties配置檔案且其中的use_tccl配置項為false,則使用thisClassLoader作為後續的類載入器,thisClassLoader就是當前類的載入器
  Properties props = getConfigurationFile(contextClassLoader, FACTORY_PROPERTIES);
  ClassLoader baseClassLoader = contextClassLoader;
  if (props != null) {
    String useTCCLStr = props.getProperty(TCCL_KEY);
    if (useTCCLStr != null) {
      if (Boolean.valueOf(useTCCLStr).booleanValue() == false) {
        baseClassLoader = thisClassLoader;
      }
    }
  }

//獲取org.apache.commons.logging.LogFactory系統配置項,若存在則使用配置的類新建一個工廠物件
String factoryClass = getSystemProperty(FACTORY_PROPERTY, null);
if (factoryClass != null) {
factory = newFactory(factoryClass, baseClassLoader, contextClassLoader);
}

//利用java中的SPI機制根據META-INF/services/org.apache.commons.logging.LogFactory中的配置類新建工廠物件
if (factory == null) {
try {
final InputStream is = getResourceAsStream(contextClassLoader, SERVICE_ID);
     ...
     factory = newFactory(factoryClassName, baseClassLoader, contextClassLoader );
     ...

//根據commons-logging.properties中的配置屬性org.apache.commons.logging.LogFactory新建工廠物件

  if (factory == null) {
    String factoryClass = props.getProperty(FACTORY_PROPERTY);
    factory = newFactory(factoryClass, baseClassLoader, contextClassLoader);

  //如果以上都沒成功建立工廠物件則直接使用預設的org.apache.commons.logging.impl.LogFactoryImpl類建立

factory = newFactory(FACTORY_DEFAULT, thisClassLoader, contextClassLoader);

//最後放入快取

cacheFactory(contextClassLoader, factory);

一般上述過程中屬性配置都不加的話預設的logFactory實現類就是LogFactoryImpl,然後進入到它的getInstance方法看下

 

public Log getInstance(String name) throws LogConfigurationException {
        Log instance = (Log) instances.get(name);
        if (instance == null) {
            instance = newInstance(name);
            instances.put(name, instance);
        }
        return instance;
    }

進入newInstance

    protected Log newInstance(String name) throws LogConfigurationException {
        Log instance;
        try {
            if (logConstructor == null) {
                instance = discoverLogImplementation(name);
            }
            else {
                Object params[] = { name };
                instance = (Log) logConstructor.newInstance(params);
            }

            if (logMethod != null) {
                Object params[] = { this };
                logMethod.invoke(instance, params);
            }

            return instance;

進入discoverLogImplementation

    private Log discoverLogImplementation(String logCategory)
        throws LogConfigurationException {
        initConfiguration();
        Log result = null;

    //
findUserSpecifiedLogClassName方法內部也是讀取一些系統配置項,如果讀取到了就根據配置的類名來建立日誌物件
        String specifiedLogClassName = findUserSpecifiedLogClassName();
        if (specifiedLogClassName != null) {
            result = createLogFromClass(specifiedLogClassName,logCategory,true);
            if (result == null) {
                StringBuffer messageBuffer =  new StringBuffer("User-specified log class '");
                messageBuffer.append(specifiedLogClassName);
                messageBuffer.append("' cannot be found or is not useable.");
                informUponSimilarName(messageBuffer, specifiedLogClassName, LOGGING_IMPL_LOG4J_LOGGER);
                informUponSimilarName(messageBuffer, specifiedLogClassName, LOGGING_IMPL_JDK14_LOGGER);
                informUponSimilarName(messageBuffer, specifiedLogClassName, LOGGING_IMPL_LUMBERJACK_LOGGER);
                informUponSimilarName(messageBuffer, specifiedLogClassName, LOGGING_IMPL_SIMPLE_LOGGER);
                throw new LogConfigurationException(messageBuffer.toString());
            }
            return result;
        }
     //如果上面沒有建立成功則根據classesToDiscover陣列的值作為類名依次建立日誌物件,直到建立成功(不為空)就返回
     for(int i=0; i<classesToDiscover.length && result == null; ++i) { result = createLogFromClass(classesToDiscover[i], logCategory, true); } if (result == null) { throw new LogConfigurationException ("No suitable Log implementation"); } return result; }

其中classesToDiscover陣列的值是寫死的

private static final String[] classesToDiscover = {
  "org.apache.commons.logging.impl.Log4JLogger",
  "org.apache.commons.logging.impl.Jdk14Logger",
  "org.apache.commons.logging.impl.Jdk13LumberjackLogger",
  "org.apache.commons.logging.impl.SimpleLog"
}

所以當我們的專案中引入的commons-logging和Log4j的jar包,其實不需要做任何配置,就會優先使用Log4JLogger做為實際的寫日誌實現類

整個過程中有兩點需要注意的:

1.日誌工廠物件和日誌實現類物件都是先使用當前類或者thread context的classLoad將類載入進來再通過反射建立物件,這樣的話如果有的外掛使用自定義的classLoad載入,當外掛內部列印日誌時可能會出現無法建立日誌物件或者使用了和預期不一致的日誌物件

2.動態查詢:對日誌工廠實現類和日誌實現類的動態查詢基本上都是通過讀取系統配置或者程式碼裡寫死的方式來查詢,其實可以通過java SPI機制來實現

slf4J的實現方式與上述是完全不同的,它採用的是靜態繫結,可以避免classLoad的問題,但在使用時要引入各種橋接包,如果引入了兩個相反的橋接包就會導致迴圈依賴的StackOverflow

相關文章