別再問我 MyBatis 了,Mapper 的解析與加載底層原理我都能講清楚~

大家都知道,利用 Spring 整合 MyBatis,我們可以直接利用 @MapperScan 註解或者 @Mapper 註解,讓 Spring 可以掃描全部的 Mapper 接口,解析然後加載。那麼如果拋開 Spring,你們可知道 MyBatis 是如何解析和加載 Mapper 接口的?

如果不知道的話,可以跟着我這篇文章,一步一步地深入和解讀源碼,帶你從底層來看通 MyBatis 解析加載 Mapper 的實現原理。

文章可是很長的,你能全部看完麼?啊哈哈哈~
當然了,如果大家本來就對 MyBatis 挺熟悉的,可以根據自己的情況挑選着目錄來看!

一、MyBatis 核心組件:

在解讀源碼之前,我們很有必要先了解 MyBatis 幾大核心組件,知道他們都是做什麼用的。

核心組件有:Configuration、SqlSession、Executor、StatementHandler、ParameterHandler、ResultSethandler。

下面簡單介紹一下他們:

  • Configuration:用於描述 MyBatis 主配置文件信息,MyBatis 框架在啓動時會加載主配置文件,將配置信息轉換爲 Configuration 對象。

  • SqlSession:面向用戶的 API,是 MyBatis 與數據庫交互的接口。

  • Executor:SQL 執行器,用於和數據庫交互。SqlSession 可以理解爲 Executor 組件的外觀(外觀模式),真正執行 SQL 的是 Executor 組件。

  • MappedStatement:用於描述 SQL 配置信息,MyBatis 框架啓動時,XML 文件或者註解配置的 SQL 信息會被轉換爲 MappedStatement 對象註冊到 Configuration 組件中。

  • StatementHandler:封裝了對 JDBC 中 Statement 對象的操作,包括爲 Statement 參數佔位符設置值,通過 Statement 對象執行 SQL 語句。

  • TypeHandler:類型處理器,用於 Java 類型與 JDBC 類型之間的轉換。

  • ParameterHandler:用於處理 SQL 中的參數佔位符,爲參數佔位符設置值。

  • ResultSetHandler:封裝了對 ResultSet 對象的處理邏輯,將結果集轉換爲 Java 實體對象。

二、簡述 Mapper 執行流程:

SqlSession組件,它是用戶層面的API。用戶可利用 SqlSession 獲取想要的 Mapper 對象(MapperProxy 代理對象);當執行 Mapper 的方法,MapperProxy 會創建對應的 MapperMetohd,然後 MapperMethod 底層其實是利用 SqlSession 來執行 SQL。

但是真正執行 SQL 操作的應該是 Executor組 件,Executor 可以理解爲 SQL 執行器,它會使用 StatementHandler 組件對 JDBC 的 Statement 對象進行操作。當 Statement 類型爲 CallableStatement 和 PreparedStatement 時,會通過 ParameterHandler 組件爲參數佔位符賦值。

ParameterHandler 組件中會根據 Java 類型找到對應的 TypeHandler 對象,TypeHandler 中會通過 Statement 對象提供的 setXXX() 方法(例如setString()方法)爲 Statement 對象中的參數佔位符設置值。

StatementHandler 組件使用 JDBC 中的 Statement 對象與數據庫完成交互後,當 SQL 語句類型爲 SELECT 時,MyBatis 通過 ResultSetHandler 組件從 Statement 對象中獲取 ResultSet 對象,然後將 ResultSet 對象轉換爲 Java 對象。

我們可以用一幅圖來描述上面各個核心組件之間的關係:

MyBatis 各大組件關係

三、簡單例子深入講解底層原理

下面我將帶着一個非常簡單的 Mapper 使用例子來講解底層的流程和原理。

例子很簡單,首先是獲取 MyBatis 的主配置文件的文件輸入流,然後創建 SqlSessinoFactory,接着利用 SqlSessionFactory 創建 SqlSessin;然後利用 SqlSession 獲取要使用的 Mapper 代理對象,最後執行 Mapper 的方法獲取結果。

1、代碼例子:

@Test
public  void testMybatis () throws IOException {
    // 獲取配置文件輸入流
    InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
    // 通過SqlSessionFactoryBuilder的build()方法創建SqlSessionFactory實例
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    // 調用openSession()方法創建SqlSession實例
    SqlSession sqlSession = sqlSessionFactory.openSession();
    // 獲取UserMapper代理對象
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    // 執行Mapper方法,獲取執行結果
    List<UserEntity> userList = userMapper.listAllUser();
    System.out.println(JSON.toJSONString(userList));
}

第一行代碼,非常的明顯,就是讀取 MyBatis 的主配置文件。正常來說,這個主配置文件應該用來創建 Configuration ,但是這裏卻是傳給 SqlSessionFactoryBuilder 來創建 SqlSessionFactory,然後就利用工廠模式來創建 SqlSession 了;上面我們也提及到, SqlSession 是提供給用戶友好的數據庫操作接口,那麼豈不是說不需要 Configuratin 也可以直接獲取 Mapper 然後操作數據庫了?

那當然不是了,Configuration 是 MyBatis 的主配置類,它裏面會包含 MyBatis 的所有信息(不管是主配置信息,還是所有 Mapper 配置信息),所以肯定是需要創建的。

所以其實在創建 SqlSessionFactory 時就已經初始化 Configuration 了,因爲 SqlSession 需要利用 Executor、ParameterHandler 和 ResultSetHandler 等等各大組件互相配合來執行 Mapper,而 Configuration 就是這些組件的工廠類。

我們可以在 SqlSessionFactoryBuilder#build() 方法中看到 Configuration 是如何被初始化的:

public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
      return build(parser.parse());
    //......
  }

從上面的代碼能看到,就是根據主配置文件的文件輸入流創建 XMLConfigBuilder 對象,然後利用 XMLConfigBuilder#parse() 方法來創建 Configuration 對象。

當然了,雖然只是簡單的調用了 XMLConfigBuilder#parse() 方法,可是裏面包含的東西是非常的多的。例如: MyBatis 主配置文件的解析;如何根據 <mappers> 標籤給每個 Mapper 接口生產 MapperProxy 代理類和將 SQL 配置轉換爲 MappedStatement;以及 <cache>、<resultMap>、<parameterMap>、<sql> 等等標籤是如何解析的。

當然了,這篇文章我們只會着重於關於 Mapper 配置的解析和加載,根據底層源碼一步一步的去分析弄明白,至於其他的知識點就不過多講解了。

2、XMLConfigBuilder 中關於 Configuration 的解析過程

Ⅰ. XMLConfigBuilder#parseConfiguration()

上面講到 XMLConfigBuilder 會調用 parse() 方法去解析 MyBatis 的主配置文件,底層主要是利用 XPATH 來解析 XML 文件。代碼如下:

public class XMLConfigBuilder extends BaseBuilder {

    // .....
    
    private final XPathParser parser;

    // .....

    public Configuration parse() {
        // 防止parse()方法被同一個實例多次調用
        if (parsed) {
          throw new BuilderException("Each XMLConfigBuilder can only be used once.");
        }
        parsed = true;
        // 調用XPathParser.evalNode()方法,創建表示configuration節點的XNode對象。
        // 調用parseConfiguration()方法對XNode進行處理
        parseConfiguration(parser.evalNode("/configuration"));
        return configuration;
    }
    
    private void parseConfiguration(XNode root) {
        try {
            propertiesElement(root.evalNode("properties"));
            Properties settings = settingsAsProperties(root.evalNode("settings"));
            loadCustomVfs(settings);
            typeAliasesElement(root.evalNode("typeAliases"));
            pluginElement(root.evalNode("plugins"));
            objectFactoryElement(root.evalNode("objectFactory"));
            objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
            reflectorFactoryElement(root.evalNode("reflectorFactory"));
            settingsElement(settings);
            environmentsElement(root.evalNode("environments"));
            databaseIdProviderElement(root.evalNode("databaseIdProvider"));
            typeHandlerElement(root.evalNode("typeHandlers"));
            // 最重要的關注點
            mapperElement(root.evalNode("mappers"));
        } catch (Exception e) {
          throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
    }
    // ....
}

在 XMLConfigBuilder#parseConfiguration() 方法裏面,會對主配置文件裏的所有標籤進行解析;當然了,由於我們這篇文章的主題是分析 Mapper 的解析和加載過程,所以接下來將直接關注 parseConfiguration() 方法裏面的 mapperElement() 方法,其他部分大家可以直接去閱讀 MyBatis 的源碼。

備註:MyBatis 裏面的所有 xxxBuilder 類都是繼承與 BaseBuilder,而 BaseBuilder 要注意的點就是它持有着 Configuration 實例的引用。

Ⅱ . XMLConfigBuilder#mapperElement()

在 XMLConfigBuilder#mapperElement() 方法裏面,主要是解析 <mappers> 標籤裏面的 <package> 標籤和 <mapper> 標籤,這兩個標籤主要是描述 Mapper 接口的全路徑、Mapper 接口所在的包的全路徑以及 Mapper 接口對應的 XML 文件的全路徑。

代碼如下:

  private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        // 通過<package>標籤指定包名
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          // 通過resource屬性指定XML文件路徑
          if (resource != null && url == null && mapperClass == null) {
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url != null && mapperClass == null) {
            // 通過url屬性指定XML文件路徑
            ErrorContext.instance().resource(url);
            InputStream inputStream = Resources.getUrlAsStream(url);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url == null && mapperClass != null) {
            // 通過class屬性指定接口的完全限定名
            Class<?> mapperInterface = Resources.classForName(mapperClass);
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }

從上面的代碼來看,主要是將標籤分爲 <package> 和 <mapper> 來解析,而再細一點可以分爲兩種解析情況:一種是指定了Mapper接口的 XML 文件,而另外一種是指定了 Mapper 接口。

那麼我們可以先看看指定 XML 文件是如何解析與加載 Mapper 的。

3、XMLMapperBuilder 中關於 Mapper 的解析過程

Mapper 接口的 XML 文件的解析當然也是利用 XPath,但此時不再是 XMLConfigBuilder 來負責了,而是需要創建一個 XMLMapperBuilder 對象,而 XMLMapperBuilder 需要傳入 XML 文件的文件輸入流。

Ⅰ . XMLMapperBuilder#parse()

我們可以看看 XMLMapperBuilder#parse() 方法,XML 文件的解析流程就是在這裏面:

public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      // 調用XPathParser的evalNode()方法獲取根節點對應的XNode對象
      configurationElement(parser.evalNode("/mapper"));
      // 將資源路徑添加到Configuration對象中
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }
    // 繼續解析之前解析出現異常的ResultMap對象
    parsePendingResultMaps();
    // 繼續解析之前解析出現異常的CacheRef對象
    parsePendingCacheRefs();
    // 繼續解析之前解析出現異常<select|update|delete|insert>標籤配置
    parsePendingStatements();
}

解析前,會先判斷 Configuratin 是否已經加載這個 XML 資源,如果不存在,則調用 configurationElement() 方法;在方法裏面會解析所有的 <cache-ref>、<cache>、<parameterMap>、<resultMap>、<sql> 和 <select|insert|update|delete> 標籤。

Ⅱ . XMLMapperBuilder#configuratinElement()

下面我們先看一下 XMLMapperBuilder#configuratinElement() 方法的代碼:

private void configurationElement(XNode context) {
    try {
      // 獲取命名空間
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      // 設置當前正在解析的Mapper配置的命名空間
      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);
    }
}

當然了,我們此時將所有注意力集中在 buildStatementFromContext 方法即可。

Ⅲ . XMLMapperBuilder#buildStatementFromContext()

在這個方法裏面,會調用重載的 buildStatementFromContext 方法;但是這裏還不是真正解析的地方,而是遍歷所有標籤,然後創建一個 XMLStatementBuilder 對象,對標籤進行解析。代碼如下:

  private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
      // 通過XMLStatementBuilder對象,對<select|update|insert|delete>標籤進行解析
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
        // 調用parseStatementNode()方法解析
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }

4、XMLStatementBuilder 中關於 <select|insert|update|delete> 的解析過程

Ⅰ. XMLStatementBuilder#parseStatementNode()

那麼我們接着看看 XMLStatementBuilder#parseStatementNode 方法:

  public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }
    // 解析<select|update|delete|insert>標籤屬性
    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);
    String resultMap = context.getStringAttribute("resultMap");
    String resultType = context.getStringAttribute("resultType");
    // 獲取LanguageDriver對象
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);
    // 獲取Mapper返回結果類型Class對象
    Class<?> resultTypeClass = resolveClass(resultType);
    String resultSetType = context.getStringAttribute("resultSetType");
    // 默認Statement類型爲PREPARED
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType",
            StatementType.PREPARED.toString()));
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);

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

    // 解析<selectKey>標籤
    processSelectKeyNodes(id, parameterTypeClass, langDriver);
    
    // 通過LanguageDriver解析SQL內容,生成SqlSource對象
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    String resultSets = context.getStringAttribute("resultSets");
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    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;
    }

    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered, 
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

從上面的代碼可得,首先會解析標籤裏的所有屬性;然後創建 LanguageDriver 來解析標籤裏面的 SQL 配置,並生成對應的 SqlSource 對象;最後,利用工具類 MapperBuilderAssistant 來將上面解析的內容組裝成 MappedStatement 對象,並且註冊到 Configuration 中。

5、詳細介紹 SqlSource 與 LanguageDriver 接口

上面我們說到,解析的 SQL 內容會生成對應的 SqlSource 對象,那麼我們先看看 SqlSource 接口,代碼如下:

public interface SqlSource {

  BoundSql getBoundSql(Object parameterObject);

}

SqlSource 接口的定義非常簡單,只有一個 getBoundSql() 方法,該方法返回一個 BoundSql 實例。

所以說 BoundSql 纔是對 SQL 語句及參數信息的封裝,它是 SqlSource 解析後的結果,BoundSql 的代碼如下:

public class BoundSql {

  // Mapper配置解析後的sql語句
  private final String sql;
  // Mapper參數映射信息
  private final List<ParameterMapping> parameterMappings;
  // Mapper參數對象
  private final Object parameterObject;
  // 額外參數信息,包括<bind>標籤綁定的參數,內置參數
  private final Map<String, Object> additionalParameters;
  // 參數對象對應的MetaObject對象
  private final MetaObject metaParameters;
  // ... 省略 get/set 和 構造函數
}

因爲 SQL 的解析是利用 LanguageDriver 組件完成的,所以我們再接着看看 LanguageDriver 接口,代碼如下:

public interface LanguageDriver {

  ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql);

  SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType);

  SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);

}

如上面的代碼所示,LanguageDriver 接口中一共有3個方法,其中 createParameterHandler() 方法用於創建 ParameterHandler 對象,另外還有兩個重載的 createSqlSource() 方法,這兩個重載的方法用於創建 SqlSource 對象。

MyBatis 中爲 LanguageDriver 接口提供了兩個實現類,分別爲 XMLLanguageDriver 和 RawLanguageDriver。

  • XMLLanguageDriver 爲 XML 語言驅動,實現了動態 SQL 的功能,也就是說可以利用 MyBatis 提供的 XML 標籤(常用的、等標籤)結合OGNL表達式語法來實現動態的條件判斷。
  • RawLanguageDriver 表示僅支持靜態 SQL 配置,不支持動態 SQL 功能。

接下來我們重點了解一下 XMLLanguageDriver 實現類的內容,代碼如下:

public class XMLLanguageDriver implements LanguageDriver {

  @Override
  public ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    return new DefaultParameterHandler(mappedStatement, parameterObject, boundSql);
  }
  @Override
  public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    // 該方法用於解析XML文件中配置的SQL信息
    // 創建XMLScriptBuilder對象
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    // 調用 XMLScriptBuilder對象parseScriptNode()方法解析SQL資源
    return builder.parseScriptNode();
  }

  @Override
  public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
    // 該方法用於解析Java註解中配置的SQL信息
    // 字符串以<script>標籤開頭,則以XML方式解析
    if (script.startsWith("<script>")) {
      XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
      return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
    } else {
      // 解析SQL配置中的全局變量
      script = PropertyParser.parse(script, configuration.getVariables());
      TextSqlNode textSqlNode = new TextSqlNode(script);
      // 如果SQL中是否仍包含${}參數佔位符,則返回DynamicSqlSource實例,否則返回RawSqlSource
      if (textSqlNode.isDynamic()) {
        return new DynamicSqlSource(configuration, textSqlNode);
      } else {
        return new RawSqlSource(configuration, script, parameterType);
      }
    }
  }

}

如上面的代碼所示,XMLLanguageDriver 類實現了 LanguageDriver 接口中兩個重載的 createSqlSource() 方法,分別用於處理 XML 文件和 Java 註解中配置的 SQL 信息,將 SQL 配置轉換爲 SqlSource 對象。

第一個重載的 createSqlSource() 方法用於處理 XML 文件中配置的 SQL 信息,該方法中創建了一個 XMLScriptBuilder 對象,然後調用 XMLScriptBuilder 對象的 parseScriptNode() 方法將 SQL 資源轉換爲 SqlSource 對象。

第二個重載的 createSqlSource() 方法用於處理 Java 註解中配置的 SQL 信息,該方法中首先判斷 SQL 配置是否以 <script> 標籤開頭。如果是,則以 XML 方式處理 Java 註解中配置的 SQL 信息;否則只是簡單處理,替換 SQL 中的全局參數即可。如果 SQL 中仍然包含 ${} 參數佔位符,則 SQL 語句仍然需要根據傳遞的參數動態生成,所以使用 DynamicSqlSource 對象描述 SQL 資源,否則說明 SQL 語句不需要根據參數動態生成,使用 RawSqlSource 對象描述 SQL 資源。

從 XMLLanguageDriver 類的 createSqlSource() 方法的實現來看,我們除了可以通過 XML 配置文件結合 OGNL 表達式配置動態 SQL 外,還可以通過 Java 註解的方式配置,只需要註解中的內容加上 <script> 標籤。

當然了,此時我們只需先關注第一個重載的 createSqlSource() 方法即可。
我們可以看到方法中,會先創建 XMLScriptBuilder 對象,接着調用 XMLScriptBuilder 對象 parseScriptNode() 方法解析SQL資源。

這一層套一層的,真深啊,不過這分層確實還是非常的棒的,每個類的職責很專一,使得代碼看起來很舒服,而且也大大地提高了代碼的可擴展性。

6、XMLScriptBuilder 中關於 SQL 資源的解析過程

Ⅰ . XMLScriptBuilder#parseScriptNode()

那麼接下來,我們可以繼續看看 XMLScriptBuilder 的 parseScriptNode 方法是如何解析的,代碼如下:

  public SqlSource parseScriptNode() {
    // 調用parseDynamicTags()方法將SQL配置轉換爲SqlNode對象
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource = null;
    // 判斷Mapper SQL配置中是否包含動態SQL元素,如果是創建DynamicSqlSource對象,否則創建RawSqlSource對象
    if (isDynamic) {
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }

從上面的代碼來看,會先調用 parseDynamicTags() 方法,將 SQL 配置轉換爲 SqlNode 對象;然後根據變量 isDynamic 判斷 Mapper SQL 配置中是否包含動態 SQL 元素,如果是創建 DynamicSqlSource 對象,否則創建 RawSqlSource 對象返回給 XMLStatementBuilder 的 parseStatementNode 方法。

Ⅱ. XMLScriptBuilder#parseDynamicTags()

那麼我們先看看 XMLScriptBuilder#parseDynamicTags() 方法吧,代碼如下:

  protected MixedSqlNode parseDynamicTags(XNode node) {
    List<SqlNode> contents = new ArrayList<SqlNode>();
    NodeList children = node.getNode().getChildNodes();
    // 對XML子元素進行遍歷
    for (int i = 0; i < children.getLength(); i++) {
      XNode child = node.newXNode(children.item(i));
      // 如果子元素爲SQL文本內容,則使用TextSqlNode描述該節點
      if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
        String data = child.getStringBody("");
        TextSqlNode textSqlNode = new TextSqlNode(data);
        // 判斷SQL文本中包含${}參數佔位符,則爲動態SQL
        if (textSqlNode.isDynamic()) {
          contents.add(textSqlNode);
          isDynamic = true;
        } else {
          // 如果SQL文本中不包含${}參數佔位符,則不是動態SQL
          contents.add(new StaticTextSqlNode(data));
        }
      } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
        // 如果子元素爲<if>、<where>等標籤,則使用對應的NodeHandler處理
        String nodeName = child.getNode().getNodeName();
        NodeHandler handler = nodeHandlerMap.get(nodeName);
        if (handler == null) {
          throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
        }
        handler.handleNode(child, contents);
        isDynamic = true;
      }
    }
    return new MixedSqlNode(contents);
  }

上面的代碼的註解已經寫得非常的明白了,就是遍歷標籤裏面的子元素,如果是 SQL 文本內容,判斷是否包含 ${} 參數佔位符,如果包含則創建 TextSqlNode 對象添加到 contents 列表裏,當然了,還需要將 isDynamic 設置爲 true 來表示這是動態 SQL;否則創建 StaticTextSqlNode 對象。

isDynamic 就是用來判斷創建 DynamicSqlSource 對象還是 RawSqlSource 對象;接着如果是爲 <if>、<where> 等標籤,則使用對應的 NodeHandler 處理。

大家可能都好奇什麼是 NodeHandler,其實我們可以看到 XMLScriptBuilder 裏面的 nodeHandlerMap 屬性就會記錄着全部的 NodeHandler,其中 key 是標籤名,value 就是對應的 NodeHandler了;並且在創建 XMLScriptBuilder 時,會調用 initNodeHandlerMap 方法來初始化 nodeHandlerMap 屬性。

public class XMLScriptBuilder extends BaseBuilder {

  private final XNode context;
  private boolean isDynamic;
  private final Class<?> parameterType;
  private final Map<String, NodeHandler> nodeHandlerMap = new HashMap<String, NodeHandler>();

   // .....
  private void initNodeHandlerMap() {
    nodeHandlerMap.put("trim", new TrimHandler());
    nodeHandlerMap.put("where", new WhereHandler());
    nodeHandlerMap.put("set", new SetHandler());
    nodeHandlerMap.put("foreach", new ForEachHandler());
    nodeHandlerMap.put("if", new IfHandler());
    nodeHandlerMap.put("choose", new ChooseHandler());
    nodeHandlerMap.put("when", new IfHandler());
    nodeHandlerMap.put("otherwise", new OtherwiseHandler());
    nodeHandlerMap.put("bind", new BindHandler());
  }
  // .....
}

從上面代碼我們可以看到,每種動態 SQL 的標籤都有對應的 NodeHandler,這些 NodeHandler 會將標籤轉換爲對應的 SqlNode。例如 <if> 標籤會轉換爲 IfSqlNode,<choose> 標籤會轉換爲 ChooseSqlNode。

最後會用 MixedSqlNode 整理 SQL 轉換的所有 SqlNode。
我們可以看看 MixedSqlNode 的代碼:

public class MixedSqlNode implements SqlNode {
  private final List<SqlNode> contents;

  public MixedSqlNode(List<SqlNode> contents) {
    this.contents = contents;
  }

  @Override
  public boolean apply(DynamicContext context) {
    for (SqlNode sqlNode : contents) {
      sqlNode.apply(context);
    }
    return true;
  }
}

MixedSqlNode 的作用其實是非常的簡單,就是用 contents 屬性來保存 SQL 配置的所有子元素對應的 SqlNode 列表,也就是 XMLScriptBuilder#parseDynamicTags() 方法中的局部列表變量 contents,裏面包含着 SQL 配置解析後的一個或多個 SqlNode 對象。

上面我們也講到:當將 SQL 配置 都轉換爲 SqlNode 後,還會根據 isDynamic 來判斷創建 DynamicSqlSource 還是 RawSqlSource 對象。

Ⅲ . DynamicSqlSource 和 RawSqlSource

創建 DynamicSqlSource 非常簡單,將 Configuration 和 MixedSqlNode 封裝起來即可,因爲當執行 Mappper 接口的方法時,會根據入參來調用 DynamicSqlSource 的 getBoundSql 方法來解析動態 SQL。

而 RawSqlSource 的創建會稍微麻煩一點,因爲他還需要將 #{} 參數佔位符轉換爲 ? ,並保存參數映射關係。

備註:DynamicSqlSource 也會處理 #{} 參數佔位符,只不過是在執行 getBoundSql() 方法時纔會進行處理。

7、SqlSourceBuilder 協助創建 RawSqlSource

首先在 RawSqlSource 的構造函數裏面,會創建 SqlSourceBuilder 對象,接着會調用 SqlSourceBuilder#parse() 方法,在 parse() 方法裏面會會創建 GenericTokenParser 和 ParameterMappingTokenHandler。

  • GenericTokenParse 是 Token解析器,用於解析#{}參數。

  • ParameterMappingTokenHandler爲Mybatis參數映射處理器,用於處理SQL中的#{}參數佔位符,將 #{} 參數佔位符轉爲 ? 。並且 ParameterMappingTokenHandler 的 parameterMappings 屬性保存着參數的映射關係。

代碼如下所示:

public class SqlSourceBuilder extends BaseBuilder {

    private static final String parameterProperties = "javaType,jdbcType,mode,numericScale,resultMap,typeHandler,jdbcTypeName";
    
    public SqlSourceBuilder(Configuration configuration) {
        super(configuration);
    }
    public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
        // ParameterMappingTokenHandler爲Mybatis參數映射處理器,用於處理SQL中的#{}參數佔位符
        ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
        // Token解析器,用於解析#{}參數
        GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
        // 調用GenericTokenParser對象的parse()方法將#{}參數佔位符轉換爲?
        String sql = parser.parse(originalSql);
        return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
    }
  ....
}

好了,到這一步,已經將 SQL 配置解析成對應的 SqlSource 對象,其實就等於解析完成了,而動態 SQL (<if> 等標籤)只能等待執行 Mapper 時,根據入參來繼續解析了。

8、將 SQL 配置解析後的信息組裝成 MappedStatement

解析完 SQL 配置後,我們需要回到 XMLStatementBuilder#parseStatementNode() 方法中,需要研究的就是最後的
MapperBuilderAssistant#addMappedStatement() 方法。

這個方法會根據上面 SQL 配置解析後的所有信息,封裝成對應的 MappedStatement 對象,然後註冊到 Configuration 的 mappedStatements 屬性中。mappedStatements 爲 Map 結構,其中 Key 爲 Mapper Id,Value 爲 MappedStatement 對象。

此時 Mapper 接口對應的 XML 文件裏面的所有配置都已經解析完成了,但是我們可以發現,還沒有爲 Mapper 接口創建對應的代理類。

9、將 Mapper 接口註冊到 Configuration 中

我們此時可以重新回到 XMLMapperBuilder#parse() 方法中。接着裏面的 configurationElement() 方法繼續往下走。

configuration.addLoadedResource(resource) 就不用講了,就是將 XML 文件的路徑添加到 Configuration 的 loadedResources 屬性中,藉此判斷 XML 文件是否已經被解析過了。

Ⅰ . XMLMapperBuilder#bindMapperForNameSpace()

接着就是下一個重點了,就是 XMLMapperBuilder#bindMapperForNamespace() 方法。

我們先直接看代碼:

  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) {
        if (!configuration.hasMapper(boundType)) {
         
          configuration.addLoadedResource("namespace:" + namespace);
          configuration.addMapper(boundType);
        }
      }
    }
  }

我們可以看到,先調用 Configuration#hasMapper() 方法來判斷 Mapper 接口是否註冊過了,如果沒註冊過就調用 Configuration#addMapper() 方法來註冊 Mapper 接口,所以說,生成動態代理類的重點在這個方法裏面。

Ⅱ . Configuration#addMapper()

Configuration#addMapper() 方法是調用屬性 MapperRegistery#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 {
        knownMappers.put(type, new MapperProxyFactory<T>(type));
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

我們可以看到方法裏面會調用 Configuration 屬性 knownMappers 的 put 方法,將 key 爲 Mapper 接口對應 Class 對象,value 爲 Mapper 接口的 代理工廠類 MapperProxyFactory。

備註:當執行 Mapper 時,MapperProxyFactory會根據 SqlSession 爲 Mapper 接口創建一個 MapperProxy 代理實例。

我們還可以看到,下面會根據當前 Configuration 配置和 Mapper 接口的 Class 對象創建一個 MapperAnnotationBuilder 對象,然後調用 MapperAnnotationBuilder#parse() 方法。

這裏我就先不給大家詳解了,因爲後面會講解到,但是可以告訴大家,這裏主要就是爲了解析 Mapper 接口那些使用 SQL 註解的方法,例如 @Select 系列註解和 @SelectProvider 系列註解。解析後也是會生成對應的 MappedStatement 註冊到 Configuration 的 mappedStatements 屬性中。

至此,關於指定 XML 文件解析和加載 Mapper 接口的整個流程已經完畢。在這裏我還是簡單的給大家總結一下流程吧。

  1. 根據 XML 文件的輸入流創建 XMLMapperBuilder 對象,調用 parse() 方法作爲解析 <mapper> 標籤的入口。

  2. XMLMapperBuilder#configurationElement() 方法解析 <cache-ref>、<cache>、<parameterMap>、<resultMap>、<sql> 和 <select|insert|update|delete> 等所有標籤,而 <select|insert|update|delete> 標籤的解析入口爲 XMLMapperBuilder#buildStatementFromContext() 方法。

  3. XMLMapperBuilder#buildStatementFromContext() 方法中會遍歷所有 <select|insert|update|delete> 標籤,然後創建對應的 XMLStatementBuilder 對象,進行標籤解析。

  4. XMLStatementBuilder#parseStatementNode() 方法裏面首先會解析 <select|insert|update|delete> 標籤裏的所有屬性,然後利用 LanguageDriver 來解析 SQL 配置,將 SQL 的所有片段(包括靜態SQL和動態SQL)解析成對應的 SqlNode 對象,然後使用 MixedSqlNode 來保存起來,最後根據是否爲動態 SQL 來創建 DynamicSqlSource 對象或者 RawSqlSource 對象。

  5. 當 <select|insert|update|delete> 標籤解析完後,會利用工具類 MapperBuilderAssistant 的 addMappedStatement() 方法來將解析的所有信息封裝爲對應的 MappedStatement 對象,然後註冊到 Configuration 中。

  6. XML 文件解析完後,XMLMapperBuilder#parse() 方法會調用 Configuration#addLoadedResource() 方法將 XML 文件的資源路徑註冊到Configuration 中。

  7. 最後,在 XMLMapperBuilder#parse() 方法中還會調用 XMLMapperBuilder#bindMapperForNamespace() 方法,將 Mapper 接口註冊到 Configuration 中。接口註冊底層是使用 MapperRegistry 類,這個類會保存着所有 Mapper 接口的註冊信息。在註冊時,會爲 Mapper 接口創建對應的 MapperFactoryBean;當執行 Mapper 時,可以根據當前 SqlSession 創建 Mapper 接口對應的 MapperProxy 代理實例。

  8. XMLMapperBuilder#bindMapperForNamespace() 方法在 Mapper 接口註冊後,還會創建 MapperAnnotationBuilder 對象來解析 Mapper 接口帶 SQL 註解方法,也是生成對應的 MappedStatement 然後註冊到 Configuration 中。

接着看看指定 Mapper 接口是如何解析與加載 Mapper 的

10、MapperRegistry 將 Mapper 接口註冊到 Configuration 中

我們再回顧一下 XMLConfigBuilder#mapperElement() 方法:

  private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        // 通過<package>標籤指定包名
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          // .... 省略
          } else if (resource == null && url == null && mapperClass != null) {
            // 通過class屬性指定接口的完全限定名
            Class<?> mapperInterface = Resources.classForName(mapperClass);
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }

從上面代碼可以看到,指定 Mapper 接口來解析與加載,都只是簡單地調用了 Configuration#addMapper() 方法。而上面我們也已經講解到了,Mapper 接口的註冊底層是利用 MapperRegistry 。

但是此時我們會有一個疑問:Mapper 接口的 XML 文件不用解析嗎?

所以到這裏,我們需要繼續講解的就是上面省略掉的 MapperAnnotationBuilder。其實它不但是可以解析 Mapper 接口使用 SQL 註解的方法,還會嘗試加載 Mapper 接口對應的 XML 文件,如果不爲空,則會使用 XMLMapperBuilder 來解析 XML 文件。當然了,XMLMapperBuilder 解析 XMl 文件的流程和上面介紹的是一致的。

Ⅰ . MapperAnnotationBuilder#parse()

下面先看看 MapperAnnotationBuilder#parse() 方法,代碼如下:

  public void parse() {
    String resource = type.toString();
    if (!configuration.isResourceLoaded(resource)) {
      // 嘗試加載和解析 XML 文件
      loadXmlResource();
      configuration.addLoadedResource(resource);
      assistant.setCurrentNamespace(type.getName());
      parseCache();
      parseCacheRef();
      Method[] methods = type.getMethods();
      for (Method method : methods) {
        try {
          // issue #237
          if (!method.isBridge()) {
            // 處理 SQL 註解
            parseStatement(method);
          }
        } catch (IncompleteElementException e) {
          configuration.addIncompleteMethod(new MethodResolver(this, method));
        }
      }
    }
    parsePendingMethods();
  }

MapperAnnotationBuilder#parse() 方法首先會調用 loadXmlResource() 方法來嘗試加載並解析 Mapper 接口的 XMl 文件。在 loadXmlResource() 方法中,會根據 Mapper 接口的 name 來拼接 XML 的文件的名字,然後嘗試獲取文件輸入流;如果文件輸入流不爲空,則表示 Mapper 接口有對應的 XML 文件,此時會創建一個 XMLMapperBuilder 對象,然後對 XML 文件進行解析。

到這裏,我們就可以把 XML 文件也解析到了。

Ⅱ . MapperAnnotationBuilder#parseStatement()

當然了,Mapper 接口的方法可以直接使用像 @Select 這種 SQL 註解,所以 MapperAnnotationBuilder 也會嘗試加載並解析方法上的註解。

在 MapperAnnotationBuilder#parse() 方法中,會遍歷 Mapper 接口的所有方法(Method),然後調用 MapperAnnotationBuilder#parseStatement() 方法來解析。

代碼如下:

for (Method method : methods) {
    try {
      // issue #237
      if (!method.isBridge()) {
        parseStatement(method);
      }
    } catch (IncompleteElementException e) {
      configuration.addIncompleteMethod(new MethodResolver(this, method));
    }
}

  void parseStatement(Method method) {
    Class<?> parameterTypeClass = getParameterType(method);
    LanguageDriver languageDriver = getLanguageDriver(method);
    SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
    if (sqlSource != null) {
      Options options = method.getAnnotation(Options.class);
      final String mappedStatementId = type.getName() + "." + method.getName();
      Integer fetchSize = null;
      Integer timeout = null;
      StatementType statementType = StatementType.PREPARED;
      ResultSetType resultSetType = ResultSetType.FORWARD_ONLY;
      SqlCommandType sqlCommandType = getSqlCommandType(method);
      boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
      boolean flushCache = !isSelect;
      boolean useCache = isSelect;

      KeyGenerator keyGenerator;
      String keyProperty = null;
      String keyColumn = null;
      if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
        // first check for SelectKey annotation - that overrides everything else
        SelectKey selectKey = method.getAnnotation(SelectKey.class);
        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 {
        keyGenerator = NoKeyGenerator.INSTANCE;
      }

      if (options != null) {
        if (FlushCachePolicy.TRUE.equals(options.flushCache())) {
          flushCache = true;
        } else if (FlushCachePolicy.FALSE.equals(options.flushCache())) {
          flushCache = false;
        }
        useCache = options.useCache();
        fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; //issue #348
        timeout = options.timeout() > -1 ? options.timeout() : null;
        statementType = options.statementType();
        resultSetType = options.resultSetType();
      }

      String resultMapId = null;
      ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
      if (resultMapAnnotation != null) {
        String[] resultMaps = resultMapAnnotation.value();
        StringBuilder sb = new StringBuilder();
        for (String resultMap : resultMaps) {
          if (sb.length() > 0) {
            sb.append(",");
          }
          sb.append(resultMap);
        }
        resultMapId = sb.toString();
      } else if (isSelect) {
        resultMapId = parseResultMap(method);
      }

      assistant.addMappedStatement(
          mappedStatementId,
          sqlSource,
          statementType,
          sqlCommandType,
          fetchSize,
          timeout,
          // ParameterMapID
          null,
          parameterTypeClass,
          resultMapId,
          getReturnType(method),
          resultSetType,
          flushCache,
          useCache,
          // TODO gcode issue #577
          false,
          keyGenerator,
          keyProperty,
          keyColumn,
          // DatabaseID
          null,
          languageDriver,
          // ResultSets
          options != null ? nullOrEmpty(options.resultSets()) : null);
    }
  }

在 MapperAnnotationBuilder#parseStatement() 方法中會調用 getSqlSourceFromAnnotations() 方法,而方法中會分別調用 getSqlAnnotationType() 和 getSqlProviderAnnotationType() 方法來判斷 Method 是否帶有 @Select 或 @SelectProvider 等系列註解。

如果有的話,會利用 LanguageDriver 來解析 SQL 註解,也就是利用 XMLLanguageDriver 的第二個 createSqlSource() 重載方法,代碼如下:

  @Override
  public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
    // 該方法用於解析Java註解中配置的SQL信息
    // 字符串以<script>標籤開頭,則以XML方式解析
    if (script.startsWith("<script>")) {
      XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
      return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
    } else {
      // 解析SQL配置中的全局變量
      script = PropertyParser.parse(script, configuration.getVariables());
      TextSqlNode textSqlNode = new TextSqlNode(script);
      // 如果SQL中是否仍包含${}參數佔位符,則返回DynamicSqlSource實例,否則返回RawSqlSource
      if (textSqlNode.isDynamic()) {
        return new DynamicSqlSource(configuration, textSqlNode);
      } else {
        return new RawSqlSource(configuration, script, parameterType);
      }
    }
  }

上面的代碼,首先判斷是否有 <script> 註解,如果有則使用第一個 createSqlSource 重載方法以XML方式解析。如果不帶有

當 SQL 註解解析出來 SqlSource 不爲空,還會進行進一步的解析,例如 <SelectKey>、<ResultMap> 等標籤的解析。

最後,將所有解析的結果封裝成 MappedStatement 對象並註冊到 Configuration 中。

到這裏,我們可以發現,雖然指定 Mapper 接口全路徑來解析和加載 Mapper 接口只是簡單地調用了 Configuration#addMaper 方法,裏面卻做了很多的操作,包括 XML 文件的解析和加載、SQL 註解的解析和加載。

四、結束語

文章到此已經全部結束!當然了,如果你讀完此文章還是處於半知不解的狀態,那麼是非常正常的現象,畢竟開源框架的源碼解讀也不可能簡單通過一篇文章就能徹底理解,而且我也承認自己的功力還非常的淺,無法更通俗易懂地給大家介紹~

所以說,我非常建議大家跟着文章的解讀思路,自己去一步一步地探索下去。

當然了,如果大家自己在探索的時候發現我這裏有啥分析不對的地方,歡迎評論,一起學習~

參考資料:《MyBatis3源碼深度解析》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章