mybaits源碼分析--日志模塊(四)


一.日志模塊

首先日志在我們開發過程中占據了一個非常重要的地位,是開發和運維管理之間的橋梁,在Java中的日志框架也非常多,Log4j,Log4j2,Apache Commons Log,java.util.logging,slf4j等,這些工具對外的接口也都不盡相同,為了統一這些工具,MyBatis定義了一套統一的日志接口供上層使用。如果要看懂首先對於適配器模式要了解下

1.1 Log

Log接口中定義了四種日志級別,相比較其他的日志框架的多種日志級別顯得非常的精簡,但也能夠滿足大多數常見的使用了

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);

}

1.2 LogFactory

LogFactory工廠類負責創建日志組件適配器,通過這玩意能找到具體的實現

在LogFactory類加載時會執行其靜態代碼塊,其邏輯是按序加載並實例化對應日志組件的適配器,然后使用LogFactory.logConstructor這個靜態字段,記錄當前使用的第三方日志組件的適配器。具體代碼如下

1.3 日志應用

如果想知道在MyBatis系統啟動的時候日志框架是如何選擇的,那么首先要在全局配置文件中我們可以設置對應的日志類型選擇

 

 

在Configuration的構造方法中其實是設置的各個日志實現的別名的,其中STDOUT_LOGGING這個也可以在里面找到

 

 然后在解析全局配置文件的時候就會處理日志的設置

 

進入方法

  private void loadCustomLogImpl(Properties props) {
    // 獲取 logImpl設置的 日志 類型
    Class<? extends Log> logImpl = resolveClass(props.getProperty("logImpl"));
    // 設置日志
    configuration.setLogImpl(logImpl);
  }

進入setLogImpl方法中

  public void setLogImpl(Class<? extends Log> logImpl) {
    if (logImpl != null) {
      this.logImpl = logImpl; // 記錄日志的類型
      // 設置 適配選擇
      LogFactory.useCustomLogging(this.logImpl);
    }
  }

再進入useCustomLogging方法

  public static synchronized void useCustomLogging(Class<? extends Log> clazz) {
    setImplementation(clazz);
  }
  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 字段
      logConstructor = candidate;
    } catch (Throwable t) {
      throw new LogException("Error setting Log implementation.  Cause: " + t, t);
    }
  }

這就關聯上了前面在LogFactory中看到的代碼,啟動測試方法看到的日志也和源碼中的對應上來了,還有就是我們自己設置的會覆蓋掉默認的sl4j日志框架的配置

 

1.4 JDBC 日志

當開啟了 STDOUT的日志管理后,當執行SQL操作時會發現在控制台中可以打印出相關的日志信息

那這些日志信息是怎么打印出來的呢?其實在MyBatis中的日志模塊中包含了一個jdbc包,它並不是將日志信息通過jdbc操作保存到數據庫中,而是通過JDK動態代理的方式,將JDBC操作通過指定的日志框架打印出來。下面就來看看它是如何實現的。

1.4.1 BaseJdbcLogger

BaseJdbcLogger是一個抽象類,它是jdbc包下其他Logger的父類。繼承關系如下

 

 從圖中也可以看到4個實現都實現了InvocationHandler接口。屬性含義如下

 // 記錄 PreparedStatement 接口中定義的常用的set*() 方法
  protected static final Set<String> SET_METHODS;
  // 記錄了 Statement 接口和 PreparedStatement 接口中與執行SQL語句有關的方法
  protected static final Set<String> EXECUTE_METHODS = new HashSet<>();

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

  protected final Log statementLog;// 用於日志輸出的Log對象
  protected final int queryStack;  // 記錄了SQL的層數,用於格式化輸出SQL

1.4.2 ConnectionLogger

ConnectionLogger的作用是記錄數據庫連接相關的日志信息,在實現中是創建了一個Connection的代理對象,在每次Connection操作的前后都可以實現日志的操作。

public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler {

  // 真正的Connection對象
  private final Connection connection;

  private ConnectionLogger(Connection conn, Log statementLog, int queryStack) {
    super(statementLog, queryStack);
    this.connection = conn;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] params)
      throws Throwable {
    try {
      // 如果是調用從Object繼承過來的方法,就直接調用 toString,hashCode,equals等
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, params);
      }
      // 如果調用的是 prepareStatement方法
      if ("prepareStatement".equals(method.getName())) {
        if (isDebugEnabled()) {
          debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
        }
        // 創建  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);
    }
  }

  /**
   * 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();
    // 創建了 Connection的 代理對象 目的是 增強 Connection對象 給他添加了日志功能
    return (Connection) Proxy.newProxyInstance(cl, new Class[]{Connection.class}, handler);
  }

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

}

其他幾個xxxxLogger的實現和ConnectionLogger幾乎是一樣的就不重復說了,看懂一個就可以看懂所有

1.4.3 應用實現

 在實際處理的時候,看下日志模塊是如何工作的,在我們要執行SQL語句前需要獲取Statement對象,而Statement對象是通過Connection獲取的,所以在SimpleExecutor中就可以看到相關的代碼

  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    // 獲取 Statement 對象
    stmt = handler.prepare(connection, transaction.getTimeout());
    // 為 Statement 設置參數
    handler.parameterize(stmt);
    return stmt;
  }

先進入如到getConnection方法中

  protected Connection getConnection(Log statementLog) throws SQLException {
    Connection connection = transaction.getConnection();
    if (statementLog.isDebugEnabled()) {
      // 創建Connection的日志代理對象
      return ConnectionLogger.newInstance(connection, statementLog, queryStack);
    } else {
      return connection;
    }
  }

再進入到handler.prepare方法中

  @Override
  public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
    ErrorContext.instance().sql(boundSql.getSql());
    Statement statement = null;
    try {
      statement = instantiateStatement(connection);
      setStatementTimeout(statement, transactionTimeout);
      setFetchSize(statement);
      return statement;
    } catch (SQLException e) {
      closeStatement(statement);
      throw e;
    } catch (Exception e) {
      closeStatement(statement);
      throw new ExecutorException("Error preparing statement.  Cause: " + e, e);
    }
  }

 

 

  @Override
  protected Statement instantiateStatement(Connection connection) throws SQLException {
    String sql = boundSql.getSql();
    if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
      String[] keyColumnNames = mappedStatement.getKeyColumns();
      if (keyColumnNames == null) {
        return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
      } else {
        // 在執行 prepareStatement 方法的時候會進入進入到ConnectionLogger的invoker方法中
        return connection.prepareStatement(sql, keyColumnNames);
      }
    } else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
      return connection.prepareStatement(sql);
    } else {
      return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
    }
  }

 

 在執行sql語句的時候

 

 如果是查詢操作,后面的ResultSet結果集操作,其他是也通過ResultSetLogger來處理的




免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM