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

Chiakiiii發表於2021-03-11

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 官網:https://mybatis.org/mybatis-3/

MyBatis 原始碼倉庫:https://github.com/mybatis/mybatis-3

《網際網路輕量級 SSM 框架解密:Spring 、 Spring MVC 、 MyBatis 原始碼深度剖析》

《MyBatis3 原始碼深度解析》

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

相關文章