帶你一步一步手撕 Mybatis 原始碼加手繪流程圖——執行部分

FrancisQ發表於2019-11-03

在上篇文章中,我向大家介紹了 Mybatis 是如何構建的,總的來說構建部分就是對於配置檔案的對映,而 Mybatis 中另一個很重要的部分就是如何去通過這些配置檔案封裝成的配置物件去執行使用者指定的 SQL 語句並且將結果集封裝成使用者需要的型別

寫原始碼分析也寫了好幾篇文章了,個人覺得如果你真的想去弄懂原始碼原理的,必須要勤動手,光看文章是沒有用的,你需要自己去嘗試 debug,由於我寫的一些原始碼分析篇幅都較長,如果剛興趣最好收藏再看,如果覺得我寫的還不錯那麼就不要吝嗇自己的贊 (#^.^#)

從 DefaultSqlSession 開始

在上篇文章中,我們知道了我們需要建立一個 Sql會話 來執行 CRUD 操作,在 Mybatis 中就是指 SqlSession 介面,其中有一個預設的實現類 DefaultSqlSession ,一般我們使用的就是它。

我們可以看一看 DefaultSqlSession 中的一些方法定義,其中主要就是一些 CRUD 操作。

public class DefaultSqlSession implements SqlSession {

  // 配置物件
  private Configuration configuration;
  // 執行器
  private Executor executor;
  // 是否自動提交
  private boolean autoCommit;
  private boolean dirty;
  
  public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {}

  public DefaultSqlSession(Configuration configuration, Executor executor) {}

  @Override
  public <T> T selectOne(String statement) {}

  //核心selectOne
  @Override
  public <T> T selectOne(String statement, Object parameter) {}

  @Override
  public <K, V> Map<K, V> selectMap(String statement, String mapKey) {}

  @Override
  public <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey) {}

  //核心selectMap
  @Override
  public <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds) {}

  @Override
  public <E> List<E> selectList(String statement) {}

  @Override
  public <E> List<E> selectList(String statement, Object parameter) {}

  //核心selectList
  @Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {}

  @Override
  public void select(String statement, Object parameter, ResultHandler handler) {}

  @Override
  public void select(String statement, ResultHandler handler) {}

  //核心select,帶有ResultHandler,和selectList程式碼差不多的,區別就一個ResultHandler
  @Override
  public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {}

  @Override
  public int insert(String statement) {}

  @Override
  public int insert(String statement, Object parameter) {}

  @Override
  public int update(String statement) {}

  //核心update
  @Override
  public int update(String statement, Object parameter) {}

  @Override
  public int delete(String statement) {}

  @Override
  public int delete(String statement, Object parameter) {}

  @Override
  public void commit() {}

  //核心commit
  @Override
  public void commit(boolean force) {}

  @Override
  public void rollback() {}

  //核心rollback
  @Override
  public void rollback(boolean force) {}

  //核心flushStatements
  @Override
  public List<BatchResult> flushStatements() {}

  // 核心close
  @Override
  public void close() {}

  @Override
  public Configuration getConfiguration() {return configuration;}

  //最後會去呼叫MapperRegistry.getMapper
  @Override
  public <T> T getMapper(Class<T> type) {}

  @Override
  public Connection getConnection() {}

  //核心clearCache
  @Override
  public void clearCache() {}

  //檢查是否需要強制commit或rollback
  private boolean isCommitOrRollbackRequired(boolean force) {}

  //把引數包裝成Collection
  private Object wrapCollection(final Object object) {}

  //嚴格的Map,如果找不到對應的key,直接拋BindingException例外,而不是返回null
  public static class StrictMap<V> extends HashMap<String, V> {}

}
複製程式碼

其實主要的就是一些 CRUD 操作,今天我們就來分析一下——Mybatisselect 這樣的操作是怎麼執行的

Mybatis 執行 select操作的入口

上面的 DefaultSqlSession 原始碼我只是貼了一些方法的定義,如果你具體去看原始碼你會發現很多 select 方法中都會呼叫相同的 selectList 方法(包括 selectOne 其實也是呼叫的 selectList 只不過它只取了第一個元素),該方法具體原始碼如下。

public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      //根據statement id找到對應的MappedStatement
      MappedStatement ms = configuration.getMappedStatement(statement);
      //轉而用執行器來查詢結果,注意這裡傳入的ResultHandler是null
      // rowBounds 不用管 是用來進行邏輯分頁操作的
      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();
    }
}
複製程式碼

處理流程很簡單分為兩步。

  1. 根據 statementIdConfiguration 中獲取對應的 MappedStatement 物件。
  2. 傳入獲取到的 MappedStatement 然後傳入執行器 Executor 的查詢 query 方法並呼叫返回結果。其中還做了對引數 parameter 的包裝操作等等

在我做分析原始碼的時候其實還有一個疑問,就是 這個 Executor 是哪個具體的 Executor? 因為 Executor 只是一個介面,並且在 DefaultSqlSession 中只是定義了這麼一個欄位並沒有進行例項化,那麼這個 Executor 到底是介面中的哪一個實現類呢?

這個問題的原因還需要在我們初始化過程中去尋找。從上一篇文章中我們知道,在 SqlSession 的構建過程中,Mybatis 使用了 工廠模式構建者模式,最終構建的 DefaultSqlSession 是通過 DefaultSqlSessionFactory 建立的,而 DefaultSqlSessionFactory 又是通過 DefaultSqlSessionFactoryBuilder 構建的。

帶你一步一步手撕 Mybatis 原始碼加手繪流程圖——執行部分

// 建立 DefaultSqlSessionFactory 
// 這是在 DefaultSqlSessionFactoryBuilder 中的方法
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
複製程式碼

上面是通過 構建者模式 建立工廠,得到工廠之後我們還需要通過 工廠模式 建立 DefaultSqlSession

// 其中有很多 openSession 建立方法 最終還是會
// 呼叫openSessionFromDataSource
@Override
public SqlSession openSession() {
  // 這裡傳入了一個 defaultExecutorType 
  return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
// 對應著上面 返回的是 DefaultExecutorType
public ExecutorType getDefaultExecutorType() {
  return defaultExecutorType;
}

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      //通過事務工廠來產生一個事務 不用管
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      //生成一個執行器(事務包含在執行器裡)
      // 這裡你會發現執行器還是在 Configuration 中建立的
      // 我們知道上面傳入的 type 是 default型別的
      final Executor executor = configuration.newExecutor(tx, execType);
      //然後產生一個DefaultSqlSession
      // 這裡傳入了執行器
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      //如果開啟事務出錯,則關閉它
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      //最後清空錯誤上下文
      ErrorContext.instance().reset();
    }
}
複製程式碼

上面就是 DefaultSqlSession 的構建流程了,你會發現我們剛剛需要找的 Executor 就是在這裡例項化了,而且最終是在 Configuration 中建立的。

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    // 這句再做一下保護,囧,防止粗心大意的人將defaultExecutorType設成null
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    // 然後就是簡單的3個分支,產生3種執行器BatchExecutor/ReuseExecutor/SimpleExecutor
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    // 如果要求快取,生成另一種CachingExecutor(預設就是有快取),裝飾者模式,所以預設都是返回CachingExecutor
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    //此處呼叫外掛,通過外掛可以改變Executor行為
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}
複製程式碼

通過上面的原始碼,我們就能得出一個結論,如果指定了執行器型別 Executor 那麼就會建立對應型別的執行器,如果不指定那麼就是預設的 DefaultExecutor但是不管是什麼型別,如果在 Configuartion 中指定了 cacheEnabled 為 true (即開啟快取),那麼就會通過一個 CachingExecutor 再次包裝原本的 Executor,所以如果開啟快取那麼返回的就是 CachingExecutor 。這裡用到了 裝飾器模式,我們可以看一下 執行器的 UML 圖。

帶你一步一步手撕 Mybatis 原始碼加手繪流程圖——執行部分

當然這個時候你肯定還會有一個問題,這個 cacheEnabled 又是啥時候初始化的?這裡我就長話短說,還記得我們進行配置檔案到配置物件是通過 XMLConfigBuilder 這個構造者嗎?其實這個 cachedEnabledmybatis-config.xml 檔案中的 <settings> 標籤底下需要配置的,如果沒有配置那麼 Mybatis 會預設配置為 true。下面我貼出了預設配置的原始碼,其中就有 cachedEnabled 和 預設的執行器型別 ExecutorType,你可以搜尋一下找到答案(這個方法在 XMLConfigBuilder 中)。

private void settingsElement(XNode context) throws Exception {
    if (context != null) {
      Properties props = context.getChildrenAsProperties();
      // Check that all settings are known to the configuration class
      //檢查下是否在Configuration類裡都有相應的setter方法(沒有拼寫錯誤)
      MetaClass metaConfig = MetaClass.forClass(Configuration.class);
      for (Object key : props.keySet()) {
        if (!metaConfig.hasSetter(String.valueOf(key))) {
          throw new BuilderException("The setting " + key + " is not known.  Make sure you spelled it correctly (case sensitive).");
        }
      }
      
      //下面非常簡單,一個個設定屬性
      //如何自動對映列到欄位/ 屬性
      configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
      //快取
      configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
      //proxyFactory (CGLIB | JAVASSIST)
      //延遲載入的核心技術就是用代理模式,CGLIB/JAVASSIST兩者選一
      configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));
      //延遲載入
      configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false));
      //延遲載入時,每種屬性是否還要按需載入
      configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), true));
      //允不允許多種結果集從一個單獨 的語句中返回
      configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true));
      //使用列標籤代替列名
      configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true));
      //允許 JDBC 支援生成的鍵
      configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false));
      //配置預設的執行器
      configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));
      //超時時間
      configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null));
      //是否將DB欄位自動對映到駝峰式Java屬性(A_COLUMN-->aColumn)
      configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false));
      //巢狀語句上使用RowBounds
      configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false));
      //預設用session級別的快取
      configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
      //為null值設定jdbctype
      configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER")));
      //Object的哪些方法將觸發延遲載入
      configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString"));
      //使用安全的ResultHandler
      configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true));
      //動態SQL生成語言所使用的指令碼語言
      configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage")));
      //當結果集中含有Null值時是否執行對映物件的setter或者Map物件的put方法。此設定對於原始型別如int,boolean等無效。 
      configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
      //logger名字的字首
      configuration.setLogPrefix(props.getProperty("logPrefix"));
      //顯式定義用什麼log框架,不定義則用預設的自動發現jar包機制
      configuration.setLogImpl(resolveClass(props.getProperty("logImpl")));
      //配置工廠
      configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
    }
}
複製程式碼

我們在這裡小結一下,在執行 SqlSession 中的一些 select 操作的時候都會呼叫到一個 selectList 方法,這個方法會從 SqlSession 中的 Configuration 配置物件中獲取使用者想要的 MappedStatement(使用者自己指定 MappedStatementId ),然後將這個獲取到的 MappedStatement 物件傳入 SqlSession 中的欄位 Executor 執行器的查詢方法中獲取結果集幷包裝結果集返回列表。在 Executor 初始化這條支線,使用了裝飾者模式,構建的是 CachingExecutor 並且裡面還封裝了指定的 Executor

帶你一步一步手撕 Mybatis 原始碼加手繪流程圖——執行部分

Mybatis 是如何一步一步執行查詢方法的?

獲取結果集

我們上面知道了在 Executor 中使用到了裝飾者模式,也就是說我們上面的主線會先執行 CachingExecutor 中的 query 方法,這裡面其實還會呼叫到裡面它包裝的真正的 Executor,也就是說外面封裝的一層 CachingExecutor 只是為了做一些快取操作的,而真正執行的還是原本的執行器

我們來看一下在 CachingExecutor 進行的操作

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
    //query時傳入一個cachekey引數
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    Cache cache = ms.getCache();
    //預設情況下是沒有開啟快取的(二級快取).要開啟二級快取,你需要在你的 SQL 對映檔案中新增一行: <cache/>
    //簡單的說,就是先查CacheKey,查不到再委託給實際的執行器去查
    if (cache != null) {
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, parameterObject, boundSql);
        @SuppressWarnings("unchecked")
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    // 最終還是呼叫的裡面封裝的委託物件的查詢方法
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
複製程式碼

原始碼也驗證了我上面的說法,CachingExecutor 只是做一些快取處理的。我們繼續看看真正的執行方法。一般來說我們呼叫的是 SimpleExecutor(你可以去看剛剛的 setting 預設配置就是 SimpleExecutor),其中 SimpleExecutor 等執行器都繼承了 BaseExecutor ,所以呼叫的是 BaseExecutor 中的 query 方法。為了方便你理解我再次把 UML 圖放在這裡。

帶你一步一步手撕 Mybatis 原始碼加手繪流程圖——執行部分

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    //如果已經關閉,報錯
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    //先清區域性快取,再查詢.但僅查詢堆疊為0,才清。為了處理遞迴呼叫
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      //加一,這樣遞迴呼叫到上面的時候就不會再清區域性快取了
      queryStack++;
      //先根據cachekey從localCache去查
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        //若查到localCache快取,處理localOutputParameterCache
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        //從資料庫查 主線在這裡
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      //清空堆疊
      queryStack--;
    }
    if (queryStack == 0) {
      //延遲載入佇列中所有元素
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      //清空延遲載入佇列
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
    	//如果是STATEMENT,清本地快取
        clearLocalCache();
      }
    }
    return list;
}

//從資料庫查
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    //先向快取中放入佔位符???
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      // 真正的執行方法在這!!!
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      //最後刪除佔位符
      localCache.removeObject(key);
    }
    //加入快取
    localCache.putObject(key, list);
    //如果是儲存過程,OUT引數也加入快取
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
}

複製程式碼

這兩個方法還是做了一些基礎的處理,最終真正幹活的還是在 doQuery 方法中。而這個方法是需要子類去實現的,我們這次就得深入到真正的子類 SimpleExecutor 中去了。

@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
      // 這裡看到ResultHandler傳入了
      // 建立的其實是 RoutingStatementHandler 裡面也用到了 裝飾者模式
      // 你可以自己去看這個原始碼
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      // 準備語句 這個裡面其實就是 原生 JDBC 程式碼 你建立 Statement 的流程
      stmt = prepareStatement(handler, ms.getStatementLog());
      // StatementHandler.query
      // 真正的主線在這裡
      return handler.<E>query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
}
複製程式碼

這裡的主線是呼叫到了 StatementHandlerquery 方法,因為 StatementHandler 是一個介面,所以真正呼叫到的是生成的 RoutingStatementHandlerquery 方法。

@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    // 這裡還是裝飾者模式
    return delegate.<E>query(statement, resultHandler);
}
複製程式碼

你會發現這裡面又用到了 裝飾者模式(但是這裡沒做什麼處理,而是直接呼叫了),最終呼叫的其實還是 PreparedStatementHandler 中的 query 方法。

@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    // 強制轉換
    PreparedStatement ps = (PreparedStatement) statement;
    // 這裡就是 JDBC 裡面的執行方法 
    ps.execute();
    // 這裡已經能獲取結果集 ResultSet 了 但是需要做 結果集的型別處理
    // 最終返回的是列表
    return resultSetHandler.<E> handleResultSets(ps);
}
複製程式碼

接下來呼叫的是 DefaultResultSetHandler 中的處理執行結果的方法。

@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {
	ErrorContext.instance().activity("handling results").object(mappedStatement.getId());

	// <select>標籤的resultMap屬性,可以指定多個值,多個值之間用逗號(,)分割
	// 這是上篇文章中我們提到過得
	final List<Object> multipleResults = new ArrayList<>();

	int resultSetCount = 0;
	// 這裡是獲取第一個結果集,將傳統JDBC的ResultSet包裝成一個包含結果列元資訊的ResultSetWrapper物件
	// 這裡獲取了結果集並且包裝成相應的包裝類
	ResultSetWrapper rsw = getFirstResultSet(stmt);

	// 這裡是獲取所有要對映的ResultMap(按照逗號分割出來的)
	List<ResultMap> resultMaps = mappedStatement.getResultMaps();
	// 要對映的ResultMap的數量
	int resultMapCount = resultMaps.size();
	validateResultMapsCount(rsw, resultMapCount);
	// 迴圈處理每個ResultMap,一般只有一個
	while (rsw != null && resultMapCount > resultSetCount) {
		// 得到結果對映資訊
		ResultMap resultMap = resultMaps.get(resultSetCount);
		// 處理結果集
		// 從rsw結果集引數中獲取查詢結果,再根據resultMap對映資訊,將查詢結果對映到multipleResults中
		// 這裡是主線 處理結果集的 查詢結果會變成列表存入 multipleResults 中去
		handleResultSet(rsw, resultMap, multipleResults, null);

		rsw = getNextResultSet(stmt);
		cleanUpAfterHandlingResultSet();
		resultSetCount++;
	}

	// 對應<select>標籤的resultSets屬性,一般不使用 不用管
	String[] resultSets = mappedStatement.getResultSets();
	if (resultSets != null) {
		while (rsw != null && resultSetCount < resultSets.length) {
			ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
			if (parentMapping != null) {
				String nestedResultMapId = parentMapping.getNestedResultMapId();
				ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
				handleResultSet(rsw, resultMap, null, parentMapping);
			}
			rsw = getNextResultSet(stmt);
			cleanUpAfterHandlingResultSet();
			resultSetCount++;
		}
	}

	// 如果只有一個結果集合,則直接從多結果集中取出第一個
	return collapseSingleResultList(multipleResults);
}
複製程式碼

現在我們就可以畫出一個簡單的獲取結果集並返回的流程圖了。

帶你一步一步手撕 Mybatis 原始碼加手繪流程圖——執行部分

處理結果集

在上文中我們得知在 getFirstResultSet 方法中通過 JDBC 原生程式碼獲取了 ResultSet(結果集) 並且封裝成包裝類 ResultSetWrapper,因為我們返回給 dao 層的肯定不能是原生的 ResultSet ,所以我們需要進一步處理結果集。

接下來還是在 DefaultResultSetHandler 中,只不過我們的主要任務變成了如何去處理結果集?

//處理結果集
private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
    try {
      if (parentMapping != null) {
        handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
      } else {
        if (resultHandler == null) {
          //如果沒有resultHandler
          //新建DefaultResultHandler
          DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
          //呼叫自己的handleRowValues
          handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
          //得到記錄的list
          multipleResults.add(defaultResultHandler.getResultList());
        } else {
          //如果有resultHandler
          handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
        }
      }
    } finally {
      //最後別忘了關閉結果集
      // issue #228 (close resultsets)
      closeResultSet(rsw.getResultSet());
    }
}
複製程式碼

上面的程式碼邏輯總結來說就是:判斷 resultHandler 是否為空,如果為空則生成一個預設的 DefaultResultHandler 不然則使用原本的,最終都會去呼叫 handleRowValues 方法進行進一步的處理

當然我們最終應該去處理 multipleResults 這個列表變數,而在 Mybatis 中是在 ResultHandler 中持有了一個列表欄位,最終是將資料賦值到 ResultHandler 中的列表欄位裡面,然後將這個列表欄位加入 multipleResults 中去的。

從上面分析可知,最終都是呼叫的 handleRowValues 所以進行結果集的資料處理的主線還在這裡。

private void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
    //是否有內嵌 不用管先
    if (resultMap.hasNestedResultMaps()) {
      ensureNoRowBounds();
      checkResultHandler();
      handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    } else {
      // 繼續處理
      handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    }
}  
複製程式碼

因為不涉及到其他複雜的步驟我們這裡是直接分析一般流程,這裡會呼叫 handleRowValuesForSimpleResultMap 方法繼續進行資料處理。

private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap,
		ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
	DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
	// 獲取結果集資訊
	ResultSet resultSet = rsw.getResultSet();
	// 使用rowBounds的分頁資訊,進行邏輯分頁(也就是在記憶體中分頁)
	// 分頁先不用管
	skipRows(resultSet, rowBounds);
	while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
		// 通過<resultMap>標籤的子標籤<discriminator>對結果對映進行鑑別
		ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
		// 將查詢結果封裝到POJO中
		// rsw 是結果集的包裝類,裡面封裝了返回結果的資訊
		// 通過結果集包裝類建立 POJO
		Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
		// 處理物件巢狀的對映關係
		storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
	}
}
複製程式碼

上面的方法做的事情很簡單,就是通過結果集包裝類和一開始定義的類欄位(比如這個 POJO 是 Admin類其中有 account 和 password 等欄位) 封裝成一個 Object 實體類。然後將這個實體類存入某個區域裡。

我們先不管 getRowValue 方法是如何去處理的,我們先思考一下,此時獲取到的 Object 物件是如何載入到 multipleResults 中的,或者說是如何加入到 resultHandler 中的列表欄位裡的(因為這裡最終也會加入 multipleResults )

想必你也看出來了,答案肯定在 storeObject 中(因為其中 resultHandler 作為了引數傳入了進去),這裡我就做簡單分析

private void storeObject(ResultHandler resultHandler, DefaultResultContext resultContext, Object rowValue, ResultMapping parentMapping, ResultSet rs) throws SQLException {
    if (parentMapping != null) {
      linkToParents(rs, parentMapping, rowValue);
    } else {
      // 一般處理這裡
      callResultHandler(resultHandler, resultContext, rowValue);
    }
}
  
private void callResultHandler(ResultHandler resultHandler, DefaultResultContext resultContext, Object rowValue) {
    resultContext.nextResultObject(rowValue);
    // resultContext 封裝了一些上下文資訊 這裡包括封裝好的 Object
    // 這裡呼叫的是 resultHandler 物件呀!!!
    resultHandler.handleResult(resultContext);
}

// 這裡是在 DefaultResultHandler 中進行的 所以
// 是給裡面的列表欄位進行新增 
@Override
public void handleResult(ResultContext context) {
    // 處理很簡單,就是把記錄加入List
    list.add(context.getResultObject());
}
複製程式碼

好了分析完之後我們就可以大膽地分析如何包裝結果 POJO 物件的了,我們來進入 getRowValue 方法中。

//核心,取得一行的值
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap) throws SQLException {
    //例項化ResultLoaderMap(延遲載入器)
    final ResultLoaderMap lazyLoader = new ResultLoaderMap();
    //呼叫自己的createResultObject,內部就是new一個物件(如果是簡單型別,new完也把值賦進去)
    Object resultObject = createResultObject(rsw, resultMap, lazyLoader, null);
    if (resultObject != null && !typeHandlerRegistry.hasTypeHandler(resultMap.getType())) {
      //一般不是簡單型別不會有typehandler,這個if會進來
      // 重新有做了封裝
      // 這步之後 metaObject 就持有了 resultObject
      // 這個得記住
      final MetaObject metaObject = configuration.newMetaObject(resultObject);
      boolean foundValues = !resultMap.getConstructorResultMappings().isEmpty();
      if (shouldApplyAutomaticMappings(resultMap, false)) {        
        // 自動對映咯 這裡離是賦值操作
        // 也就是說此時我們的 resultHandler 其中的值都是 null 或預設值
        // 這裡把每個列的值都賦到相應的欄位裡去了
    	foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, null) || foundValues;
      }
      // 執行完這個方法 resultObject 中的欄位才有值
      // 你會發現這裡傳入的其實是 metaObject
      foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, null) || foundValues;
      foundValues = lazyLoader.size() > 0 || foundValues;
      resultObject = foundValues ? resultObject : null;
      return resultObject;
    }
    return resultObject;
}
複製程式碼

上面我們進行了一個操作就是:使用 resultObject 和其他一些東西構造了 metaObject 例項,如果翻看原始碼你會發現這個 metaObject 裡面是持有了 resultObject ,這裡因為篇幅有限,我不做過多解釋,感興趣可以自己去檢視原始碼。整個方法就是通過原先定義好的配置資訊建立一個 POJO 物件的空殼,然後通過結果集中的資料給空殼賦值。

我們來具體看一下 Mybatis 是如何給空殼賦值的。

private boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, ResultLoaderMap lazyLoader, String columnPrefix)
      throws SQLException {
    // 獲取欄位名稱列表
    final List<String> mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix);
    boolean foundValues = false;
    final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
    // 迴圈賦值
    for (ResultMapping propertyMapping : propertyMappings) {
      final String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
      if (propertyMapping.isCompositeResult() 
          || (column != null && mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH))) 
          || propertyMapping.getResultSet() != null) {
        // 重要 從結果集包裝類裡面獲取每個欄位的值
        // 這裡面還涉及到一個很重要的類 TypeHandler
        Object value = getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader, columnPrefix);
        // issue #541 make property optional
        final String property = propertyMapping.getProperty();
        // issue #377, call setter on nulls
        if (value != NO_VALUE && property != null && (value != null || configuration.isCallSettersOnNulls())) {
          if (value != null || !metaObject.getSetterType(property).isPrimitive()) {
            // 在這裡賦值了,其實這裡面是給 metaObject 持有的
            // resultObject中的欄位賦值
            metaObject.setValue(property, value);
          }
          foundValues = true;
        }
      }
    }
    return foundValues;
}
複製程式碼

上面整個流程就是先獲取 POJO類 中欄位的一些名稱,然後通過結果集包裝類 ResultSetWarpper 獲取結果集中對應欄位的值並且賦值給 metaObject 中持有的 resultObject “空殼”物件中的相應欄位,這麼一來 resultObject 就不是空殼了。在上面我標註了一個比較重要的地方就是這段程式碼。

Object value = getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader, columnPrefix);
複製程式碼

你有沒有想過,這其中是如何將欄位原本的值包裝成一個 Object 物件的?

我們進來找一下答案。

private Object getPropertyMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix)
      throws SQLException {
    if (propertyMapping.getNestedQueryId() != null) {
      return getNestedQueryMappingValue(rs, metaResultObject, propertyMapping, lazyLoader, columnPrefix);
    } else if (propertyMapping.getResultSet() != null) {
      addPendingChildRelation(rs, metaResultObject, propertyMapping);
      return NO_VALUE;
    } else if (propertyMapping.getNestedResultMapId() != null) {
      // the user added a column attribute to a nested result map, ignore it
      return NO_VALUE;
    } else {
      // 前面的一些可以不用看 這裡我們涉及到了一個 TypeHandler 
      // 其中獲取到了 ResultMapping 原本的 TypeHandler
      // 然後通過 TypeHandler 這個型別處理器去處理值
      final TypeHandler<?> typeHandler = propertyMapping.getTypeHandler();
      final String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
      return typeHandler.getResult(rs, column);
    }
}
複製程式碼

也就是說最終一些物件的型別轉換是通過 TypeHandler 來實現的,這是 Mybatis 中做型別轉換的主要工具類,在上一篇文章中我也提到過。

這樣,整個 Mybatis 的基本的執行流程我們就分析完畢了,我在網上找到一張挺好的圖,你可以參考著我上面的分析去理解(不過我還是建議你 debug 去看一下原始碼),圖有點不清晰,目前沒找到清晰版本的。

帶你一步一步手撕 Mybatis 原始碼加手繪流程圖——執行部分

總結

總結來說呢,整個 Mybatis 的查詢流程就是 封裝了 JDBC 原生程式碼,並做了 JDBC 沒有為我們做的 結果集的型別轉換

相關文章