手把手帶你閱讀Mybatis源碼(一)構造篇

前言

今天會給你們分享咱們經常使用的持久層框架——MyBatis的工做原理和源碼解析,後續會圍繞Mybatis框架作一些比較深刻的講解,以後這部份內容會歸置到公衆號菜單欄:連載中…-框架分析中,歡迎探討!html

說實話MyBatis是我第一個接觸的持久層框架,在這以前我也沒有用過Hibernate,從Java原生的Jdbc操做數據庫以後就直接過渡到了這個框架上,當時給個人第一感受是,有一個框架太方便了。java

舉一個例子吧,咱們在Jdbc操做的時候,對於對象的封裝,咱們是須要經過ResultSet.getXXX(index)來獲取值,而後在經過對象的setXXX()方法進行手動注入,這種重複且無任何技術含量的工做一直以來都是被咱們程序猿所鄙視的一環,而MyBatis就能夠直接將咱們的SQL查詢出來的數據與對象直接進行映射而後直接返回一個封裝完成的對象,這節省了程序猿大部分的時間,固然其實JdbcTemplate也能夠作到,可是這裏先不說。node

MyBatis的優勢有很是多,固然這也只有同時使用過Jdbc和MyBatis以後,產生對比,纔會有這種巨大的落差感,但這並非今天要討論的重點,今天的重心仍是放在MyBatis是如何作到這些的。mysql

對於MyBatis,給我我的的感覺,其工做流程實際上分爲兩部分:第一,構建,也就是解析咱們寫的xml配置,將其變成它所須要的對象。第二,就是執行,在構建完成的基礎上,去執行咱們的SQL,完成與Jdbc的交互。而這篇的重點會先放在構建上。spring

Xml配置文件

玩過這個框架的同窗都知道,咱們在單獨使用它的時候,會須要兩個配置文件,分別是mybatis-config.xml和mapper.xml,在官網上能夠直接看到,固然這裏爲了方便,我就直接將個人xml配置複製一份。sql

<!-- mybatis-config.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!-- 和spring整合後 environments配置將廢除 -->
    <environments default="development">
        <environment id="development">
            <!-- 使用jdbc事務管理 -->
            <transactionManager type="JDBC" />
            <!-- 數據庫鏈接池 -->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver" />
                <property name="url"
                          value="jdbc:mysql://xxxxxxx:3306/test?characterEncoding=utf8"/>
                <property name="username" value="username" />
                <property name="password" value="password" />
            </dataSource>
        </environment>
    </environments>

    <!-- 加載mapper.xml -->
     <mappers>
         <!-- <package name=""> -->
         <mapper resource="mapper/DemoMapper.xml"  ></mapper>
     </mappers>
</configuration>

 

<!-- DemoMapper.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper  namespace="com.DemoMapper">
    <select  id="queryTest"   parameterType="Map" resultType="Map">
        select * from test WHERE id =#{id}
    </select>
</mapper>

咱們不難看出,在mybatis-config.xml這個文件主要是用於配置數據源、配置別名、加載mapper.xml,而且咱們能夠看到這個文件的<mappers>節點中包含了一個<mapper>,而這個mapper所指向的路徑就是另一個xml文件:DemoMapper.xml,而這個文件中寫了咱們查詢數據庫所用的SQL。數據庫

而,MyBatis實際上就是將這兩個xml文件,解析成配置對象,在執行中去使用它。express

解析

MyBatis須要什麼配置對象?

雖然在這裏咱們並無進行源碼的閱讀,可是做爲一個程序猿,咱們能夠憑藉平常的開發經驗作出一個假設。假設來源於問題,那麼問題就是:爲何要將配置和SQL語句分爲兩個配置文件而不是直接寫在一塊兒?緩存

是否是就意味着,這兩個配置文件會被MyBatis分開解析成兩個不一樣的Java對象?mybatis

不妨先將問題擱置,進行源碼的閱讀。

環境搭建

首先咱們能夠寫一個最基本的使用MyBatis的代碼,我這裏已經寫好了。

public static void main(String[] args) throws Exception {
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    //建立SqlSessionFacory
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    /******************************分割線******************************/
    SqlSession sqlSession = sqlSessionFactory.openSession();
    //獲取Mapper
    DemoMapper mapper = sqlSession.getMapper(DemoMapper.class);
    Map<String,Object> map = new HashMap<>();
    map.put("id","123");
    System.out.println(mapper.selectAll(map));
    sqlSession.close();
    sqlSession.commit();
  }

看源碼重要的一點就是要找到源碼的入口,而咱們能夠從這幾行程序出發,來看看構建到底是在哪開始的。

首先不難看出,這段程序顯示經過字節流讀取了mybatis-config.xml文件,而後經過SqlSessionFactoryBuilder.build()方法,建立了一個SqlSessionFactory(這裏用到了工廠模式和構建者模式),前面說過,MyBatis就是經過咱們寫的xml配置文件,來構建配置對象的,那麼配置文件所在的地方,就必定是構建開始的地方,也就是build方法。

構建開始

進入build方法,咱們能夠看到這裏的確有解析的意思,這個方法返回了一個SqlSessionFactory,而這個對象也是使用構造者模式建立的,不妨繼續往下走。

  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      //解析mybatis-config.xml
      //XMLConfigBuilder  構造者
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      //parse(): 解析mybatis-config.xml裏面的節點
      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.
      }
    }
  }

進入parse():

public Configuration parse() {
    //查看該文件是否已經解析過
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    //若是沒有解析過,則繼續往下解析,而且將標識符置爲true
    parsed = true;
    //解析<configuration>節點
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

注意parse的返回值,Configuration,這個似曾相識的單詞好像在哪見過,是否與mybatis-config.xml中的<configuration>節點有所關聯呢?

答案是確定的,咱們能夠接着往下看。

看到這裏,雖然代碼量還不是特別多,可是至少如今咱們能夠在大腦中獲得一個大體的主線圖,也以下圖所示:

手把手帶你閱讀Mybatis源碼(一)構造篇

沿着這條主線,咱們進入parseConfiguration(XNode)方法,接着往下看。

 private void parseConfiguration(XNode root) {
    try {
      //解析<Configuration>下的節點
      //issue #117 read properties first
      //<properties>
      propertiesElement(root.evalNode("properties"));
      //<settings>
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(settings);
      //別名<typeAliases>解析
      // 所謂別名 其實就是把你指定的別名對應的class存儲在一個Map當中
      typeAliasesElement(root.evalNode("typeAliases"));
      //插件 <plugins>
      pluginElement(root.evalNode("plugins"));
      //自定義實例化對象的行爲<objectFactory>
      objectFactoryElement(root.evalNode("objectFactory"));
      //MateObject   方便反射操做實體類的對象
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      //<environments>
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      // typeHandlers
      typeHandlerElement(root.evalNode("typeHandlers"));
      //主要 <mappers> 指向咱們存放SQL的xxxxMapper.xml文件
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

能夠看到這個方法已經在解析<configuration>下的節點了,例如<settings>,<typeAliases>,<environments><mappers>

這裏主要使用了分步構建,每一個解析不一樣標籤的方法內部都對Configuration對象進行了set或者其它相似的操做,通過這些操做以後,一個Configuration對象就構建完畢了,這裏因爲代碼量比較大,並且大多數構建都是些細節,大概知道怎麼用就能夠了,就不在文章中說明了,我會挑一個主要的說,固然有興趣的同窗能夠本身去pull MyBatis的源碼看看。

Mappers

上文中提到,mybatis-config.xml文件中咱們必定會寫一個叫作<mappers>的標籤,這個標籤中的<mapper>節點存放了咱們對數據庫進行操做的SQL語句,因此這個標籤的構建會做爲今天分析的重點。

首先在看源碼以前,咱們先回憶一下咱們在mapper標籤內一般會怎樣進行配置,一般有以下幾種配置方式。

<mappers>
    <!-- 經過配置文件路徑 -->
  <mapper resource="mapper/DemoMapper.xml" ></mapper>
    <!-- 經過Java全限定類名 -->
  <mapper class="com.mybatistest.TestMapper"/>
   <!-- 經過url 一般是mapper不在本地時用 -->
  <mapper url=""/>
    <!-- 經過包名 -->
  <package name="com.mybatistest"/>
    <!-- 注意 mapper節點中,可使用resource/url/class三種方式獲取mapper-->
</mappers>

這是<mappers>標籤的幾種配置方式,經過這幾種配置方式,能夠幫助咱們更容易理解mappers的解析。

private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
      //遍歷解析mappers下的節點
      for (XNode child : parent.getChildren()) {
      //首先解析package節點
      if ("package".equals(child.getName())) {
        //獲取包名
        String mapperPackage = child.getStringAttribute("name");
        configuration.addMappers(mapperPackage);
      } else {
        //若是不存在package節點,那麼掃描mapper節點
        //resource/url/mapperClass三個值只能有一個值是有值的
        String resource = child.getStringAttribute("resource");
        String url = child.getStringAttribute("url");
        String mapperClass = child.getStringAttribute("class");
        //優先級 resource>url>mapperClass
        if (resource != null && url == null && mapperClass == null) {
            //若是mapper節點中的resource不爲空
          ErrorContext.instance().resource(resource);
           //那麼直接加載resource指向的XXXMapper.xml文件爲字節流
          InputStream inputStream = Resources.getResourceAsStream(resource);
          //經過XMLMapperBuilder解析XXXMapper.xml,能夠看到這裏構建的XMLMapperBuilde還傳入了configuration,因此以後確定是會將mapper封裝到configuration對象中去的。
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
          //解析
          mapperParser.parse();
        } else if (resource == null && url != null && mapperClass == null) {
          //若是url!=null,那麼經過url解析
          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) {
            //若是mapperClass!=null,那麼經過加載類構造Configuration
          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.");
        }
      }
    }
  }
}

手把手帶你閱讀Mybatis源碼(一)構造篇

咱們的配置文件中寫的是經過resource來加載mapper.xml的,因此會經過XMLMapperBuilder來進行解析,咱們能夠進去他的parse方法中看一下:

public void parse() {
    //判斷文件是否以前解析過
    if (!configuration.isResourceLoaded(resource)) {
        //解析mapper文件節點(主要)(下面貼了代碼)
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      //綁定Namespace裏面的Class對象
      bindMapperForNamespace();
    }
    //從新解析以前解析不了的節點,先不看,最後填坑。
    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }


//解析mapper文件裏面的節點
// 拿到裏面配置的配置項 最終封裝成一個MapperedStatemanet
private void configurationElement(XNode context) {
  try {
      //獲取命名空間 namespace,這個很重要,後期mybatis會經過這個動態代理咱們的Mapper接口
    String namespace = context.getStringAttribute("namespace");
    if (namespace == null || namespace.equals("")) {
        //若是namespace爲空則拋一個異常
      throw new BuilderException("Mapper's namespace cannot be empty");
    }
    builderAssistant.setCurrentNamespace(namespace);
    //解析緩存節點
    cacheRefElement(context.evalNode("cache-ref"));
    cacheElement(context.evalNode("cache"));

    //解析parameterMap(過期)和resultMap  <resultMap></resultMap>
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    resultMapElements(context.evalNodes("/mapper/resultMap"));
    //解析<sql>節點 
    //<sql id="staticSql">select * from test</sql> (可重用的代碼段)
    //<select> <include refid="staticSql"></select>
    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);
  }
}

在這個parse()方法中,調用了一個configuationElement代碼,用於解析XXXMapper.xml文件中的各類節點,包括<cache><cache-ref><paramaterMap>(已過期)、<resultMap><sql>、還有增刪改查節點,和上面相同的是,咱們也挑一個主要的來講,由於解析過程都大同小異。

毋庸置疑的是,咱們在XXXMapper.xml中必不可少的就是編寫SQL,與數據庫交互主要靠的也就是這個,因此着重說說解析增刪改查節點的方法——buildStatementFromContext()。

在沒貼代碼以前,根據這個名字就能夠略知一二了,這個方法會根據咱們的增刪改查節點,來構造一個Statement,而用過原生Jdbc的都知道,Statement就是咱們操做數據庫的對象。

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

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
    final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
    try {
      //解析xml節點
      statementParser.parseStatementNode();
    } catch (IncompleteElementException e) {
      //xml語句有問題時 存儲到集合中 等解析完能解析的再從新解析
      configuration.addIncompleteStatement(statementParser);
    }
  }
}


public void parseStatementNode() {
    //獲取<select id="xxx">中的id
    String id = context.getStringAttribute("id");
    //獲取databaseId 用於多數據庫,這裏爲null
    String databaseId = context.getStringAttribute("databaseId");

    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }
    //獲取節點名  select update delete insert
    String nodeName = context.getNode().getNodeName();
    //根據節點名,獲得SQL操做的類型
    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);
    //是否須要處理嵌套查詢結果 group by

    // 三組數據 分紅一個嵌套的查詢結果
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // Include Fragments before parsing
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    //替換Includes標籤爲對應的sql標籤裏面的值
    includeParser.applyIncludes(context.getNode());

    //獲取parameterType名
    String parameterType = context.getStringAttribute("parameterType");
    //獲取parameterType的Class
    Class<?> parameterTypeClass = resolveClass(parameterType);

    //解析配置的自定義腳本語言驅動 這裏爲null
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    // Parse selectKey after includes and remove them.
    //解析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;
    }
/************************************************************************************/
    //解析Sql(重要)  根據sql文原本判斷是否須要動態解析 若是沒有動態sql語句且 只有#{}的時候 直接靜態解析使用?佔位 當有 ${} 不解析
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    //獲取StatementType,能夠理解爲Statement和PreparedStatement
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    //沒用過
    Integer fetchSize = context.getIntAttribute("fetchSize");
    //超時時間
    Integer timeout = context.getIntAttribute("timeout");
    //已過期
    String parameterMap = context.getStringAttribute("parameterMap");
    //獲取返回值類型名
    String resultType = context.getStringAttribute("resultType");
    //獲取返回值烈性的Class
    Class<?> resultTypeClass = resolveClass(resultType);
    //獲取resultMap的id
    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");

    //將剛纔獲取到的屬性,封裝成MappedStatement對象(代碼貼在下面)
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

//將剛纔獲取到的屬性,封裝成MappedStatement對象
  public MappedStatement addMappedStatement(
      String id,
      SqlSource sqlSource,
      StatementType statementType,
      SqlCommandType sqlCommandType,
      Integer fetchSize,
      Integer timeout,
      String parameterMap,
      Class<?> parameterType,
      String resultMap,
      Class<?> resultType,
      ResultSetType resultSetType,
      boolean flushCache,
      boolean useCache,
      boolean resultOrdered,
      KeyGenerator keyGenerator,
      String keyProperty,
      String keyColumn,
      String databaseId,
      LanguageDriver lang,
      String resultSets) {

    if (unresolvedCacheRef) {
      throw new IncompleteElementException("Cache-ref not yet resolved");
    }

    //id = namespace
    id = applyCurrentNamespace(id, false);
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

      //經過構造者模式+鏈式變成,構造一個MappedStatement的構造者
    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
        .resource(resource)
        .fetchSize(fetchSize)
        .timeout(timeout)
        .statementType(statementType)
        .keyGenerator(keyGenerator)
        .keyProperty(keyProperty)
        .keyColumn(keyColumn)
        .databaseId(databaseId)
        .lang(lang)
        .resultOrdered(resultOrdered)
        .resultSets(resultSets)
        .resultMaps(getStatementResultMaps(resultMap, resultType, id))
        .resultSetType(resultSetType)
        .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
        .useCache(valueOrDefault(useCache, isSelect))
        .cache(currentCache);

    ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
    if (statementParameterMap != null) {
      statementBuilder.parameterMap(statementParameterMap);
    }

      //經過構造者構造MappedStatement
    MappedStatement statement = statementBuilder.build();
     //將MappedStatement對象封裝到Configuration對象中
    configuration.addMappedStatement(statement);
    return statement;
  }

這個代碼段雖然很長,可是一句話形容它就是繁瑣但不復雜,裏面主要也就是對xml的節點進行解析。舉個比上面簡單的例子吧,假設咱們有這樣一段配置:

<select id="selectDemo" parameterType="java.lang.Integer" resultType='Map'>
    SELECT * FROM test
</select>

MyBatis須要作的就是,先判斷這個節點是用來幹什麼的,而後再獲取這個節點的id、parameterType、resultType等屬性,封裝成一個MappedStatement對象,因爲這個對象很複雜,因此MyBatis使用了構造者模式來構造這個對象,最後當MappedStatement對象構造完成後,將其封裝到Configuration對象中。

代碼執行至此,基本就結束了對Configuration對象的構建,MyBatis的第一階段:構造,也就到這裏結束了,如今再來回答咱們在文章開頭提出的那兩個問題:MyBatis須要構造什麼對象?以及是否兩個配置文件對應着兩個對象?,彷佛就已經有了答案,這裏作一個總結:

MyBatis須要對配置文件進行解析,最終會解析成一個Configuration對象,可是要說兩個配置文件對應了兩個對象實際上也沒有錯:

  • Configuration對象,保存了mybatis-config.xml的配置信息。

  • MappedStatement,保存了XXXMapper.xml的配置信息。

可是最終MappedStatement對象會封裝到Configuration對象中,合二爲一,成爲一個單獨的對象,也就是Configuration。

最後給你們畫一個構建過程的流程圖:

手把手帶你閱讀Mybatis源碼(一)構造篇

填坑

SQL語句在哪解析?

細心的同窗可能已經發現了,上文中只說了去節點中獲取一些屬性從而構建配置對象,可是最重要的SQL語句並無提到,這是由於這部分我想要和屬性區分開單獨說,因爲MyBatis支持動態SQL和${}#{}的多樣的SQL,因此這裏單獨提出來講會比較合適。

首先能夠確認的是,剛纔咱們走完的那一整個流程中,包含了SQL語句的生成,下面貼代碼(這一段代碼至關繞,很差讀)。

//解析Sql(重要)  根據sql文原本判斷是否須要動態解析 若是沒有動態sql語句且 只有#{}的時候 直接靜態解析使用?佔位 當有 ${} 不解析
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

這裏就是生成Sql的入口,以單步調試的角度接着往下看。

/*進入createSqlSource方法*/
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    //進入這個構造
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    //進入parseScriptNode
    return builder.parseScriptNode();
}
/**
進入這個方法
*/
public SqlSource parseScriptNode() {
    //#
    //會先解析一遍
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource;
    if (isDynamic) {
      //若是是${}會直接不解析,等待執行的時候直接賦值
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      //用佔位符方式來解析  #{} --> ?
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
}
protected MixedSqlNode parseDynamicTags(XNode node) {
    List<SqlNode> contents = new ArrayList<>();
    //獲取select標籤下的子標籤
    NodeList children = node.getNode().getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
      XNode child = node.newXNode(children.item(i));
      if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
          //若是是查詢
        //獲取原生SQL語句 這裏是 select * from test where id = #{id}
        String data = child.getStringBody("");
        TextSqlNode textSqlNode = new TextSqlNode(data);
        //檢查sql是不是${}
        if (textSqlNode.isDynamic()) {
            //若是是${}那麼直接不解析
          contents.add(textSqlNode);
          isDynamic = true;
        } else {
            //若是不是,則直接生成靜態SQL
            //#{} -> ?
          contents.add(new StaticTextSqlNode(data));
        }
      } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
          //若是是增刪改
        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);
  }

 

/*從上面的代碼段到這一段中間須要通過不少代碼,就不一段一段貼了*/
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    //這裏會生成一個GenericTokenParser,傳入#{}做爲開始和結束,而後調用其parse方法,便可將#{}換爲 ?
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    //這裏能夠解析#{} 將其替換爲?
    String sql = parser.parse(originalSql);
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

//通過一段複雜的解析過程
public String parse(String text) {
    if (text == null || text.isEmpty()) {
      return "";
    }
    // search open token
    int start = text.indexOf(openToken);
    if (start == -1) {
      return text;
    }
    char[] src = text.toCharArray();
    int offset = 0;
    final StringBuilder builder = new StringBuilder();
    StringBuilder expression = null;
    //遍歷裏面全部的#{} select ?  ,#{id1} ${}
    while (start > -1) {
      if (start > 0 && src[start - 1] == '\') {
        // this open token is escaped. remove the backslash and continue.
        builder.append(src, offset, start - offset - 1).append(openToken);
        offset = start + openToken.length();
      } else {
        // found open token. let's search close token.
        if (expression == null) {
          expression = new StringBuilder();
        } else {
          expression.setLength(0);
        }
        builder.append(src, offset, start - offset);
        offset = start + openToken.length();
        int end = text.indexOf(closeToken, offset);
        while (end > -1) {
          if (end > offset && src[end - 1] == '\') {
            // this close token is escaped. remove the backslash and continue.
            expression.append(src, offset, end - offset - 1).append(closeToken);
            offset = end + closeToken.length();
            end = text.indexOf(closeToken, offset);
          } else {
            expression.append(src, offset, end - offset);
            break;
          }
        }
        if (end == -1) {
          // close token was not found.
          builder.append(src, start, src.length - start);
          offset = src.length;
        } else {
            //使用佔位符 ?
            //注意handler.handleToken()方法,這個方法是核心
          builder.append(handler.handleToken(expression.toString()));
          offset = end + closeToken.length();
        }
      }
      start = text.indexOf(openToken, offset);
    }
    if (offset < src.length) {
      builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
}

//BindingTokenParser 的handleToken
//當掃描到${}的時候調用此方法  其實就是不解析 在運行時候在替換成具體的值
@Override
public String handleToken(String content) {
  this.isDynamic = true;
  return null;
}
//ParameterMappingTokenHandler的handleToken
//全局掃描#{id} 字符串以後  會把裏面全部 #{} 調用handleToken 替換爲?
@Override
public String handleToken(String content) {
      parameterMappings.add(buildParameterMapping(content));
      return "?";
}

這段代碼至關繞,咱們應該站在一個宏觀的角度去看待它。因此我直接在這裏歸納一下:

首先這裏會經過<select>節點獲取到咱們的SQL語句,假設SQL語句中只有${},那麼直接就什麼都不作,在運行的時候直接進行賦值。

而若是掃描到了#{}字符串以後,會進行替換,將#{}替換爲 ?

那麼他是怎麼進行判斷的呢?

這裏會生成一個GenericTokenParser,這個對象能夠傳入一個openToken和closeToken,若是是#{},那麼openToken就是#{,closeToken就是 },而後經過parse方法中的handler.handleToken()方法進行替換。

在這以前因爲已經進行過SQL是否含有#{}的判斷了,因此在這裏若是是隻有${},那麼handler就是BindingTokenParser的實例化對象,若是存在#{},那麼handler就是ParameterMappingTokenHandler的實例化對象。

分別進行處理。

上文中提到的解析不了的節點是什麼意思?

根據上文的代碼咱們可知,解析Mapper.xml文件中的每一個節點是有順序的。

那麼假設我寫了這麼一個幾個節點:

<select id="demoselect" paramterType='java.lang.Integer' resultMap='demoResultMap'>
</select>
<resultMap id="demoResultMap" type="demo">
    <id column property>
    <result coulmn property>
</resultMap>

select節點是須要獲取resultMap的,可是此時resultMap並無被解析到,因此解析到<select>這個節點的時候是沒法獲取到resultMap的信息的。

咱們來看看MyBatis是怎麼作的:

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
    final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
    try {
      //解析xml節點
      statementParser.parseStatementNode();
    } catch (IncompleteElementException e) {
      //xml語句有問題時 存儲到集合中 等解析完能解析的再從新解析
      configuration.addIncompleteStatement(statementParser);
    }
  }
}

當解析到某個節點出現問題的時候,會拋一個異常,而後會調用configuration的addIncompleteStatement方法,將這個解析對象先暫存到這個集合中,等到全部的節點都解析完畢以後,在對這個集合內的解析對象繼續解析:

public void parse() {
      //判斷文件是否以前解析過
    if (!configuration.isResourceLoaded(resource)) {
        //解析mapper文件
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      //綁定Namespace裏面的Class對象
      bindMapperForNamespace();
    }

    //從新解析以前解析不了的節點
    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
}
private void parsePendingResultMaps() {
    Collection<ResultMapResolver> incompleteResultMaps = configuration.getIncompleteResultMaps();
    synchronized (incompleteResultMaps) {
      Iterator<ResultMapResolver> iter = incompleteResultMaps.iterator();
      while (iter.hasNext()) {
        try {
            //添加resultMap
          iter.next().resolve();
          iter.remove();
        } catch (IncompleteElementException e) {
          // ResultMap is still missing a resource...
        }
      }
    }
}
public ResultMap resolve() {
    //添加resultMap
    return assistant.addResultMap(this.id, this.type, this.extend, this.discriminator, this.resultMappings, this.autoMapping);
  }

結語

至此整個MyBatis的查詢前構建的過程就基本說完了,簡單地總結就是,MyBatis會在執行查詢以前,對配置文件進行解析成配置對象:Configuration,以便在後面執行的時候去使用,而存放SQL的xml又會解析成MappedStatement對象,可是最終這個對象也會加入Configuration中。

至於Configuration是如何被使用的,以及SQL的執行部分,我會在下一篇說SQL執行的時候分享。

原文出處:https://www.cnblogs.com/javazhiyin/p/12340498.html

相關文章
相關標籤/搜索