天天看點

Mybatis 源碼學習(11)-日志子產品

曆史文章:

Mybatis 源碼學習(10)-類型轉換(TypeAliasRegistry)

Java 開發中常用的日志工具類包括Log4J、Log4J2、Apache Common Log、java.util.logging、Slf4j,這些工具的接口并不統一,為了提供統一的接口,Mybatis 對這些日志接口做了統一适配。

Mybatis 的日志子產品使用了擴充卡模式,其内部提供了統一的擴充卡接口:org.apache.ibatis.logging.Log,通過實作對接不同的第三方日志元件,實作多個 Adapter,進而将第三方元件适配成 org.apache.ibatis.logging.Log 對象,這樣 Mybatis 内部就可以統一通過 org.apache.ibatis.logging.Log 接口調用第三方元件。

日志擴充卡

前面已經提到,第三方日志元件各自具有自己的日志級别,如 java.util.logging 包含 ALL、FINEST、FINER、FINE、CONFIG、INFO、WARNING、SEVERE、OFF 這 9 種日志級别,而 Log4j2 則隻有 trace、debug、info、warn、error、fatal 這 6 種日志級别。Mybatis 僅支援 trace、debug、warn、error 這四種級别的日志。

Mybatis 的日志子產品位于 org.apache.ibatis.logging 包内,通過 Log 接口對外提供功能,LogFactory 工廠類對外提供日志元件适配功能。

Mybatis 源碼學習(11)-日志子產品

LogFactory 工廠類,使用其内部的靜态代碼初識化日志擴充卡,并使用 LogFactory.logConstructor 記錄首個适配成功的日志擴充卡。

// 記錄第一個搜尋到的日志元件的構造器
private static Constructor<? extends Log> logConstructor;

static {
  // 針對每種日志元件嘗試加載,預設順序是:
  // useSlf4jLogging ->useCommonsLogging -> useLog4J2Logging ->
  // useLog4JLogging -> useJdkLogging -> useNoLogging
  tryImplementation(new Runnable() {
    @Override
    public void run() {
      useJdkLogging();
    }
  });
  // … 省略其他方法
}
           

LogFactory.tryImplementation

方法會檢查是否已查找到可以日志擴充卡,如果尚未找到,則使用 Runnable.run 嘗試加載對應的日志擴充卡,嘗試失敗則忽略異常,嘗試成功則記錄下該日志擴充卡的構造器。

// 嘗試加載
private static void tryImplementation(Runnable runnable) {
  if (logConstructor == null) { // 未找到時,才允許加載
    try {
      runnable.run();
    } catch (Throwable t) {
      // 忽略所有異常
    }
  }
}

// 加載 Slf4
public static synchronized void useJdkLogging() {
  setImplementation(org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl.class);
}

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(“…”);
    }
    // 如果加載正常,則初始化 logConstructor
    logConstructor = candidate;
  } catch (Throwable t) {
    throw new LogException(“…”);
  }
}
           

Jdk14LoggingImpl 實作了 org.apache.ibatis.logging.Log 接口,并封裝了 java.util.logging.Logger,所有的日志操作,均由 java.util.logging.Logger 實作。

public class Jdk14LoggingImpl implements Log {

  private final Logger log; // 封裝 java.util.logging.Logger 對象

  public Jdk14LoggingImpl(String clazz) {
    log = Logger.getLogger(clazz); // 初始化 java.util.logging.Logger 對象
  }

   // 所有的日志請求都由 java.util.logging.Logger 進行操作
  @Override
  public void debug(String s) {
    log.log(Level.FINE, s);
  }

  @Override
  public void warn(String s) {
    log.log(Level.WARNING, s);
  }
  // … 忽略其他方法
}
           

其他幾個日志擴充卡的邏輯和 Jdk14LoggingImpl 的邏輯類似。

JDBC 日志功能

除了基本的日志适配外,Mybatis 還提供了 org.apache.ibatis.logging.jdbc 包,這個包提供了 SQL 日志調試功能,可以列印 SQL 語句、SQL 參數、結果集的内容以及行數。該目錄下的 BaseJdbcLogger 是所有 SQL 日志類的基類,内部定義了 SET_METHODS、EXECUTE_METHODS 兩個集合,用于記錄 PreparedStatement 執行的方法和設定參數的方法(set* 方法)。

Mybatis 源碼學習(11)-日志子產品

BaseJdbcLogger 内的兩個集合的定義:

// PreparedStatement 接口定義的 set*() 方法集合
protected static final Set<String> SET_METHODS = new HashSet<String>();
// 記錄 Statement 和 PreparedStatement 執行 SQL 的方法集合
protected static final Set<String> EXECUTE_METHODS = new HashSet<String>();

// 初始化兩個集合
static {
  SET_METHODS.add("setString");
  SET_METHODS.add("setNString");
  SET_METHODS.add("setInt");
  // … 省略其他 set*() 方法

  EXECUTE_METHODS.add("execute");
  EXECUTE_METHODS.add("executeUpdate");
  EXECUTE_METHODS.add("executeQuery");
  EXECUTE_METHODS.add("addBatch");
}
           

BaseJdbcLogger 的核心字段定義如下:

// 記錄 PreparedStatement.set*() 方法設定的鍵值對
private final Map<Object, Object> columnMap = new HashMap<Object, Object>();
// 記錄 PreparedStatement.set*() 方法設定的 key
private final List<Object> columnNames = new ArrayList<Object>();
// 記錄 PreparedStatement.set*() 方法設定的 value
private final List<Object> columnValues = new ArrayList<Object>();
           

BaseJdbcLogger 提供的工具方法會在後邊繼續分析,繼續分析 ConnectionLogger,ConnectionLogger 是 BaseJdbcLogger 的子類,也是基于 JDK 的代理,它實作了 InvocationHandler 接口。ConnectionLogger.newInstance 方法會封裝對應的 Connection 對象,并建立對應的代理對象 —— ConnectionLogger。

public static Connection newInstance(Connection conn, Log statementLog, int queryStack) {
  // 傳入 Connection 對象,并建立 InvocationHandler
  InvocationHandler handler = new ConnectionLogger(conn, statementLog, queryStack);
  ClassLoader cl = Connection.class.getClassLoader();
  // 建立 Connection 的代理對象
  return (Connection) Proxy.newProxyInstance(cl, new Class[]{Connection.class}, handler);
}
           

ConnectionLogger.invoke 是實際執行 Connection 方法的具體實作,它會攔截 prepareStatement、prepareCall、createStatement 方法并執行實際的 Connection 的方法。

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、prepareCall、createStatement 方法,
    // 并為 Statement 建立對應的代理對象
    if ("prepareStatement".equals(method.getName())) {
      if (isDebugEnabled()) {
        debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
      }
      // 實際執行底層 Connection.prepareStatement 方法,擷取 PreparedStatement 對象
      PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
      // 為該 PreparedStatement 建立代理對象
      stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
      return stmt;
    } else if ("prepareCall".equals(method.getName())) {
      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())) {
      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);
  }
}
           

PreparedStatementLogger 也繼承自 BaseJdbcLogger,并實作了 InvocationHandler,它的 newInstance 方法也建立了代理對象。PreparedStatementLogger.invoke 方法會為 EXECUTE_METHODS 集合、SET_METHODS 集合、getResultSet 方法、getUpdateCount 方法進行代理。

public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
  try {
    // 直接調用 Object 的方法
    if (Object.class.equals(method.getDeclaringClass())) {
      return method.invoke(this, params);
    }
    // 調用執行類方法
    if (EXECUTE_METHODS.contains(method.getName())) {
      if (isDebugEnabled()) {
        debug("Parameters: " + getParameterValueString(), true);
      }
      clearColumnInfo(); // 清空 BaseJdbcLogger 中記錄的三個參數集合
      if ("executeQuery".equals(method.getName())) {
        // 如果執行的是 executeQuery 則為 ResultSet 建立代理對象
        // 其他 execute* 方法的傳回值不是 ResultSet
        ResultSet rs = (ResultSet) method.invoke(statement, params);
        return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack);
      } else {
        // 其他情況,直接傳回結果
        return method.invoke(statement, params);
      }
    } else if (SET_METHODS.contains(method.getName())) {
      // 調用 SET_METHODS 中記錄的 set*() 方法設定參數,則需要通過 setColumn 方法記錄對應的參數
      if ("setNull".equals(method.getName())) {
        setColumn(params[0], null);
      } else {
        setColumn(params[0], params[1]);
      }
      return method.invoke(statement, params);
    } else if ("getResultSet".equals(method.getName())) {
      // 如果是 getResultSet 方法,則建立 ResultSet 代理
      ResultSet rs = (ResultSet) method.invoke(statement, params);
      return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack);
    } else if ("getUpdateCount".equals(method.getName())) {
      // 如果調用 getUpdateCount 方法,則直接輸出更新影響的行數
      int updateCount = (Integer) method.invoke(statement, params);
      if (updateCount != -1) {
        debug("   Updates: " + updateCount, false);
      }
      return updateCount;
    } else {
      return method.invoke(statement, params);
    }
  } catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
  }
}
           

StatementLogger 和 PreparedStatementLogger 的實作類似,但是它不允許設定參數,是以不會攔截 SET_METHODS 記錄的設定參數的方法。

ResultSetLogger 封裝了 ResultSet,也繼承自 BaseJdbcLogger,實作了 InvocationHandler。它的主要字段如下:

private boolean first = true; // 是否是 ResultSet 的第一行
private int rows; // 影響行數
private final ResultSet rs; // 實際的 ResultSet
private final Set<Integer> blobColumns = new HashSet<Integer>(); // 記錄超長字段的下标

// 長度超長的字段
private static Set<Integer> BLOB_TYPES = new HashSet<Integer>();
static {
  BLOB_TYPES.add(Types.BINARY);
  // … 添加其他類型:Types.BLOB、Types.CLOB、Types.LONGNVARCHAR、
  // Types.LONGVARBINARY、Types.LONGVARCHAR、Types.NCLOB、Types.VARBINARY
}
           

ResultSetLogger.newInstance 也隻是在建立代理對象,ResultSetLogger.invoke 方法會攔截 ResultSet 的 next() 方法,并解析列名和參數值,輸出到日志裡。

public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
  try {
    // 直接轉發繼承自 Object 的方法
    if (Object.class.equals(method.getDeclaringClass())) {
      return method.invoke(this, params);
    }
    Object o = method.invoke(rs, params);
    // 對 next 方法進行攔截
    if ("next".equals(method.getName())) {
      if (((Boolean) o)) { // 是否存在下一行
        rows++;
        if (isTraceEnabled()) {
          ResultSetMetaData rsmd = rs.getMetaData(); // 擷取表頭
          final int columnCount = rsmd.getColumnCount(); // 擷取行數
          if (first) {
            first = false;
            // 輸出表頭,并将超長字段記錄到 blobColumns 中
            printColumnHeaders(rsmd, columnCount);
          }
          // 輸出本行内容,但是特殊展示超長字段
          printColumnValues(columnCount);
        }
      } else { // 列印總共影響的行數
        debug("     Total: " + rows, false);
      }
    }
    clearColumnInfo(); // 清除 BaseJdbcLogger 記錄的參數集合
    return o;
  } catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
  }
}
           

總結

日志擴充卡的邏輯不算複雜,它使用 LogFactory 作為工廠類建立對應的日志擴充卡,而 Log 類提供了日志擴充卡的接口。在進行日志加載時,是按照既定的順序執行的,即如果查找到第一個可用擴充卡時,就不會使用後面的擴充卡了。至于為什麼嘗試加載使用 Runnable.run,這是為了友善,不再新定義操作接口。另外,如果 LogFactory 加載了 classpath 中沒有的日志類,tryImplementation 會忽略掉錯誤,繼續加載後邊的日志擴充卡。

JDBC 類型的日志關鍵在于為每種 JDBC 操作對象建立 InvocationHandler 代理對象,代理對象會攔截每個操作,并根據方法名稱和操作結果生成對應日志資訊。

參考文檔:《Mybatis 技術内幕》

本文的基本脈絡參考自《Mybatis 技術内幕》,編寫文章的原因是希望能夠系統地學習 Mybatis 的源碼,但是如果僅閱讀源碼或者僅從官方文檔很難去系統地學習,是以希望參考現成的文檔,按照文章的脈絡逐漸學習。

歡迎關注我的公衆号:我的搬磚日記,我會定時分享自己的學習曆程。

Mybatis 源碼學習(11)-日志子產品