天天看點

MyBatis源碼解析之基礎子產品—Log

MyBatis源碼解析之基礎子產品—Log

MyBatis源碼解析之基礎子產品—Log

前文回顧

上一章節我們一起學習了DataSource源碼邏輯。本次我們學習MyBatis的Log日志處理子產品。

背景描述

隻要做過技術開發的小夥伴都應該明白日志的重要性。這是用于追蹤線上運作情況及排查問題的利器。如果沒有有效規範的日志輸出,碰到問題特别是線上問題将會陷入一片迷茫,而且線上環境又不能随意調整。而日志中很重要的一部分還是與資料變更相關的日志。從目前國内的java開發來看,采用mybatis作為持久層架構占比更大,是以對mybatis如何處理日志,以及其處理的深層機制我們更應該一探究竟,了解其運作原理。

接下來,我們将通過源碼詳細介紹Log的執行邏輯。

架構設計

Log子產品所在包路徑為

org.apache.ibatis.logging

,其具體劃分如下:

logging
- commons
        JakartaCommonsLoggingImpl
- jdbc
    BaseJdbcLogger
    ConnectionLogger
    PreparedStatementLogger
        ResultSetLogger
        StatementLogger
- jdk14
      Jdk14LoggingImpl
- log4j
      Log4jImpl
- log4j2
    Log4j2AbstractLoggerImpl
    Log4j2Impl
    Log4j2LoggerImpl
- nologging
      NoLoggingImpl
- slf4j
      Slf4jImpl
    Slf4jLocationAwareLoggerImpl
    Slf4jLoggerImpl    
- stdout
      StdOutImpl
- Log
- LogException
- LogFactory           

對應的類架構設計圖如下:

MyBatis源碼解析之基礎子產品—Log

從架構圖中各實作類可以看出,mybatis支援目前各主流的日志元件。

源碼解讀

Log接口

該接口提供了兩個boolean類型的屬性及五個日志級别的方法。内容很簡單,源碼如下:

public interface Log {
  //是否啟用debug
  boolean isDebugEnabled();
  //是否啟用trace
  boolean isTraceEnabled();
  //錯誤日志級别輸出方法
  void error(String s, Throwable e);
  //重載錯誤日志級别輸出方法
  void error(String s);
  //debug日志級别輸出方法
  void debug(String s);
  //trace日志級别輸出方法
  void trace(String s);
  //warn日志級别輸出方法
  void warn(String s);
}           

簡單吧。 而對Log的各實作類。都是采用

implements Log

并 引用相應接口或類的方式來實作的,隻是不同的實作類細節不同而已。接下來分别介紹不同實作類的實作細節:

JakartaCommonsLoggingImpl

該類引組合了common-loggingj.jar 中的Log類,在不同級别的日志輸出時,會調用Log對應的日志輸出方法。

Jdk14LoggingImpl

Jdk14LoggingImpl實作與Log4jImpl類似,調用時傳入對應的日志級别及日志資訊。不過其引用的Logger為java.util.logging.Logger。

StdOutImpl

該實作是通過标準輸出到控制台的方式将日志資訊列印出來,沒有什麼複雜邏輯:

public class StdOutImpl implements Log {
  public StdOutImpl(String clazz) {
    // Do Nothing
  }
  @Override
  public boolean isDebugEnabled() {
    return true;
  }
  @Override
  public boolean isTraceEnabled() {
    return true;
  }
  @Override
  public void error(String s, Throwable e) {
    System.err.println(s);
    e.printStackTrace(System.err);
  }
  @Override
  public void error(String s) {
    System.err.println(s);
  }
  @Override
  public void debug(String s) {
    System.out.println(s);
  }
  @Override
  public void trace(String s) {
    System.out.println(s);
  }
  @Override
  public void warn(String s) {
    System.out.println(s);
  }
}           

NoLoggingImpl

該接口不輸出任何日志,為空實作。

Log4jImpl

該類引組合了apache Log4j.jar 中的Logger類,在不同級别的日志輸出時,會傳入對應的日志級别,及Log4jImpl的全路徑類名。

public class Log4jImpl implements Log {
  private static final String FQCN = Log4jImpl.class.getName();
  private final Logger log;
  public Log4jImpl(String clazz) {
    log = Logger.getLogger(clazz);
  }
  @Override
  public boolean isDebugEnabled() {
    return log.isDebugEnabled();
  }
  @Override
  public boolean isTraceEnabled() {
    return log.isTraceEnabled();
  }
  @Override
  public void error(String s, Throwable e) {
    log.log(FQCN, Level.ERROR, s, e);
  }
  @Override
  public void error(String s) {
    log.log(FQCN, Level.ERROR, s, null);
  }
  @Override
  public void debug(String s) {
    log.log(FQCN, Level.DEBUG, s, null);
  }
  @Override
  public void trace(String s) {
    log.log(FQCN, Level.TRACE, s, null);
  }
  @Override
  public void warn(String s) {
    log.log(FQCN, Level.WARN, s, null);
  }
}           

Log4j2Impl

Log4j2Impl 輸出方法就是調用對應Log的方法,但與衆不同之處就是其構造函數:

/**
   * 根據傳入的全限定類名擷取log4j-api.jar 下對應的Logger對象:
   * 若該對象為AbstractLogger的執行個體,則log的執行個體化為Log4j2AbstractLoggerImpl(Mybatis中Log接口的一種實作,該類會從LogFactory中擷取Mybatis辨別,在執行個體化時傳入logger對象。日志輸出時,需要傳入全路徑類名,日志界别,marker辨別,messeage資訊等)
   * 否則 log的執行個體化對象為Log4j2LoggerImpl(該類的構造函數需要傳入log4j-api中Logger類型參數,并在輸入日志時傳入Maker辨別)
   * @param clazz
   */
   public Log4j2Impl(String clazz) {
    Logger logger = LogManager.getLogger(clazz);

    if (logger instanceof AbstractLogger) {
      log = new Log4j2AbstractLoggerImpl((AbstractLogger) logger);
    } else {
      log = new Log4j2LoggerImpl(logger);
    }
  }           

Slf4jImpl

Slf4jImpl 處理邏輯跟Log4j2Impl基本一緻,實作方式也基本相同,此處不再贅述。

public Slf4jImpl(String clazz) {
    Logger logger = LoggerFactory.getLogger(clazz);

    if (logger instanceof LocationAwareLogger) {
      try {
        // check for slf4j >= 1.6 method signature
        logger.getClass().getMethod("log", Marker.class, String.class, int.class, String.class, Object[].class, Throwable.class);
        log = new Slf4jLocationAwareLoggerImpl((LocationAwareLogger) logger);
        return;
      } catch (SecurityException | NoSuchMethodException e) {
        // fail-back to Slf4jLoggerImpl
      }
    }

    // Logger is not LocationAwareLogger or slf4j version < 1.6
    log = new Slf4jLoggerImpl(logger);
  }           

至此,Mybatis下針對不同日志元件的處理邏輯就結束了。那針對這麼多的日志開源元件,mybatis到底怎麼選擇的呢?咱們繼續往下看。

日志元件加載機制

public final class LogFactory {

  /**
   * Marker to be used by logging implementations that support markers.
   */
  public static final String MARKER = "MYBATIS";

  private static Constructor<? extends Log> logConstructor;

  /**
   * 類加載時執行嘗試設定使用的日志元件:
   * 依次執行直到找到一個可用的,預設第一個調用的為slf4j
   * 備注:第一個設定成功後,logConstructor 設定對應的值(每次判斷該值是否為空來辨別是否繼續嘗試)
   * 注意注意:這是在預設加載時使用的方式,而對于使用者自定一個則直接調用setImplementation(這樣就繞過了系統預設的)
   */
  static {
    tryImplementation(LogFactory::useSlf4jLogging);
    tryImplementation(LogFactory::useCommonsLogging);
    tryImplementation(LogFactory::useLog4J2Logging);
    tryImplementation(LogFactory::useLog4JLogging);
    tryImplementation(LogFactory::useJdkLogging);
    tryImplementation(LogFactory::useNoLogging);
  }

  private LogFactory() {
    // disable construction
  }

  public static Log getLog(Class<?> aClass) {
    return getLog(aClass.getName());
  }

  public static Log getLog(String logger) {
    try {
      return logConstructor.newInstance(logger);
    } catch (Throwable t) {
      throw new LogException("Error creating logger for logger " + logger + ".  Cause: " + t, t);
    }
  }

  /**
   * 使用者自定義采用的日志元件,根據使用者在config.xml中setting節點配置的值來決定使用哪個
   * @param clazz
   */
  public static synchronized void useCustomLogging(Class<? extends Log> clazz) {
    setImplementation(clazz);
  }
  //使用slf4j日志元件
  public static synchronized void useSlf4jLogging() {
    setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
  }
  //使用common-logging日志元件
  public static synchronized void useCommonsLogging() {
    setImplementation(org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl.class);
  }
  //使用log4j日志元件
  public static synchronized void useLog4JLogging() {
    setImplementation(org.apache.ibatis.logging.log4j.Log4jImpl.class);
  }
  //使用slf4j2日志元件
  public static synchronized void useLog4J2Logging() {
    setImplementation(org.apache.ibatis.logging.log4j2.Log4j2Impl.class);
  }
  //使用jul下日志元件
  public static synchronized void useJdkLogging() {
    setImplementation(org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl.class);
  }
  //使用控制台标準輸出
  public static synchronized void useStdOutLogging() {
    setImplementation(org.apache.ibatis.logging.stdout.StdOutImpl.class);
  }
  //不使用日志元件
  public static synchronized void useNoLogging() {
    setImplementation(org.apache.ibatis.logging.nologging.NoLoggingImpl.class);
  }
  //嘗試啟用日志元件方法
  private static void tryImplementation(Runnable runnable) {
    if (logConstructor == null) {
      try {
        runnable.run();
      } catch (Throwable t) {
        // ignore
      }
    }
  }

  /**
   * 根據傳入的實作類,擷取對應的構造函數candidate,并根據candidate擷取log執行個體,如果正常執行完,則将candidate指派給logConstructor
   * @param implClass
   */
  private static void setImplementation(Class<? extends Log> implClass) {
    try {
      Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
      Log log = candidate.newInstance(LogFactory.class.getName());
      if (log.isDebugEnabled()) {
        log.debug("Logging initialized using '" + implClass + "' adapter.");
      }
      logConstructor = candidate;
    } catch (Throwable t) {
      throw new LogException("Error setting Log implementation.  Cause: " + t, t);
    }
  }
}           
  1. 首先在類加載時,會執行所有的預設開源日志元件,直到找到一個。
  2. 同時提供useCustomLogging方法,并根據config.xml中的setting配置使用對應的日志元件。

具體的說明請參看源碼中注釋。

Configuration

在Configuration類執行個體化的無參構造函數中,會通過typeAliasRegistry屬性注冊所有的日志實作類。

public Configuration() {
    //注冊日志元件
    typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
    typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);
    typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
    typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class);
    typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class);
    typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class);
    typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class);
  }           

這段registerAlias代碼是 配置生效的基礎。

如何配置指定日志元件

在傳統通過Configuration.xml配置檔案配置日志的代碼如下:

<configuration>
  <settings>
    ...
    <setting name="logImpl" value="LOG4J"/>
    ...
  </settings>
</configuration>           

logImpl

作為typeAlias的key,咱們先埋個伏筆。

我們知道經典的mybatis配置加載采用了下面兩行代碼:

InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(is);           

第一行是将配置檔案轉成輸入流,沒有深入研究的必要。

我們主要跟蹤下第二行。

通過 org.apache.ibatis.session.SqlSessionFactoryBuilder#build(java.io.InputStream)方法,執行了相關解析操作。

SqlSessionFactoryBuilder核心代碼:

public class SqlSessionFactoryBuilder extends BaseBuilder {
  //SqlSessionFactory 組建方法
    public SqlSessionFactory build(InputStream inputStream) {
    return build(inputStream, null, null);
  }
  
  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      //此處執行個體化XMLConfigBuilder,同時在XMLConfigBuilder中,建立 Configuration 執行個體對象,并在Configuration執行個體化時,注冊各種注冊器
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        // 2020/3/13 架構幫我們關閉了讀取流,是以使用者無需關心
        inputStream.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }
  //配置檔案的解析
  public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }
  
  /**
   * 解析mybatis-config.xml配置檔案中所有資訊,采用模闆方法的設計模式
   */
  private void parseConfiguration(XNode root) {
    try {
      //在解析config檔案時,會設定對應的日志配置
      loadCustomLogImpl(settings);
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }
  
  //載入日志配置
  private void loadCustomLogImpl(Properties props) {
    //從這裡可以看到為什麼Configuration中日志的key為什麼設定為【logImpl】
    //logImpl配置的value值可以為:SLF4J、LOG4J、LOG4J2、JDK_LOGGING、COMMONS_LOGGING、STDOUT_LOGGING、NO_LOGGING 
    //擷取對應的類路徑(具體參看resolveClass源碼)
    Class<? extends Log> logImpl = resolveClass(props.getProperty("logImpl"));
    //通過configuration設定使用的日志元件
    configuration.setLogImpl(logImpl);
  }
}           

props.getProperty("logImpl")

此處解答了上面configuration中埋下的伏筆。

稍微提一下MyBatis特點,在閱讀源碼過程中,new Xxxx() 的操作就是調用不同的構造函數 ,沒有特殊的邏輯。

org.apache.ibatis.builder.xml.XMLConfigBuilder#loadCustomLogImpl方法中的resolveClass方法為XMLConfigBuilder繼承的父類BaseBuilder中的方法,其源碼為:

public abstract class BaseBuilder {
  
    protected <T> Class<? extends T> resolveClass(String alias) {
    if (alias == null) {
      return null;
    }
    try {
      return resolveAlias(alias);
    } catch (Exception e) {
      throw new BuilderException("Error resolving class. Cause: " + e, e);
    }
  }
  
  //通過别名擷取對應的類
  protected <T> Class<? extends T> resolveAlias(String alias) {
    return typeAliasRegistry.resolveAlias(alias);
  }
  
  public <T> Class<T> resolveAlias(String string) {
    try {
      if (string == null) {
        return null;
      }
      // issue #748
      String key = string.toLowerCase(Locale.ENGLISH);
      Class<T> value;
      if (typeAliases.containsKey(key)) {
        value = (Class<T>) typeAliases.get(key);
      } else {
        value = (Class<T>) Resources.classForName(string);
      }
      return value;
    } catch (ClassNotFoundException e) {
      throw new TypeException("Could not resolve type alias '" + string + "'.  Cause: " + e, e);
    }
  }
}           

在調loadCustomLogImpl方法中調用configuration.setLogImpl(logImpl);來完成日志元件的設定。

其代碼實作邏輯如下:

public void setLogImpl(Class<? extends Log> logImpl) {
  //不為空時設定,否則使用預設的日志元件
  if (logImpl != null) {
    this.logImpl = logImpl;
    //最終會調用LogFactory中的useCustomLogging方法來設定日志元件
    LogFactory.useCustomLogging(this.logImpl);
  }
}           

此處我們找到了最終的配置入口:LogFactory.useCustomLogging(this.logImpl);

我們現在分析下LogFactory源碼,注意源碼中的注釋說明:

public final class LogFactory {

  /**
   * Marker to be used by logging implementations that support markers.
   */
  public static final String MARKER = "MYBATIS";

  private static Constructor<? extends Log> logConstructor;

  /**
   * 類加載時執行嘗試設定使用的日志元件:
   * 依次執行直到找到一個可用的,預設第一個調用的為slf4j
   * 備注:第一個設定成功後,logConstructor 設定對應的值(每次判斷該值是否為空來辨別是否繼續嘗試)
   * 注意注意:這是在預設加載時使用的方式,而對于使用者自定一個則直接調用setImplementation(這樣就繞過了系統預設的)
   */
  static {
    tryImplementation(LogFactory::useSlf4jLogging);
    tryImplementation(LogFactory::useCommonsLogging);
    tryImplementation(LogFactory::useLog4J2Logging);
    tryImplementation(LogFactory::useLog4JLogging);
    tryImplementation(LogFactory::useJdkLogging);
    tryImplementation(LogFactory::useNoLogging);
  }

  private LogFactory() {
    // disable construction
  }

  public static Log getLog(Class<?> aClass) {
    return getLog(aClass.getName());
  }

  public static Log getLog(String logger) {
    try {
      return logConstructor.newInstance(logger);
    } catch (Throwable t) {
      throw new LogException("Error creating logger for logger " + logger + ".  Cause: " + t, t);
    }
  }

  /**
   * 使用者自定義采用的日志元件,根據使用者在config.xml中setting節點配置的值來決定使用哪個
   * @param clazz
   */
  public static synchronized void useCustomLogging(Class<? extends Log> clazz) {
    setImplementation(clazz);
  }
  //使用slf4j日志元件
  public static synchronized void useSlf4jLogging() {
    setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
  }
  //使用common-logging日志元件
  public static synchronized void useCommonsLogging() {
    setImplementation(org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl.class);
  }
  //使用log4j日志元件
  public static synchronized void useLog4JLogging() {
    setImplementation(org.apache.ibatis.logging.log4j.Log4jImpl.class);
  }
  //使用slf4j2日志元件
  public static synchronized void useLog4J2Logging() {
    setImplementation(org.apache.ibatis.logging.log4j2.Log4j2Impl.class);
  }
  //使用jul下日志元件
  public static synchronized void useJdkLogging() {
    setImplementation(org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl.class);
  }
  //使用控制台标準輸出
  public static synchronized void useStdOutLogging() {
    setImplementation(org.apache.ibatis.logging.stdout.StdOutImpl.class);
  }
  //不使用日志元件
  public static synchronized void useNoLogging() {
    setImplementation(org.apache.ibatis.logging.nologging.NoLoggingImpl.class);
  }
  //嘗試啟用日志元件方法
  private static void tryImplementation(Runnable runnable) {
    if (logConstructor == null) {
      try {
        runnable.run();
      } catch (Throwable t) {
        // ignore
      }
    }
  }

  /**
   * 根據傳入的實作類,擷取對應的構造函數candidate,并根據candidate擷取log執行個體,如果正常執行完,則将candidate指派給logConstructor
   * @param implClass
   */
  private static void setImplementation(Class<? extends Log> implClass) {
    try {
      Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
      Log log = candidate.newInstance(LogFactory.class.getName());
      if (log.isDebugEnabled()) {
        log.debug("Logging initialized using '" + implClass + "' adapter.");
      }
      logConstructor = candidate;
    } catch (Throwable t) {
      throw new LogException("Error setting Log implementation.  Cause: " + t, t);
    }
  }
}           

最終會看到useCustomLogging會調用setImplementation方法來設定對應的日志元件。

至此,mybatis的日志子產品分析完成。

總結

關于MyBatis的Log子產品介紹至此告一段落。感謝垂閱,如有不妥之處請多多指教~

微觀世界,達觀人生。

做一名踏實的coder !

歡迎關注我的個人微信公衆号:todobugs ~