拋開 Spring ,你知道 MyBatis 加載 Mapper 的底層原理嗎?


原文鏈接:拋開 Spring ,你知道 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方式解析。如果不帶有 <script> 注解,則解析 SQL 配置中的全局變量並返回 SqlSource。

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

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

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

四、結束語

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

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

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

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


免責聲明!

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



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