myBatis原始碼解析-日誌篇(1)

超人小冰發表於2020-07-27

上半年在進行知識儲備,下半年爭取寫一點好的部落格來記錄自己原始碼之路。在學習原始碼的路上也掌握了一些設計模式,可所謂一舉兩得。本次打算寫Mybatis的原始碼解讀。

準備工作

1. 下載mybatis原始碼

下載地址:https://github.com/mybatis/mybatis-3 

2. 下載mybatis-parent原始碼

下載地址:https://github.com/mybatis/parent

3. 編譯

進入mybatis-paren所在資料夾

mvn clean install

進入mybatis所在資料夾

mvn clean 
mvn install -Dmaven.test.skip=true

4. 用IDEA或Eclipse開啟mybatis即可

原始碼分析-日誌模組

1. 日誌基礎包

 

 

 

package org.apache.ibatis.logging;

// mybatis自定義介面,提供四種級別 error->debug->trace->warn
public interface Log {
  boolean isDebugEnabled();
  boolean isTraceEnabled();
  void error(String s, Throwable e);
  void error(String s);
  void debug(String s);
  void trace(String s);
  void warn(String s);
}

如上,mybatis提供了日誌的四種級別,error->debug->trace->warn

2. 從原始碼可以看到,mybatis提供瞭如jdk14,log4j,log4j2等日誌實現,分析常用的如log4j2原始碼

// log4j的介面卡
public class Log4j2Impl implements Log {

  // 真正提供日誌能力的log4j的日誌類
  private Log log;
  // 構造方法,匯入真正的實現類
  public Log4j2Impl(String clazz) {
    Logger logger = LogManager.getLogger(clazz);

    if (logger instanceof AbstractLogger) {
      log = new Log4j2AbstractLoggerImpl((AbstractLogger) logger);
    } else {
      log = new Log4j2LoggerImpl(logger);
    }
  }
  
  public boolean isDebugEnabled() {
    return log.isDebugEnabled();
  }

  public boolean isTraceEnabled() {
    return log.isTraceEnabled();
  }
  // 呼叫真正的實現方法
  public void error(String s, Throwable e) {
    log.error(s, e);
  }
 // 呼叫真正的實現方法
  public void error(String s) {
    log.error(s);
  }
// 呼叫真正的實現方法
  public void debug(String s) {
    log.debug(s);
  }
// 呼叫真正的實現方法
  public void trace(String s) {
    log.trace(s);
  }
// 呼叫真正的實現方法
  public void warn(String s) {
    log.warn(s);
  }

}

綜上,可以看到,mybatis並沒有提供真正的日誌實現介面,只是定義了一套自己的日誌介面,其實現交給真正的具體日誌類(如log4j,log4j2)。此處用到了介面卡模式。比如log4j2,mybatis提供自定義介面,提供log4j2介面卡繼承自定義介面,在log4j2介面卡裡呼叫log4j2的真實方法來實現自己的介面。

3.各日誌預設呼叫流程

看原始碼包中有很多日誌實現,那具體的預設呼叫流程是怎樣。檢視org.apache.ibatis.logging包中的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->commonsLoging->log4j2->log4j->jdklog
static {
// 呼叫tryImplementation方法
tryImplementation(new Runnable() {
public void run() {
useSlf4jLogging();
}
});
tryImplementation(new Runnable() {
public void run() {
useCommonsLogging();
}
});
tryImplementation(new Runnable() {
public void run() {
useLog4J2Logging();
}
});
tryImplementation(new Runnable() {
public void run() {
useLog4JLogging();
}
});
tryImplementation(new Runnable() {
public void run() {
useJdkLogging();
}
});
tryImplementation(new Runnable() {
public void run() {
useNoLogging();
}
});
}

......

LogFactory有預設的日誌載入順序,寫在靜態程式碼塊中。預設實現順序為slf4j->commonsLoging->log4j2->log4j->jdklog。接著分析tryImplementation方法

// 此方法呼叫的是執行緒的run方法,仔細看,是直接run,不是start,所以不是多執行緒
  private static void tryImplementation(Runnable runnable) {
    // 判斷全域性的日誌構造方法是否為空,若為空,則呼叫執行緒的run方法
    if (logConstructor == null) {
      try {
        runnable.run();
      } catch (Throwable t) {
         // 對沒有具體實現的日誌類,直接忽略,不拋異常,
        // ignore
      }
    }
  }

檢視具體的run方法,此處繼續分析log4j2的run方法。

 public static synchronized void useLog4J2Logging() {
    setImplementation(org.apache.ibatis.logging.log4j2.Log4j2Impl.class);
  }



 // 使用到了java的反射機制
  private static void setImplementation(Class<? extends Log> implClass) {
    try {
      // 利用反射獲取log4j2的真實構造方法
      Constructor<? extends Log> candidate = implClass.getConstructor(new Class[] { String.class });
      // 獲取log4j2的例項
      Log log = candidate.newInstance(new Object[] { LogFactory.class.getName() });
      log.debug("Logging initialized using '" + implClass + "' adapter.");
      // 賦值給全域性變數logConstructor
      logConstructor = candidate;
    } catch (Throwable t) {
      throw new LogException("Error setting Log implementation.  Cause: " + t, t);
    }
  }

以log4j2為例,分析了具體的日誌實現。但我們使用mybatis時通常會在執行sql的時候,對sql進行列印輸出。所以接下來檢視mybatis如何使用這些日誌介面。

4. 分析org.apache.ibatis.logging.jdbc下的類

在原始碼分析前,回顧下jdbc連線資料庫方式。

         ........
         Class.forName("com.mysql.jdbc.Driver");
         //2.獲得資料庫連結
         Connection conn=DriverManager.getConnection(URL, USER, PASSWORD);
         //3.通過資料庫的連線運算元據庫,實現增刪改查(使用Statement類)
         Statement st=conn.createStatement();
         ResultSet rs=st.executeQuery("select * from user");
         .........

載入資料庫忽略,此處有Connection,Statement,ResultSet等引數,檢視org.apache.ibatis.logging.jdbc下的類,都有對應的log類,先分析BaseJdbcLogger類。

public abstract class BaseJdbcLogger {
  // PrepareedStatement下的所有set方法
  protected static final Set<String> SET_METHODS = new HashSet<String>();
  protected static final Set<String> EXECUTE_METHODS = new HashSet<String>();
  // PrepareecdStatement 設定的key value pair
  private Map<Object, Object> columnMap = new HashMap<Object, Object>();
  // PreparedStatement 設定的column
  private List<Object> columnNames = new ArrayList<Object>();
  // PrepareedStatement 設定的value
  private List<Object> columnValues = new ArrayList<Object>();

  protected Log statementLog;
  protected int queryStack;

  /*
   * Default constructor
   */
  public BaseJdbcLogger(Log log, int queryStack) {
    this.statementLog = log;
    if (queryStack == 0) queryStack = 1;
    this.queryStack = queryStack;
  }
  // 初始化預設的一些set方法
  static {
    SET_METHODS.add("setString");
    SET_METHODS.add("setInt");
    SET_METHODS.add("setByte");
    SET_METHODS.add("setShort");
    SET_METHODS.add("setLong");
    SET_METHODS.add("setDouble");
    SET_METHODS.add("setFloat");
    SET_METHODS.add("setTimestamp");
    SET_METHODS.add("setDate");
    SET_METHODS.add("setTime");
    SET_METHODS.add("setArray");
    SET_METHODS.add("setBigDecimal");
    SET_METHODS.add("setAsciiStream");
    SET_METHODS.add("setBinaryStream");
    SET_METHODS.add("setBlob");
    SET_METHODS.add("setBoolean");
    SET_METHODS.add("setBytes");
    SET_METHODS.add("setCharacterStream");
    SET_METHODS.add("setClob");
    SET_METHODS.add("setObject");
    SET_METHODS.add("setNull");

    EXECUTE_METHODS.add("execute");
    EXECUTE_METHODS.add("executeUpdate");
    EXECUTE_METHODS.add("executeQuery");
    EXECUTE_METHODS.add("addBatch");
  }
  // setColumn方會記錄設定的column和對應的value
  protected void setColumn(Object key, Object value) {
    columnMap.put(key, value);
    columnNames.add(key);
    columnValues.add(value);
  }

再來分析ConnectionLogger類

// 實現InvocationHandler就知道是個代理類
public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler {
  // 真正的connection
  private Connection connection;

  private ConnectionLogger(Connection conn, Log statementLog, int queryStack) {
    super(statementLog, queryStack);
    this.connection = conn;
  }
  // 增強
  public Object invoke(Object proxy, Method method, Object[] params)
      throws Throwable {
    try {
      // 若是從Object繼承的方法直接忽略
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, params);
      }
      // 若是prepareStatement方法
      if ("prepareStatement".equals(method.getName())) {
        // 列印sql引數
        if (isDebugEnabled()) {
          debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
        }
        // 呼叫connection真實方法
        PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
        // 將生成的PreparedStatement也構建成代理物件
        stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
        return stmt;
      } else if ("prepareCall".equals(method.getName())) { // 若是prepareCall方法
        if (isDebugEnabled()) {
          debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
        }        
        PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
        stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
        return stmt;
      } else if ("createStatement".equals(method.getName())) { // 若是createStatement方法
        Statement stmt = (Statement) method.invoke(connection, params);
        stmt = StatementLogger.newInstance(stmt, statementLog, queryStack);
        return stmt;
      } else {
        return method.invoke(connection, params);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

  /*
   * Creates a logging version of a connection
   *
   * @param conn - the original connection
   * @return - the connection with logging
   */
  public static Connection newInstance(Connection conn, Log statementLog, int queryStack) {
    InvocationHandler handler = new ConnectionLogger(conn, statementLog, queryStack);
    ClassLoader cl = Connection.class.getClassLoader();
    return (Connection) Proxy.newProxyInstance(cl, new Class[]{Connection.class}, handler);
  }

  /*
   * return the wrapped connection
   *
   * @return the connection
   */
  public Connection getConnection() {
    return connection;
  }

}

後續如PreparedStatementLogger,ResultSetLogger等也是一樣,都是封裝了PreparedStatement或ResultSet,在執行真實語句前後進行日誌列印,列印執行的Sql語句,此處用到了代理模式。熟悉AOP的可能對此有了解。

 

相關文章