分析mybatis如何實現打印sql語句


  使用mybatis查詢數據庫時,若日志級別為debug時,自動打印sql語句,參數值以及結果集數目,類似這樣

==>  Preparing: select id, description from demo where id = ? 
==>  Parameters: 1(Integer)
<==  Total: 1

  若是使用jdbc,打印類似日志,原有的jdbc邏輯里,我們需要插入日志打印邏輯

 1 String sql = "select id, description from demo where id = ?";
 2 System.out.println("==>  Preparing: " + sql);
 3 PreparedStatement psmt = conn.prepareStatement(sql);
 4 System.out.println("==>  Parameters: 1(Integer)");
 5 psmt.setInt(1,1);
 6 ResultSet rs = psmt.executeQuery();
 7 int count = 0;
 8 while(rs.next()) {
 9     count++;
10 }
11 System.out.println("<==  Total:" + count);

  這樣做是因為我們無法改變jdbc代碼,不能讓數據庫連接獲取PreparedStatement對象時,告訴它你把傳給你的sql語句打印出來吧。這時候就在想如果prepareStatement有一個日志打印的功能就好了,還要可以傳入日志對象和日志格式參數就更好了,可惜它沒有這樣的功能。

  我們只能額外在獲取PreparedStatement對象時,PreparedStatement對象設置參數時和ResultSet處理返回結果集時這些邏輯之上加上日志打印邏輯,這是很讓人沮喪的代碼。其實很容易想到,這種受限於原有功能實現,需要增強其能力,正是代理模式適用的場景,我們只需要創建對應的代理類去重寫我們想要增強的方法就好。

 

  回到mybatis。mybatis查詢數據庫時也是使用的jdbc,mybatis作為一種持久層框架,使用了動態代理來增強jdbc原有邏輯,代碼在org.apache.ibatis.logging.jdbc包下,下面從getMapper來逐步分析mybatis如何實現打印sql語句。

  mybatis有兩種接口調用方式,一種是基於默認方法傳入statementID,另一種是基於Mapper接口,其實兩種方式是一樣的,一會就可以知道,先介紹getMapper。

  getMapper是mybatis頂層API SqlSession的一個方法,默認實現

public class DefaultSqlSession implements SqlSession {
   // mybatis配置信息對象
private final Configuration configuration; @Override public <T> T getMapper(Class<T> type) { return configuration.<T>getMapper(type, this); } }
  Configuration 保存解析后的配置信息,繼續往下走
public class Configuration { 
  
protected final MapperRegistry mapperRegistry = new MapperRegistry(this); public <T> T getMapper(Class<T> type, SqlSession sqlSession) { return mapperRegistry.getMapper(type, sqlSession); } }
  MapperRegistry 保存着Mapper接口與對應代理工廠的映射關系
public class MapperRegistry {
    private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();

    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
        if (mapperProxyFactory == null) {
          throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
        }
        try {
          return mapperProxyFactory.newInstance(sqlSession);
        } catch (Exception e) {
          throw new BindingException("Error getting mapper instance. Cause: " + e, e);
        }
    }
}
  MapperProxyFactory是一個生成Mapper代理類的工廠,使用動態代理去生成具體的mapper接口代理類
public class MapperProxyFactory<T> {
   // 構造器中初始化
private final Class<T> mapperInterface; protected T newInstance(MapperProxy<T> mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } public T newInstance(SqlSession sqlSession) { final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); } }  
  分析MapperProxy定義的代理行為,調用mapper接口里面的方法時,都會走這里,這里完成了mapper接口方法與Mapper.xml中sql語句的綁定,相關參數已在MapperMethod構造器中初始化,這里邏輯較為復雜,簡單來說,就是讓接口中的方法指向具體的sql語句
public class MapperProxy<T> implements InvocationHandler, Serializable {
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else if (isDefaultMethod(method)) {
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
   // 這里關聯mapper中接口方法與Mapper.xml中的sql語句
final MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); } }

  下一步就是MapperMethod具體的執行邏輯了,這里內容較多,主要是對執行sql的類型進行判斷,簡單截取select部分

case SELECT:
  if (method.returnsVoid() && method.hasResultHandler()) {
    executeWithResultHandler(sqlSession, args);
    result = null;
  } else if (method.returnsMany()) {
    result = executeForMany(sqlSession, args);
  } else if (method.returnsMap()) {
    result = executeForMap(sqlSession, args);
  } else if (method.returnsCursor()) {
    result = executeForCursor(sqlSession, args);
  } else {
    Object param = method.convertArgsToSqlCommandParam(args);
    result = sqlSession.selectOne(command.getName(), param);
  }
  break;

  看到這里就可以發現原來當我們使用getMapper生成的代理類型時,調用內部自定義方法,仍然是基於mybatis默認方法的。不過這好像和打印sql語句沒啥關系,重點在類似 sqlSession.selectOne(command.getName(), param)方法,依舊去查看其默認實現DefaultSqlSession,可以發現是調用內部的執行器去執行查詢方法的

  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
    // mybatis存在3種執行器,批處理、緩存和標准執行器
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }

  這里的執行器是在構造方法內賦值的,默認情況下使用的是SimpleExecutor,這里省略父類query方法中的緩存相關代碼,重點是子類去實現的doQuery方法

  @Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.<E>query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    stmt = handler.prepare(connection, transaction.getTimeout());
    handler.parameterize(stmt);
    return stmt;
  }

  這里獲取Connection對象時,猜測mybatis一定是對Connection進行了增強,不然無法在獲取Statement 之前打印sql語句

protected Connection getConnection(Log statementLog) throws SQLException {
    Connection connection = transaction.getConnection();
    if (statementLog.isDebugEnabled()) {
      return ConnectionLogger.newInstance(connection, statementLog, queryStack);
    } else {
      return connection;
    }
}
  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); }

  代碼很清楚的,如果你的當前系統支持打印debug日志,那么就動態幫你生成一個連接對象,這里傳入的代理行為是這個類本身,那么只需要分析其的invoke方法就好了,這里依舊只分析一部分必要的

public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
  if (Object.class.equals(method.getDeclaringClass())) {
    return method.invoke(this, params);
  }    
  if ("prepareStatement".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;
  }
}
    

  在這里找到了打印的sql信息,還可以發現下面的PreparedStatementLogger繼續增強,邏輯都是一樣的,就不分析了。有一點需要注意的,這里是mapper接口的動態代理類,所以這里的日志級別是受接口所在包的日志級別控制的,只需要配置mapper接口的日志級別是debug,就可以看見打印的sql語句了。

  

  到這里,已經知道了,sql信息是如何打印出來的了,可是,還有一個問題需要解決,這里的日志對象是怎么來的,mybatis本身是沒有日志打印能力的。

  mybatis本身並沒有內嵌日志框架,而是考慮了用戶本身的日志框架的選擇。簡而言之,就是用戶用啥日志框架,它就用什么框架,當用戶存在多種選擇時,它也有自己的偏好設計(可以指定)。這樣做,就需要mybatis兼容市面上常見的日志框架,同時自己也要有一套日志接口,mybatis定義日志輸出級別控制,兼容日志框架提供的具體實現類。這是不是又和一種設計模式很像了,適配器模式,轉換不同接口,實現統一調用,具體的代碼在org.apache.ibatis.logging包下。

  org.apache.ibatis.logging.Log 是mybatis自己定義的日志接口,org.apache.ibatis.logging.LogFactory 是mybatis用於加載合適的日志實現類,其下的眾多包,均是日志框架的適配器類,主要做日志級別的轉換和具體log對象的創建,那么這些適配器類怎么加載的,LogFactory有一個靜態代碼塊去嘗試加載合適的日志框架,然后創建正確的log對象。

public final class LogFactory {
  private static Constructor<? extends Log> logConstructor;

  static {
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useSlf4jLogging();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useCommonsLogging();
      }
    });
    ...
  }

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

  private static void tryImplementation(Runnable runnable) {
    if (logConstructor == null) {
      try {
        runnable.run();
      } catch (Throwable t) {
        // ignore
      }
    }
  }

  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);
    }
  }
}
View Code

  這里會按順序去嘗試加載不同的日志框架,若當前系統中存在對應的日志框架,才可以加載成功,這樣logConstructor就有值了,下面則不會再嘗試加載,在getLog里面則是實例化具體的日志對象。

 

  這樣就完成了mybatis如何打印sql語句的整體流程解析,主要有兩點,創建Log對象和通過動態代理給jdbc增加日志打印能力。

 


免責聲明!

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



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