淺析MyBatis(一):由一個快速案例剖析MyBatis的整體架構與運行流程

MyBatis 是輕量級的 Java 持久層中間件,完全基於 JDBC 實現持久化的數據訪問,支援以 xml 和註解的形式進行配置,能靈活、簡單地進行 SQL 映射,也提供了比 JDBC 更豐富的結果集,應用程式可以從中選擇對自己的數據更友好的結果集。本文將從一個簡單的快速案例出發,為讀者剖析 MyBatis 的整體架構與運行流程。本次分析中涉及到的程式碼和資料庫表可以從 GitHub 上下載:mybatis-demo

1.一個簡單的 MyBatis 快速案例

MyBatis官網 給出了一個 MyBatis 快速入門案例,簡單概括下來就是如下步驟:

  • 創建 Maven 項目並在 pom.xml 文件中引入 MyBatis 的依賴;
  • 準備資料庫連接配置文件(database.properties)及 MyBatis 配置文件(mybatis-config.xml);
  • 準備資料庫表單對應的實體類(Entity)以及持久層介面(Mapper/Dao);
  • 編寫持久層介面的映射文件(Mapper/Dao.xml);
  • 編寫測試類。

創建學生表用於測試:

CREATE TABLE `student` (
  `id` int(10) NOT NULL AUTO_INCREMENT COMMENT '學生ID',
  `name` varchar(20) DEFAULT NULL COMMENT '姓名',
  `sex` varchar(20) DEFAULT NULL COMMENT '性別',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

該表單對應的實體類以及包括增刪改查方法的持久層介面可在 entity 包和 mapper 包查看,資料庫連接和 MyBatis 的配置文件以及持久層介面的映射文件可以在 resource 包下查看。

測試類如下:

public class StudentTest {
  
  private InputStream in;
  private SqlSession sqlSession;

  @Before
  public void init() throws IOException {
    // 讀取MyBatis的配置文件
    in = Resources.getResourceAsStream("mybatis-config.xml");
    // 創建SqlSessionFactory的構建者對象
    SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
    // 使用builder創建SqlSessionFactory對象
    SqlSessionFactory factory = builder.build(in);
    // 使用factory創建sqlSession對象並設置自動提交事務
    sqlSession = factory.openSession(true);
  }

  @Test
  public void test() {
    // 使用sqlSession創建StudentMapper介面的代理對象
    StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
    // 使用代理對象執行相關方法
    System.out.println(studentMapper.getStudentById(2));
    studentMapper.updateStudentName("托尼·李四", 2);
    System.out.println(studentMapper.getStudentById(2));
    System.out.println(studentMapper.findAll());
  }

  @After
  public void close() throws IOException {
    // 關閉資源
    sqlSession.close();
    in.close();
  }
}

測試類運行結果如下:

可以看到測試類成功執行了相應方法,這樣就完成了 MyBatis 的快速案例實現。要注意的是,在上面的案例中我們採用的是為持久層介面編寫相應 xml 映射文件的方法,其部分配置如下所示:

<select id="getStudentById" parameterType="int" resultType="com.chiaki.entity.Student">
  SELECT id,name,sex FROM student WHERE id = #{id}
</select>

此外,在 MyBatis 中還提供了基於 Java 註解的方式,即在持久層介面的方法前使用對應的註解,如下所示:

@Select("SELECT id,name,sex FROM student WHERE id = #{id}")
Student getStudentById(int id);

兩種方法各有優劣。基於註解的方法減少了配置文件,使程式碼更加簡潔,但是在面對複雜 SQL 時候會顯得力不從心;基於配置文件的方法雖然需要編寫配置文件,但其處理複雜 SQL 語句的能力更強,實現了 Java 程式碼與 SQL 語句的分離,更容易維護。在筆者看來, Mapper.xml 配置文件就像是 MyBatis 的靈魂,少了它就沒那味兒了,😄😄😄。不過到底採用哪種方式來配置映射,讀者可以根據實際業務來靈活選擇。

當然上述關於 MyBatis 的使用方式都離不開通過程式碼手動注入配置,包括創建 SqlSessionFactory、SqlSession等對象的步驟。此外,也可以採用將 MyBatis 與 Spring 等容器集成的方式來進行使用,這也是目前非常受歡迎的方式,由於本文主要是介紹 MyBatis 的偏底層的原理,因此這裡不做詳細介紹。

2. MyBatis 的整體架構

在上一小節中我們進行了 MyBatis 的快速實現,也看到了 Resources 、 SqlSessionFactory 以及 SqlSession 等 MyBatis 框架中的一些類,那麼 MyBatis 的系統架構到底是什麼樣的呢?我們通過結合 MyBatis 的源碼項目結構得到下面的 MyBatis 整體框架圖:

可以看出,在 MyBatis 源碼中基本上是每一個 package 對應一個模組,模組之間互相配合確保 MyBatis 的成功運行。下面分層介紹 MyBatis 的整體架構。

2.1 基礎支援層

模組名稱 關聯package 作用
數據源模組 datasource 數據源及數據工廠的程式碼。
事務管理模組 transaction 事務支援程式碼。
快取模組 cache 快取實現程式碼。 MyBatis 提供以及快取與二級快取。
Binding模組 binding 映射綁定。將用戶自定義的 Mapper 介面與映射配置文件關聯。
反射模組 reflection 反射是框架的靈魂。 MyBatis 對原生的反射進行了良好的封裝,實現了更簡潔的調用。
類型轉換 type 類型處理。包含了類型處理器介面 TypeHandler 、父類 BaseTypeHandler 以及若干子類。
日誌模組 logging 提供日誌輸出資訊,並且能夠集成 log4j 等第三方日誌框架。
資源載入 io 對類載入器進行封裝,確定類載入器的使用順序,並提供載入資源文件的功能。
解析器模組 parsing 一是對 XPath 進行封裝,二是為處理動態 SQL 中的佔位符提供支援。

2.2 核心處理層

模組名稱 關聯package 作用
配置解析 builder 解析 Mybatis 的配置文件和映射文件,包括 xml 和 annotation 兩種形式的配置。
參數映射 mapping 主要是 ParameterMap ,支援對輸入參數的判斷、組裝等。
SQL解析 scripting 根據傳入參數解析映射文件中定義的動態 SQL 節點,處理佔位符並綁定傳入參數形成可執行的 SQL 語句。
SQL執行 executor 在 SQL 解析完成之後執行SQL語句德奧結果並返回。
結果集映射 mapping 主要是 ResultMap ,與 ParameterMap 類似。
插件 plugin 可以通過添加自定義插件的方式對 MyBatis 進行擴展。

2.3 介面層

介面層對應的 package 主要是 session ,其中的核心是 SqlSession 介面,該介面定義了 MyBatis 暴露給用戶調用的一些 API ,包括了 Select() 、 update() 、 insert() 、 delete() 等方法。當介面層收到調用請求時就會調用核心處理層的模組來完成具體的資料庫操作。

3. MyBatis的運行流程

3.1 MyBatis 運行流程結構

本節中首先結合快速入門案例與 MyBatis 的整體架構來梳理其運行流程結構,如下圖所示:

可以說, MyBatis 的整個運行流程結構,緊緊圍繞著配置文件 MyBatis-config.xml 與 SQL 映射文件 Mapper.xml 文件展開。首先 SqlSessionFactory 會話工廠會通過 io 包下的 Resources 資源資訊載入對象獲取 MyBatis-config.xml 配置文件資訊,然後產生可以與資料庫進行交互的 SqlSession 會話實例類。會話實例 SqlSession 可以根據 Mapper.xml 配置文件中的 SQL 配置,去執行相應的增刪改查操作。而在 SqlSession 類中,是通過執行器 Executor 對資料庫進行操作。執行器與資料庫交互依靠的是底層封裝對象 Mapped Statement ,其封裝了從 Mapper 文件中讀取的包括 SQL 語句、輸入參數類型、輸出結果類型的資訊。通過執行器 Executor 與 Mapped Statement 的結合, MyBatis 就實現了與資料庫進行交互的功能。

3.2 一條 SQL 語句的執行過程分析

本小節以一條具體的 SQL 語句為例,來分析 MyBatis 的執行過程,測試方法如下所示,其對應的語句是根據主鍵 ID 查詢學生資訊,測試方法運行前後的執行動作參見第 1 小節中 @Before 與 @After 註解下的方法,此處省略。

@Test
public void testSqlExecute() {
  StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
  Student student = studentMapper.getStudentById(2);
  System.out.println(student);
}

3.2.1 配置文件轉換成輸入流

首先,通過 io 包下的 Resources 類載入配置文件,將 Mapper .xml 文件轉換為輸入流,具體源碼可以參考 org.apache.ibatis.session.io.Resources 類,如下所示。同時在 Resources 類中, MyBatis 還提供了其它的一些文件讀取方法,方便用戶使用。

public static InputStream getResourceAsStream(ClassLoader loader, String resource)throws IOException {
  InputStream in = classLoaderWrapper.getResourceAsStream(resource, loader);
  if (in == null) {
    throw new IOException("Could not find resource " + resource);
  }
  return in;
}

3.2.2 創建會話工廠 SqlSessionFactory

在得到配置文件的輸入流之後, MyBatis 會調用 org.apache.ibatis.session.SqlSessionFactory 類中的 build() 方法創建 SqlSessionFactory 會話工廠。通過查看源碼可以發現在 SqlSessionFactory 類中重載了很多 build() 方法,這裡主要介紹下面三個方法:

// 方法一
public SqlSessionFactory build(InputStream inputStream) {
  return build(inputStream, null, null);
}

// 方法二
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
  try {
    // 創建XMLConfigBuilder類型的對象用於解析配置文件
    XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
    // 調用parse()方法生成Configuration對象並調用build()方法返回SqlSessionFactory對象
    return build(parser.parse());
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error building SqlSession.", e);
  } finally {
    ErrorContext.instance().reset();
    try {
      inputStream.close();
    } catch (IOException e) {
      // Intentionally ignore. Prefer previous error.
    }
  }
}

// 方法三
public SqlSessionFactory build(Configuration config) {
  return new DefaultSqlSessionFactory(config);
}

從案例中我們看到在創建會話工廠時調用方法一,即 build(InputStream inputStream) 方法,在該方法中其實調用了方法二,只不過將 environment 和 propertoes 參數置為 null 。我們重點看方法二,該方法中涉及到 org.apache.ibatis.builder.xml 包的 XMLConfigBuilder 類,該類繼承自 BaseBuilder 類,並初始化了一個用於解析配置文件的對象 parser , 然後在 return 語句中調用的是方法三,看到這裡我們肯定發現方法三中 build() 方法的參數 parser.parse() 肯定是 Configuration 類型。在創建會話工廠的步驟中, Configuration 的解析過程是一個關鍵的流程,下面我們會逆向探究 Configuration 的詳細解析過程。

3.2.2.1 XMLConfigBuilder#parse()

先看看這個 XMLConfigBuilder 類型的 parser 對象下的 parse() 方法,探究這個方法是如何生成 Configuration 類型的對象的。 parse() 方法定義在 org.apache.ibatis.session.builder.XMLConfigBuilder 類中,該方法的源程式碼以及相應注釋如下所示,可以看出真正重要的是 parseConfiguration() 方法。

public Configuration parse() {
  if (parsed) {
    throw new BuilderException("Each XMLConfigBuilder can only be used once.");
  }
  parsed = true;
  // 先調用parser的evalNode()方法獲取 "/configuration"下的節點
  // 然後調用parseConfiguration()方法解析節點的資訊並返回Configuration對象
  parseConfiguration(parser.evalNode("/configuration"));
  return configuration;
}
3.2.2.2 XMLConfigBuilder#parseConfiguration()

直接查看 XMLConfigBuilder#parseConfiguration() 方法的源碼如下所示:

// 解析配置文件的各個節點並將其設置到configuration對象
private void parseConfiguration(XNode root) {
  try {
    // 1.處理properties節點
    propertiesElement(root.evalNode("properties"));
    // 2.處理settings節點
    Properties settings = settingsAsProperties(root.evalNode("settings"));
    // 3.載入自定義的VFS設置
    loadCustomVfs(settings);
    // 4.載入自定義的日誌實現設置
    loadCustomLogImpl(settings);
    // 5.處理typeAliases節點
    typeAliasesElement(root.evalNode("typeAliases"));
    // 6.處理plugins節點
    pluginElement(root.evalNode("plugins"));
    // 7.處理objectFactory節點
    objectFactoryElement(root.evalNode("objectFactory"));
    // 8.處理objectWrapperFactory節點
    objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
    // 9.處理reflectorFactory節點
    reflectorFactoryElement(root.evalNode("reflectorFactory"));
    // 10.處理settings節點
    settingsElement(settings);
    // 11.處理environments節點
    environmentsElement(root.evalNode("environments"));
    // 12.處理databaseIdProvider節點
    databaseIdProviderElement(root.evalNode("databaseIdProvider"));
    // 13.處理typeHandlers節點
    typeHandlerElement(root.evalNode("typeHandlers"));
    // 14.處理mappers節點
    mapperElement(root.evalNode("mappers"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  }
}

要注意的是 parseConfiguration() 方法在處理配置文件的節點後會把相應配置寫入到該類的成員變數 configuration 中然後返回。我們以處理 mappers 節點的 mapperElement() 方法為例來進行說明,對其它主配置文件節點的解析方法讀者可以自行參照源碼閱讀和理解。

3.2.2.3 XMLConfigBuilder#mapperElement()

在 mappers 節點下主要是 mapper 的配置方式,是 MyBatis 中重要的一部分。首先要明確在 MyBatis 配置文件的 mappers 節點下配置 mapper 的四種方式:

<mappers>
  <!-- 1.使用相對於類路徑的資源引用 -->
  <mapper resource="mapper/StudentMapper.xml"/>
  <!-- 2.使用完全限定資源定位符(URL) -->
  <mapper url="file:src/main/resources/mapper/StudentMapper.xml"/>
  <!-- 3.使用映射器介面實現類的完全限定類名-->
  <mapper class="com.chiaki.mapper.StudentMapper"/>
  <!-- 4.使用包內的映射器介面實現全部註冊為映射器 -->
  <package name="com.chiaki.mapper"/>
</mappers>

下面我們通過 MyBatis 的源碼來看看 mappers 節點是如何被解析的,在 XMLConfigBuilder 類中找到 mapperElement() 方法,如下所示:

private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
    // 遍歷子節點
    for (XNode child : parent.getChildren()) {
      // 子節點是package,也就是上面配置mapper的第四種方式
      if ("package".equals(child.getName())) {
        // 獲取package的路徑
        String mapperPackage = child.getStringAttribute("name");
        // 向Configuration的類成員遍歷MapperRegistry添加mapper介面
        // addMappers()方法位於Configuration類中
        configuration.addMappers(mapperPackage);
      } else {
        // 永遠先執行else語句,因為dtd文件聲明mappers節點下mapper子節點必須在package子節點前面
        // 獲取mapper節點中的resource、url以及class屬性
        String resource = child.getStringAttribute("resource");
        String url = child.getStringAttribute("url");
        String mapperClass = child.getStringAttribute("class");
        // 只有resource屬性,也就是上面配置mapper的第一種方式
        if (resource != null && url == null && mapperClass == null) {
          ErrorContext.instance().resource(resource);
          try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
            // 生成XMLMapperBuilder類型的mapperParser對象,即mapper解析器
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            // 調用解析器的parse方法進行解析
            mapperParser.parse();
          }
        } else if (resource == null && url != null && mapperClass == null) {
          ErrorContext.instance().resource(url);
          try(InputStream inputStream = Resources.getUrlAsStream(url)){
            // 只有url屬性,也就是上面配置mapper的第二種方式
            // 仍然是生成XMLMapperBuilder類型的mapper解析器
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            // 調用parse()方法
            mapperParser.parse();
          }
        } else if (resource == null && url == null && mapperClass != null) {
          // 只有class屬性,也就是上面配置的第三種方式
          // 通過反射獲取mapper介面類
          Class<?> mapperInterface = Resources.classForName(mapperClass);
          // 調用addMapper()方法
          configuration.addMapper(mapperInterface);
        } else {
          throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
        }
      }
    }
  }
}

這段程式碼中對應著 mappers 節點配置的四種情況:

  • 節點名為 mapper 時分三種情況:
    • resource 不為空時從 classpath 載入 xml 文件(方式一);
    • url 不為空時從 URL 載入 xml 文件(方式二);
    • mapperClass不為空時掃描 mapper 介面和介面上的註解,調用 addMapper() 方法(方式三)。
  • 節點名為 package ,掃描該包下所有 mapper 介面和註解,調用 addMappers() 方法(方法四)。

方式一和方式二指定了 Mapper 介面與 xml 配置文件,方式三和方式四指定了 Mapper 介面。

Ⅰ. 指定 xml 文件時的 Mapper 解析與載入
1)XMLMapperBuilder#parse()

方式一和方式二都涉及到構造 XMLMapperBuilder ,該類位於 org.apache.ibatis.builder.xml 包下,同樣繼承自 BaseBuilder 類。同時以上兩種方式都涉及到 XMLMapperBuilder類下的一個 parse() 方法,要注意與 XMLConfigBuilder 類中的 parse() 方法進行對比區分理解。顯然, XMLConfigBuilder 負責解析 MyBatis 的配置文件,而 XMLMapperBuilder 負責解析 Mapper.xml 文件。找到 XMLMapperBuilder#parse() 方法,其源碼如下:

public void parse() {
  // Configuration類中定義了Set<String> loadedResources表示已載入的mapper.xml文件
  // 判斷是否已載入過該mapper.xml文件
  if (!configuration.isResourceLoaded(resource)) {
    // 解析文件中的各種配置
    configurationElement(parser.evalNode("/mapper"));
    // 解析完畢後將該文件添加到loadedResources中
    configuration.addLoadedResource(resource);
    // 為介面的全限定類名綁定相應的Mapper代理
    bindMapperForNamespace();
  }
  // 移除Configuration中解析失敗的resultMap節點
  parsePendingResultMaps();
  // 移除Configuration中解析失敗的cache-ref節點
  parsePendingCacheRefs();
  // 移除Configuration中解析失敗的statement
  parsePendingStatements();
}
2)XMLMapperBuilder#configurationElement()

在 parse() 方法中涉及到的 configurationElement() 方法是一個核心方法,其源碼如下:

private void configurationElement(XNode context) {
  try {
    // 獲取全限定類名
    String namespace = context.getStringAttribute("namespace");
    if (namespace == null || namespace.isEmpty()) {
      throw new BuilderException("Mapper's namespace cannot be empty");
    }
    // 設置全限定類名
    builderAssistant.setCurrentNamespace(namespace);
    // 解析cache-ref節點
    cacheRefElement(context.evalNode("cache-ref"));
    // 解析cache節點
    cacheElement(context.evalNode("cache"));
    // 解析parameterMap 
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    // 解析resultMap
    resultMapElements(context.evalNodes("/mapper/resultMap"));
    // 解析sql節點
    sqlElement(context.evalNodes("/mapper/sql"));
    // 解析select|insert|update|delete節點
    buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
  }
}

該方法解析了 mapper 節點中所有子標籤,最終通過 buildStatementFromContext() 方法解析具體 SQL 語句並生成 MappedStatement 對象。

3)XMLMapperBuilder#buildStatementFromContext()

進一步找到 XMLMapperBuilder#buildStatementFromContext() 方法,該方法進行了重載,功能是遍歷所有標籤,然後創建一個 XMLStatementBuilder 類型的對象對表示實際 SQL 語句的標籤進行解析,重點調用的是 parseStatementNode() 方法,源碼如下所示:

private void buildStatementFromContext(List<XNode> list) {
  if (configuration.getDatabaseId() != null) {
    buildStatementFromContext(list, configuration.getDatabaseId());
  }
  buildStatementFromContext(list, null);
}

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
  for (XNode context : list) {
    // 創建XMLStatementBuilder類型的statementParse用於對select|insert|update|delete節點進行解析
    final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
    try {
      // 調用parseStatementNode()方法解析
      statementParser.parseStatementNode();
    } catch (IncompleteElementException e) {
      configuration.addIncompleteStatement(statementParser);
    }
  }
}
4)XMLStatementBuilder#parseStatementNode()

找到 parseStatementNode() 方法,其位於 org.apache.ibatis.builder.xml.XMLStatementBuilder 類下,源碼如下:

public void parseStatementNode() {
  String id = context.getStringAttribute("id");
  String databaseId = context.getStringAttribute("databaseId");
  if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
    return;
  }
  // 解析標籤屬性
  String nodeName = context.getNode().getNodeName();
  SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
  boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
  boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
  boolean useCache = context.getBooleanAttribute("useCache", isSelect);
  boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
  // 將include標籤內容替換為sql標籤定義的sql片段
  XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
  includeParser.applyIncludes(context.getNode());
  // 獲取Mapper返回結果類型的Class對象
  String parameterType = context.getStringAttribute("parameterType");
  Class<?> parameterTypeClass = resolveClass(parameterType);
  // 獲取LanguageDriver對象
  String lang = context.getStringAttribute("lang");
  LanguageDriver langDriver = getLanguageDriver(lang);
  // 解析selectKey標籤
  processSelectKeyNodes(id, parameterTypeClass, langDriver);
  // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
  KeyGenerator keyGenerator;
  String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
  keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
  // 獲取主鍵生成策略
  if (configuration.hasKeyGenerator(keyStatementId)) {
    keyGenerator = configuration.getKeyGenerator(keyStatementId);
  } else {
    keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
  			configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
        ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
  }
  // 通過LanguageDriver對象解析SQL內容生成SqlSource對象
  SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
  // 默認Statement的類型為PreparedStatament
  StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
  // 解析並獲取select|update|delete|insert標籤屬性
  Integer fetchSize = context.getIntAttribute("fetchSize");
  Integer timeout = context.getIntAttribute("timeout");
  String parameterMap = context.getStringAttribute("parameterMap");
  String resultType = context.getStringAttribute("resultType");
  Class<?> resultTypeClass = resolveClass(resultType);
  String resultMap = context.getStringAttribute("resultMap");
  String resultSetType = context.getStringAttribute("resultSetType");
  ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
  if (resultSetTypeEnum == null) {
    resultSetTypeEnum = configuration.getDefaultResultSetType();
  }
  String keyProperty = context.getStringAttribute("keyProperty");
  String keyColumn = context.getStringAttribute("keyColumn");
  String resultSets = context.getStringAttribute("resultSets");
  // 調用addMappedStatement()將解析內容組裝生成MappedStatement對象並註冊到Configuration
  builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

在上面的源碼中會解析 select|update|delete|insert 標籤的屬性,然後重點是用 LanguageDriver 對象來解析 SQL 生成 SqlSource 對象。 org.apache.ibatis.scripting.LanguageDriver 類是一個介面,對應的實現類有 XMLLanguageDriver 和 RawLanguageDriver ,同時涉及到 XMLScriptBuilder 類與 SqlSourceBuilder 類等。關於 LanguageDriver 對象解析 SQL 的詳細過程,讀者可以循序漸進去閱讀 MyBatis 的源碼,這裡限於篇幅就不做詳細介紹了。最後會調用 org.apache.ibatis.builder.MapperBuilderAssistant#addMappedStatement() 方法將解析內容組裝成 MappedStatement 對象並將其註冊到 Configuration 的 mappedStatements 屬性中。至此, Mapper 介面對應的 xml 文件配置就解析完成了。下面我們再回到 XMLMapperBuilder#parse() 方法看看 Mapper 是如何註冊介面的。

5)XMLMapperBuilder#bindMapperForNameSpace()

在 XMLMapperBuilder#parse() 中通過 XMLMapperBuilder#configurationElement() 方法解析完 xml 文件配置後會將其添加到已載入的資源 loadedResources 中,然後會調用 XMLMapperBuilder#bindMapperForNameSpace() 方法為介面的全限定類名綁定 Mapper 代理,即為 Mapper 介面創建對應的代理類,找到相應源碼如下:

private void bindMapperForNamespace() {
  String namespace = builderAssistant.getCurrentNamespace();
  if (namespace != null) {
    Class<?> boundType = null;
    try {
      boundType = Resources.classForName(namespace);
    } catch (ClassNotFoundException e) {
      // ignore, bound type is not required
    }
    if (boundType != null && !configuration.hasMapper(boundType)) {
      // 調用Configuration#hasMapper()方法判斷當前Mapper介面是否已經註冊
      configuration.addLoadedResource("namespace:" + namespace);
      // 調用Configuration#addMapper()註冊介面
      configuration.addMapper(boundType);
    }
  }
}

在上面的程式碼中先調用 Configuration#hasMapper() 方法判斷當前 Mapper 介面是否已經註冊,只有沒被註冊過的介面會調用 Configuration#addMapper() 方法類註冊介面。

6)Configuration#addMapper()

在 Configuration 類中, 找到 addMapper() 方法發現其調用的是 MapperRegistry#addMapper() 方法。

// Configuration#addMapper()方法
public <T> void addMapper(Class<T> type) {
  mapperRegistry.addMapper(type);
}
7)MapperRegistry#addMapper()

找到 MapperRegistry#addMapper() 方法對應的源碼如下:

// Configuration#addMapper()方法
public <T> void addMapper(Class<T> type) {
  mapperRegistry.addMapper(type);
}

// MapperRegistry#addMapper()方法
public <T> void addMapper(Class<T> type) {
  if (type.isInterface()) {
    if (hasMapper(type)) {
      throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
    }
    boolean loadCompleted = false;
    try {
      // 調用Configuration#knownMappers屬性的put方法
      knownMappers.put(type, new MapperProxyFactory<>(type));
      // 創建MapperAnnotationBuilder對象parser
      MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
      // 調用MapperAnnotationBuilder#parse()方法
      parser.parse();
      loadCompleted = true;
    } finally {
      if (!loadCompleted) {
        knownMappers.remove(type);
      }
    }
  }
}

在 MapperRegistry#addMapper() 方法中,首先會調用 Configuration 類下 knownMappers 屬性的 put() 方法,可以看到 key 值為 Mapper 介面對應的 Class 對象,而 value 值為 Mapper 介面對應的 Class 類型的代理工廠類 MapperProxyFactory 。這裡 MapperProxyFactory 會根據 sqlSeesion 創建 Mapper 介面的一個 MapperProxy 代理實例,具體的分析我們將在後續小節解讀。

8)MapperAnnotationBuilder#parse()

在 Mapper 介面註冊之後,繼續往下可以看到創建了一個 MapperAnnotationBuilder 類型的對象 parser ,然後調用 MapperAnnotationBuilder#parse() 方法進行解析,我們找到 MapperAnnotationBuilder#parse() 的源碼如下:

public void parse() {
  String resource = type.toString();
  // 判斷是否被載入過
  if (!configuration.isResourceLoaded(resource)) {
    // 如果沒有被載入則對資源進行載入
    loadXmlResource();
    configuration.addLoadedResource(resource);
    assistant.setCurrentNamespace(type.getName());
    // 解析快取
    parseCache();
    parseCacheRef();
    for (Method method : type.getMethods()) {
      if (!canHaveStatement(method)) {
        continue;
      }
      // 解析Mapper介面的使用SQL註解的方法,比如@Select以及@SelectProvider
      if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
          && method.getAnnotation(ResultMap.class) == null) {
        parseResultMap(method);
      }
      try {
        // 調用parseStatement()方法
        parseStatement(method);
      } catch (IncompleteElementException e) {
        configuration.addIncompleteMethod(new MethodResolver(this, method));
      }
    }
  }
  parsePendingMethods();
}

通過閱讀源碼可以知道 MapperAnnotationBuilder#parse() 方法會對介面上的 SQL 註解進行解析,解析完成後生成對應的 MappedStatement 對象並註冊到 Configuration 的 mappedStatements 屬性中,這裡在後面展開詳細解析。

小結

至此我們已經梳理清楚了在指定 xml 文件時 Mapper 的解析與載入流程。回過頭看,我們從 XMLMapperBuilder#parse() 方法開始層層遞進,猶如抽絲剝繭一般,讓人覺得酣暢淋漓,也在這裡做一個小結。

  • 根據 xml 文件的輸入流創建 XMLMapperBuilder 對象後,調用 parse() 方法開始解析 xml 文件下 mapper 標籤的資訊。在閱讀源碼時,可以發現 XMLMapperBuilder#parse() 是一個十分重要的方法,相當於我們閱讀源碼的入口;
  • 在 parse() 方法中又調用 XMLMapperBuilder#configurationElement() 方法解析 cache-ref 、 cache 、 parameterMap 、 resultMap 、 sql 以及 select|insert|update|delete 等標籤的資訊;
  • 解析 select|insert|update|delete 標籤時的入口方法是 XMLMapperBuilder#buildStatementFromContext() 方法,在解析時會遍歷所有標籤並創建對應的 XMLStatementBuilder 對象;
  • 調用 XMLStatementBuilder#parseStatementNode() 方法解析 select|update|delete|insert 標籤的屬性,然後用 LanguageDriver 對象來解析 SQL 生成 SqlSource 對象,並調用 MapperBuilderAssistant#addMappedStatement() 方法將解析內容組裝成 MappedStatement 對象並將其註冊到 Configuration 的 mappedStatements 屬性中;
  • 回到 XMLMapperBuilder#parse() 方法,調用 Configuration#addLoadedResource() 方法將 xml 文件資源註冊到 Configuration 中;
  • 繼續調用 XMLMapperBuilder#bindMapperForNameSpace() 方法實現當前介面的註冊,方法中調用了 Configuration#addMapper() 方法,實際底層調用的是 MapperRegistry#addMapper() 方法。該方法中創建了 MapperProxyFactory 對象,負責在執行 Mapper 時根據當前 SqlSession 對象創建當前介面的 MapperProxy 代理實例;
  • 最後,在 Mapper 介面註冊後, MapperRegistry#addMapper() 方法中還創建了 MapperAnnotationBuilder 對象,並調用 MapperAnnotationBuilder#parse() 方法完成了對 Mapper 介面的 SQL 註解進行了解析並生成對應 MappedStatement 對象並將其註冊到 Configuration 的 mappedStatements 屬性中。
Ⅱ. 指定Mapper介面時Mapper的解析與載入

看完了方式一和方式二的解析與載入流程之後,我們繼續回到 XMLConfigBuilder#mapperElement() 方法探究方式三和方式四中指定 Mapper 介面時的 Mapper 解析與載入流程。方式三和方式四涉及到調用 configuration 對象的 addMappers() 和 addMapper() 方法。我們找到這兩個方法,發現其都位於 org.apache.ibatis.seesion 包的 Configuration 類中,其源碼如下:

public <T> void addMapper(Class<T> type) {
  mapperRegistry.addMapper(type);
}

public void addMappers(String packageName) {
  mapperRegistry.addMappers(packageName);
}

看到這是不是覺得十分熟悉?沒錯,其實 addMappers() 和 addMapper() 方法的底層都是調用 MapperRegistry#addMapper() 方法實現 Mapper 介面的註冊,這個方法我們已經在上文中詳細介紹過了。是不是感覺少了什麼流程?確實,讀者可能疑惑的是:在上文中提到的指定 xml 文件時的解析和載入流程中,會先有很多解析 xml 文件的步驟然後才到 MapperRegistry#addMapper() 方法實現 Mapper 介面的註冊,而在現在這種指定 Mapper 介面時的流程中一開始就調用 MapperRegistry#addMapper() 方法,那這種情況是不是就不解析 xml 了呢?說到這,就不得不提 MapperRegistry#addMapper() 方法中創建的 MapperAnnotationBuilder 對象了,上文中我們提到該對象用於解析 Mapper 介面的 SQL 註解並生成對應 MappedStatement 對象並將其註冊到 Configuration 的 mappedStatements 屬性中。其實方式三和方式四的重點就是對指定 Mapper 介面上的註解進行解析的,而我們知道 MyBatis 的基於註解的配置方式最大的優點就是沒有 xml 配置文件,連 xml 配置文件都沒有的話自然就沒有 xml 文件相關的解析流程啦!不過,如果指定了 xml 文件,仍會使用 XMLMapperBuilder 來解析 xml 文件。

1)MapperAnnotationBuilder#parse()

現在再看看 MapperAnnotationBuilder#parse() 的源碼,如下所示:

public void parse() {
  String resource = type.toString();
  // 判斷是否被載入過
  if (!configuration.isResourceLoaded(resource)) {
    // 如果沒有被載入則對資源進行載入
    loadXmlResource();
    configuration.addLoadedResource(resource);
    assistant.setCurrentNamespace(type.getName());
    // 解析快取
    parseCache();
    parseCacheRef();
    for (Method method : type.getMethods()) {
      if (!canHaveStatement(method)) {
        continue;
      }
      // 解析Mapper介面的使用SQL註解的方法,比如@Select以及@SelectProvider
      if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
          && method.getAnnotation(ResultMap.class) == null) {
        parseResultMap(method);
      }
      try {
        // 調用parseStatement()方法解析SQL註解
        parseStatement(method);
      } catch (IncompleteElementException e) {
        configuration.addIncompleteMethod(new MethodResolver(this, method));
      }
    }
  }
  parsePendingMethods();
}

閱讀源碼我們發現關鍵的方法,即 MapperAnnotationBuilder#parseStatement() 方法,該方法是解析 SQL 註解的入口方法。

2)MapperAnnotationBuilder#parseStatement()

在 org.apache.ibatis.builder.annotation 找到 MapperAnnotationBuilder#parseStatement() 方法的源碼,如下所示:

// 四個類成員變數
// 註解對應Class對象組成的set集合
// 包括@Select、@Insert、@Update、@Delete、@SelectProvider、@InsertProvider、@UpdateProvider、@DeleteProvider註解
private static final Set<Class<? extends Annotation>> statementAnnotationTypes = Stream
      .of(Select.class, Update.class, Insert.class, Delete.class, SelectProvider.class, UpdateProvider.class,
          InsertProvider.class, DeleteProvider.class)
      .collect(Collectors.toSet());
// 核心配置對象Configuration
private final Configuration configuration;
// Mapper構建工具
private final MapperBuilderAssistant assistant;
// 要解析的Mapper介面的Class對象
private final Class<?> type;

// parseStatement()方法,入參為Mapper中的方法
void parseStatement(Method method) {
  // 獲取輸入參數類型的Class對象
  final Class<?> parameterTypeClass = getParameterType(method);
  // 獲取LanguageDriver對象
  final LanguageDriver languageDriver = getLanguageDriver(method);

  // 流方法中的ifPresent()方法,包含lambda表達式
  getAnnotationWrapper(method, true, statementAnnotationTypes).ifPresent(statementAnnotation -> {
    // 獲取SqlSource
    final SqlSource sqlSource = buildSqlSource(statementAnnotation.getAnnotation(), parameterTypeClass, languageDriver, method);
    // 通過註解獲取SQL命令類型
    final SqlCommandType sqlCommandType = statementAnnotation.getSqlCommandType();
    // 獲取方法上的@Options註解
    final Options options = getAnnotationWrapper(method, false, Options.class).map(x -> (Options)x.getAnnotation()).orElse(null);
    // 映射語句id設置為類的全限定名.方法名
    final String mappedStatementId = type.getName() + "." + method.getName();

    // 鍵生成器
    final KeyGenerator keyGenerator;
    String keyProperty = null;
    String keyColumn = null;
    // 如果是insert或者update,只有insert或者update才解析@SelectKey註解
    if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
      // 首先檢查@SelectKey註解,它會覆蓋其它任何配置
      // 獲取方法上的@SelectKey註解
      SelectKey selectKey = getAnnotationWrapper(method, false, SelectKey.class).map(x -> (SelectKey)x.getAnnotation()).orElse(null);
      // 如果存在@SelectKey註解
      if (selectKey != null) {
        keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver);
        keyProperty = selectKey.keyProperty();
      } else if (options == null) {
        keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
      } else {
        keyGenerator = options.useGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
        keyProperty = options.keyProperty();
        keyColumn = options.keyColumn();
      }
    } else {
      // 其它SQL命令沒有鍵生成器
      keyGenerator = NoKeyGenerator.INSTANCE;
    }

    Integer fetchSize = null;
    Integer timeout = null;
    StatementType statementType = StatementType.PREPARED;
    ResultSetType resultSetType = configuration.getDefaultResultSetType();
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    boolean flushCache = !isSelect;
    boolean useCache = isSelect;
    if (options != null) {
      // 省略
    }

    String resultMapId = null;
    if (isSelect) {
      // 如果是查詢,獲取@ResultMap註解
      ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
      if (resultMapAnnotation != null) {
        // @ResultMap註解不為空則解析@ResultMap註解
        resultMapId = String.join(",", resultMapAnnotation.value());
      } else {
        resultMapId = generateResultMapName(method);
      }
    }
    // 調用addMappedStatement()將解析內容組裝生成MappedStatement對象並註冊到Configuration
    assistant.addMappedStatement(mappedStatementId, sqlSource, statementType, sqlCommandType, fetchSize, timeout, 
                                 null, parameterTypeClass, resultMapId, getReturnType(method), resultSetType, 
                                 flushCache, useCache, false, keyGenerator, keyProperty, keyColumn, 
                                 statementAnnotation.getDatabaseId(), languageDriver, 
                                 options != null ? nullOrEmpty(options.resultSets()) : null);});
}

通過閱讀這段源碼,我們發現 parseStatement() 方法中關於註解的解析過程與 XMLStatementBuilder#parseStatementNode() 方法中對 xml 文件的解析有些許相似之處。在對 xml 解析時是獲取對應標籤然後解析,而對註解解析時是獲取方法上的註解然後進行解析,解析完成後二者都是調用 MapperBuilderAssistant#addMappedStatement() 方法組裝解析內容生成 MappedStatement 對象並註冊到 Configuration 中。

小結

在指定 Mapper 介面的情況下,我們分析了 Mapper 的解析與載入流程。在這種情況下主要是從 MapperAnnotationBuilder#parse() 方法入手,調用 MapperAnnotationBuilder#parseStatement() 方法對 Mapper 介面上的註解進行解析,然後將解析內容組裝並生成 MappedStatement 對象並註冊到 Configuration 對象的 mappedStatements 屬性中。這裡要注意的是,指定 Mapper 介面這種方式一般沒有指定 xml 文件,這樣就只會對註解進行解析,當指定 xml 文件後仍會按上小節中的步驟對 xml 文件進行解析。同理,指定 xml 文件的方式一般也沒有註解,因此也只會解析 xml 文件,當存在註解時也同樣會對註解進行解析。

3.2.3 創建會話 SqlSession

在上一小節中,我們花了很大的篇幅如剝洋蔥一般一層一層地理清了創建會話工廠中 Configuration 對象的解析流程,讀者是否感覺對 MyBatis 的源碼閱讀漸入佳境了呢?下面介紹通過會話工廠 SqlSessionFactory 創建會話 SqlSession 的步驟, SqlSession 是 MyBatis 暴露給外部使用的統一介面層。

3.2.3.1 SqlSessionFactory#openSession()

通過案例可以看到調用 SqlSessionFactory#openSession() 方法可以創建 SqlSession 對象,找到對應源碼如下:

public interface SqlSessionFactory {
  SqlSession openSession();
  SqlSession openSession(boolean autoCommit);
  SqlSession openSession(Connection connection);
  SqlSession openSession(TransactionIsolationLevel level);
  SqlSession openSession(ExecutorType execType);
  SqlSession openSession(ExecutorType execType, boolean autoCommit);
  SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
  SqlSession openSession(ExecutorType execType, Connection connection);
  Configuration getConfiguration();
}

SqlSessionFactory 是一個介面,其中重載了很多 openSession() 方法,同時還包括一個獲取 Configuration 對象的 getConfiguration() 方法。

3.2.3.2 DefaultSqlSessionFactory#openSessionFromDataSource()

對於 SqlSessionFactory 介面,其對應的默認實現類是 org.apache.ibatis.session.defaults.DefaultSqlSessionFactory 類,在該類中找到了對應的 openSession() 方法的實現,其底層調用的是 DefaultSqlSessionFactory#openSessionFromDataSource() 方法來獲取 SqlSession 對象,對應源碼如下所示:

@Override
public SqlSession openSession(boolean autoCommit) {
  return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, autoCommit);
}

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
  // jdbc事務管理器
  Transaction tx = null;
  try {
    // 資料庫環境資訊
    final Environment environment = configuration.getEnvironment();
    // 事務工廠
    final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
    // 通過事務工廠獲取一個事務實例
    tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
    // 獲取執行器
    final Executor executor = configuration.newExecutor(tx, execType);
    // 獲取SqlSession會話對象,其中org.apache.ibatis.session.defaults.DefaultSqlSession是SqlSession的默認實現類
    return new DefaultSqlSession(configuration, executor, autoCommit);
  } catch (Exception e) {
    closeTransaction(tx);
    throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

通過下面的時序圖可以更好地理解創建 SqlSession 的過程:

3.2.4 創建Mapper介面的代理對象

在案例中,創建了 SqlSession 對象後會調用 getMapper() 方法創建 Mapper 介面的代理實例,下面我們先看調用該方法的時序圖,如下所示:

由時序圖我們可以得知真正創建 MapperProxy 代理實例涉及到的核心類是 MapperProxyFactoy 類和 MapperProxy 類,這兩個類我們在上文中提到過,這裡我們詳細閱讀相關源碼。

3.2.4.1 MapperProxyFactory

在 org.apache.ibatis.binding 包找到 MapperProxyFactory 類,其源碼如下所示。

public class MapperProxyFactory<T> {
  // Mapper介面
  private final Class<T> mapperInterface;
  // Mapper介面中的方法和方法封裝類的映射
  private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();
  public MapperProxyFactory(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }
  public Class<T> getMapperInterface() {
    return mapperInterface;
  }
  public Map<Method, MapperMethodInvoker> getMethodCache() {
    return methodCache;
  }
  @SuppressWarnings("unchecked")
  // newInstance()方法一:代理模式,創建一個MapperProxy
  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }
  // newInstance()方法二:根據SqlSession為Mapper介面創建一個MapperProxy代理實例
  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }
}

一看這個類的名字就知道這是個工廠類,目的就是為了生成 MapperProxy 。該類中由兩個 newInstance() 方法,第二個 newInstance() 方法中結合通過 SqlSession 類型的參數生成了一個 MapperProxy 代理實例,然後調用第一個 newInstance() 方法返回。在第一個方法中使用 Java 的 Proxy 類生成了一個 Mapper 介面的代理類,採用的是動態代理的方式。

3.2.4.2 MapperProxy

緊接著找到 MapperProxy 類的部分源碼如下。可以看到 MapperProxy 類實現了 InvocationHandler 介面並實現了其中的 invoke() 方法,這就是因為動態代理。

public class MapperProxy<T> implements InvocationHandler, Serializable {

  private static final long serialVersionUID = -6424540398559729838L;
  private final SqlSession sqlSession;
  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodCache;

  public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
    this.sqlSession = sqlSession;
    this.mapperInterface = mapperInterface;
    this.methodCache = methodCache;
  }
  // 省略......
  @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 {
        return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }
  // 省略......
}

3.2.5 使用代理對象執行相關方法

在創建了 Mapper 介面的代理對象之後,代理對象又是怎麼執行相應的方法的呢?我們在 3.2 節開頭展示案例中根據ID查找學生的語句處打上斷點進行調試,如圖所示。

image-20210308191500980

點擊 step into 按鈕會進入 org.apache.ibatis.binding.MapperProxy#invoke() 方法,如下圖所示。

image-20210308194637539

在執行 invoke() 方法後會調用 cacheMapperMethod() 方獲取 MapperMethod 對象。在 MapperProxy 類中找到 cacheMapperMethod() 方法,源碼如下:

private MapperMethod cachedMapperMethod(Method method) {
  return methodCache.computeIfAbsent(method, 
                                     k -> new MapperMethod(mapperInterface, 
                                                           method, 
                                                           sqlSession.getConfiguration()));
}

在上述程式碼中通過 new MapperMethod() 方法創建 MapperMethod , 其中 mapperInterface 就是 com.jd.yip.mapper.StudentMapper 介面, method 就是 cacheMapperMethod() 方法傳入的 Method 類型的參數,即 getStudentById() 方法,而 sqlSession.getConfiguration() 獲取的就是 Configuration 配置對象。在獲取到 MapperMethod 後,會執行 mapperMethod.execute(sqlSession, args) 方法返回。該方法位於 org.apache.ibatis.binding 包下的 MapperMethod 類中,源碼如下所示,首先會獲取 SQL 語句的類型,然後進入 switch-case 結構。

public Object execute(SqlSession sqlSession, Object[] args) {
  Object result;
  // 獲取SQL命令類型進入switch-case
  switch (command.getType()) {
    case INSERT: {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.insert(command.getName(), param));
      break;
    }
    case UPDATE: {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.update(command.getName(), param));
      break;
    }
    case DELETE: {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.delete(command.getName(), param));
      break;
    }
    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 {
        // 將參數轉換成SQL語句的參數
        Object param = method.convertArgsToSqlCommandParam(args);
        // 調用SqlSession#selectOne()方法得到結果
        result = sqlSession.selectOne(command.getName(), param);
        if (method.returnsOptional()
            && (result == null || !method.getReturnType().equals(result.getClass()))) {
          result = Optional.ofNullable(result);
        }
      }
      break;
    case FLUSH:
      result = sqlSession.flushStatements();
      break;
    default:
      throw new BindingException("Unknown execution method for: " + command.getName());
  }
  if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
    throw new BindingException("Mapper method '" + command.getName()
        + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
  }
  return result;
}

案例中根據 ID 查詢學生資訊屬於 SELECT ,因此進入對應的 case 分支判斷當前方法的返回情況,案例的情況會直接進入最後的 else 語句,先將參數轉化成 SQL 語句所需參數,然後進入到 SqlSession 的默認實現類 DefaultSqlSession 中調用 selectOne() 方法,如下圖所示。

image-20210308202739089

進入 DefaultSqlSession#selectOne() 方法後會繼續調用當前類的 selectList() 方法,如圖所示。

image-20210308202408597

繼續 step into 進入到 selectList() 方法,可以看到該方法有多種重載形式,其中最關鍵的是調用 Executor#query() 方法獲取到查詢結果。

image-20210308203427087

至此,我們通過調試案例程式碼,就理清了 Mpaaer 介面代理對象執行相關方法的流程,現對上述流程進行小結。

  • 代理對象調用 invoke() 方法通過 cacheMapperMethod() 方法創建 MapperMethod ,然後執行 execute(sqlSession, args) 方法;
  • 在 execute() 方法中根據 SQL 的類型進入相應的 case 分支;
  • 在 case 分支調用 SqlSession 默認實現類 DefaultSqlSession 中與 select 、 update 、 delete 、 insert 相關的方法;
  • 在 DefaultSqlSession 類中,這些與增刪改查相關的方法又會調用 Executor 類中對應 query() 或 update() 等方法;
  • 返回相應的結果。

總結

本節中以根據學生 ID 查找學生資訊這樣一條 SQL 語句為案例,結合 MyBatis 的源碼梳理了 SQL 語句的具體執行流程:

  • 調用 Resources 類載入配置文件;
  • 創建 SqlSessionFactory 會話工廠,這個過程中首先涉及到 XMLConfigBuilder 類解析 MyBatis 的基礎配置文件,然後我們詳細介紹了 XMLMapperBuilder 類與 MapperAnnotationBuilder 類實現指定 xml 文件和 指定 Mapper 介面時對 Mapper 的解析與載入,這也是本文的重點,最後 Mapper 解析載入完成後最重要的是將解析結果組裝生成 MappedStatement 並註冊到 Configuration 對象中;
  • 根據 SqlSessionFactory 會話工廠創建 SqlSession 會話,這裡涉及到 SqlSessionFactory 和 SqlSession 的默認實現類,即 DefaultSqlSessionFactory 以及 DefaultSqlSession。調用 SqlSessionFactory#openSession() 方法創建 SqlSession 的底層其實調用的是 DefaultSqlSessionFactory#openSessionFromDataSource() 方法。
  • 創建 MapperProxy 代理實例,這裡涉及到 MapperProxyFactory 與 MapperProxy 兩個類。通過動態代理的方式,在 SqlSession 執行的時候通過 MapperProxyFactory#newInstance() 方法創建 Mapper 介面的代理對象;
  • 代理對象執行相應的方法。通過斷點調試的方式可以看到在執行方法時會進入代理對象的 invoke() 方法創建 MapperMethod,然後執行 execute() 方法中相應的 case 分支,隨後進入 SqlSession 中執行 SQL 語句類型對應的方法,最後進入 Executor 中執行 query() 方法得到並返回結果。

全文總結

本文從 MyBatis 的簡單快速案例出發介紹了 MyBatis 的整體架構,然後介紹了 MyBatis 的運行流程結構,進一步以一條實際的 SQL 語句為案例從源碼角度剖析了 SQL 語句的詳細執行流程,其中重點在於 Mapper 的解析與載入以及 Mapper 介面代理對象的創建,最後對 MyBatis 的運行流程做了一定的總結。在閱讀源碼的過程中不難發現 MyBatis 運行時對方法的調用是一層套一層,這時候就需要讀者耐心地從入口函數開始層層深入,如升級打怪一般,到最後理清整個流程後你可以獲得的就是如遊戲通關般的暢快感。當然,由於筆者水平有限,本文只是管中窺豹,只可見一斑而不能得全貌,讀者可以跟著文章的解讀思路自行探索直到窺得 MyBatis 全貌。

參考資料

MyBatis 官網://mybatis.org/mybatis-3/

MyBatis 源碼倉庫://github.com/mybatis/mybatis-3

《互聯網輕量級 SSM 框架解密:Spring 、 Spring MVC 、 MyBatis 源碼深度剖析》

《MyBatis3 源碼深度解析》

《Spring MVC + MyBatis 開發從入門到實踐》