MyBatis源碼解析(一)——構(gòu)造篇

前言

好久不見(jiàn),從上一篇文章過(guò)后将饺,休整了兩個(gè)月,又逢疫情特殊時(shí)期痛黎,天天宅在家里挺尸予弧,真是見(jiàn)證了一個(gè)人可以懶惰到什么境界。好吧廢話不多說(shuō)了湖饱,今天會(huì)給大家分享我們常用的持久層框架——MyBatis的工作原理和源碼解析掖蛤。

說(shuō)實(shí)話MyBatis是我第一個(gè)接觸的持久層框架,在這之前我也沒(méi)有用過(guò)Hibernate井厌,從Java原生的Jdbc操作數(shù)據(jù)庫(kù)之后就直接過(guò)渡到了這個(gè)框架上蚓庭,當(dāng)時(shí)給我的第一感覺(jué)是,有一個(gè)框架太方便了仅仆,舉一個(gè)例子吧器赞,我們?cè)贘dbc操作的時(shí)候,對(duì)于對(duì)象的封裝墓拜,我們是需要通過(guò)ResultSet.getXXX(index)來(lái)獲取值港柜,然后在通過(guò)對(duì)象的setXXX()方法進(jìn)行手動(dòng)注入,這種重復(fù)且無(wú)任何技術(shù)含量的工作一直以來(lái)都是被我們程序猿所鄙視的一環(huán),而MyBatis就可以直接將我們的SQL查詢出來(lái)的數(shù)據(jù)與對(duì)象直接進(jìn)行映射然后直接返回一個(gè)封裝完成的對(duì)象夏醉,這節(jié)省了程序猿大部分的時(shí)間爽锥,當(dāng)然其實(shí)JdbcTemplate也可以做到,但是這里先不說(shuō)畔柔。MyBatis的優(yōu)點(diǎn)有非常多氯夷,當(dāng)然這也只有同時(shí)使用過(guò)Jdbc和MyBatis之后,產(chǎn)生對(duì)比靶擦,才會(huì)有這種巨大的落差感腮考,但這并不是今天要討論的重點(diǎn),今天的重心還是放在MyBatis是如何做到這些的奢啥。

對(duì)于MyBatis秸仙,給我個(gè)人的感受,其工作流程實(shí)際上分為兩部分:第一桩盲,構(gòu)建寂纪,也就是解析我們寫的xml配置,將其變成它所需要的對(duì)象赌结。第二捞蛋,就是執(zhí)行,在構(gòu)建完成的基礎(chǔ)上柬姚,去執(zhí)行我們的SQL拟杉,完成與Jdbc的交互。而這篇的重點(diǎn)會(huì)先放在構(gòu)建上量承。

Xml配置文件

玩過(guò)這個(gè)框架的同學(xué)都知道搬设,我們?cè)趩为?dú)使用它的時(shí)候,會(huì)需要兩個(gè)配置文件撕捍,分別是mybatis-config.xml和mapper.xml拿穴,在官網(wǎng)上可以直接看到,當(dāng)然這里為了方便忧风,我就直接將我的xml配置復(fù)制一份默色。

<!-- 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事務(wù)管理 -->
            <transactionManager type="JDBC" />
            <!-- 數(shù)據(jù)庫(kù)連接池 -->
            <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這個(gè)文件主要是用于配置數(shù)據(jù)源狮腿、配置別名腿宰、加載mapper.xml,并且我們可以看到這個(gè)文件的<mappers>節(jié)點(diǎn)中包含了一個(gè)<mapper>缘厢,而這個(gè)mapper所指向的路徑就是另外一個(gè)xml文件:DemoMapper.xml吃度,而這個(gè)文件中寫了我們查詢數(shù)據(jù)庫(kù)所用的SQL。

而昧绣,MyBatis實(shí)際上就是將這兩個(gè)xml文件规肴,解析成配置對(duì)象,在執(zhí)行中去使用它

解析

  • MyBatis需要什么配置對(duì)象拖刃?

    雖然在這里我們并沒(méi)有進(jìn)行源碼的閱讀删壮,但是作為一個(gè)程序猿,我們可以憑借日常的開發(fā)經(jīng)驗(yàn)做出一個(gè)假設(shè)兑牡。假設(shè)來(lái)源于問(wèn)題其监,那么問(wèn)題就是:為什么要將配置和SQL語(yǔ)句分為兩個(gè)配置文件而不是直接寫在一起铅辞?

    是不是就意味著,這兩個(gè)配置文件會(huì)被MyBatis分開解析成兩個(gè)不同的Java對(duì)象?

    不妨先將問(wèn)題擱置阅酪,進(jìn)行源碼的閱讀晶通。

  • 環(huán)境搭建

    首先我們可以寫一個(gè)最基本的使用MyBatis的代碼级野,我這里已經(jīng)寫好了棒厘。

    public static void main(String[] args) throws Exception {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        //創(chuàng)建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();
      }
    

    看源碼重要的一點(diǎn)就是要找到源碼的入口,而我們可以從這幾行程序出發(fā)如迟,來(lái)看看構(gòu)建究竟是在哪開始的收毫。

    首先不難看出,這段程序顯示通過(guò)字節(jié)流讀取了mybatis-config.xml文件殷勘,然后通過(guò)SqlSessionFactoryBuilder.build()方法此再,創(chuàng)建了一個(gè)SqlSessionFactory(這里用到了工廠模式和構(gòu)建者模式),前面說(shuō)過(guò)玲销,MyBatis就是通過(guò)我們寫的xml配置文件输拇,來(lái)構(gòu)建配置對(duì)象的,那么配置文件所在的地方贤斜,就一定是構(gòu)建開始的地方策吠,也就是build方法。

  • 構(gòu)建開始

    進(jìn)入build方法瘩绒,我們可以看到這里的確有解析的意思,這個(gè)方法返回了一個(gè)SqlSessionFactory奴曙,而這個(gè)對(duì)象也是使用構(gòu)造者模式創(chuàng)建的,不妨繼續(xù)往下走草讶。

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

    進(jìn)入parse():

    public Configuration parse() {
      //查看該文件是否已經(jīng)解析過(guò)
        if (parsed) {
          throw new BuilderException("Each XMLConfigBuilder can only be used once.");
        }
        //如果沒(méi)有解析過(guò),則繼續(xù)往下解析炉菲,并且將標(biāo)識(shí)符置為true
        parsed = true;
        //解析<configuration>節(jié)點(diǎn)
        parseConfiguration(parser.evalNode("/configuration"));
        return configuration;
      }
    

    注意parse的返回值堕战,Configuration,這個(gè)似曾相識(shí)的單詞好像在哪見(jiàn)過(guò)拍霜,是否與mybatis-config.xml中的<configuration>節(jié)點(diǎn)有所關(guān)聯(lián)呢嘱丢?

    答案是肯定的,我們可以接著往下看祠饺。

    看到這里越驻,雖然代碼量還不是特別多,但是至少現(xiàn)在我們可以在大腦中得到一個(gè)大致的主線圖,也如下圖所示:

    大致構(gòu)建圖

    沿著這條主線缀旁,我們進(jìn)入parseConfiguration(XNode)方法记劈,接著往下看。

     private void parseConfiguration(XNode root) {
        try {
          //解析<Configuration>下的節(jié)點(diǎn)
          //issue #117 read properties first
          //<properties>
          propertiesElement(root.evalNode("properties"));
          //<settings>
          Properties settings = settingsAsProperties(root.evalNode("settings"));
          loadCustomVfs(settings);
          loadCustomLogImpl(settings);
          //別名<typeAliases>解析
          // 所謂別名 其實(shí)就是把你指定的別名對(duì)應(yīng)的class存儲(chǔ)在一個(gè)Map當(dāng)中
          typeAliasesElement(root.evalNode("typeAliases"));
          //插件 <plugins>
          pluginElement(root.evalNode("plugins"));
          //自定義實(shí)例化對(duì)象的行為<objectFactory>
          objectFactoryElement(root.evalNode("objectFactory"));
          //MateObject   方便反射操作實(shí)體類的對(duì)象
          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);
        }
      }
    

    可以看到這個(gè)方法已經(jīng)在解析<configuration>下的節(jié)點(diǎn)了并巍,例如<settings>,<typeAliases>,<environments><mappers>目木。

    這里主要使用了分步構(gòu)建每個(gè)解析不同標(biāo)簽的方法內(nèi)部都對(duì)Configuration對(duì)象進(jìn)行了set或者其它類似的操作懊渡,經(jīng)過(guò)這些操作之后刽射,一個(gè)Configuration對(duì)象就構(gòu)建完畢了,這里由于代碼量比較大剃执,而且大多數(shù)構(gòu)建都是些細(xì)節(jié)誓禁,大概知道怎么用就可以了,就不在文章中說(shuō)明了肾档,我會(huì)挑一個(gè)主要的說(shuō)摹恰,當(dāng)然有興趣的同學(xué)可以自己去pull MyBatis的源碼看看。

  • Mappers

    上文中提到阁最,mybatis-config.xml文件中我們一定會(huì)寫一個(gè)叫做<mappers>的標(biāo)簽戒祠,這個(gè)標(biāo)簽中的<mapper>節(jié)點(diǎn)存放了我們對(duì)數(shù)據(jù)庫(kù)進(jìn)行操作的SQL語(yǔ)句,所以這個(gè)標(biāo)簽的構(gòu)建會(huì)作為今天分析的重點(diǎn)速种。

    首先在看源碼之前姜盈,我們先回憶一下我們?cè)趍apper標(biāo)簽內(nèi)通常會(huì)怎樣進(jìn)行配置,通常有如下幾種配置方式配阵。

    <mappers>
        <!-- 通過(guò)配置文件路徑 -->
      <mapper resource="mapper/DemoMapper.xml" ></mapper>
        <!-- 通過(guò)Java全限定類名 -->
      <mapper class="com.mybatistest.TestMapper"/>
       <!-- 通過(guò)url 通常是mapper不在本地時(shí)用 -->
      <mapper url=""/>
        <!-- 通過(guò)包名 -->
      <package name="com.mybatistest"/>
        <!-- 注意 mapper節(jié)點(diǎn)中馏颂,可以使用resource/url/class三種方式獲取mapper-->
    </mappers>
    

    這是<mappers>標(biāo)簽的幾種配置方式,通過(guò)這幾種配置方式棋傍,可以幫助我們更容易理解mappers的解析救拉。

    private void mapperElement(XNode parent) throws Exception {
      if (parent != null) {
          //遍歷解析mappers下的節(jié)點(diǎn)
          for (XNode child : parent.getChildren()) {
          //首先解析package節(jié)點(diǎn)
          if ("package".equals(child.getName())) {
            //獲取包名
            String mapperPackage = child.getStringAttribute("name");
            configuration.addMappers(mapperPackage);
          } else {
            //如果不存在package節(jié)點(diǎn),那么掃描mapper節(jié)點(diǎn)
            //resource/url/mapperClass三個(gè)值只能有一個(gè)值是有值的
            String resource = child.getStringAttribute("resource");
            String url = child.getStringAttribute("url");
            String mapperClass = child.getStringAttribute("class");
            //優(yōu)先級(jí) resource>url>mapperClass
            if (resource != null && url == null && mapperClass == null) {
                //如果mapper節(jié)點(diǎn)中的resource不為空
              ErrorContext.instance().resource(resource);
               //那么直接加載resource指向的XXXMapper.xml文件為字節(jié)流
              InputStream inputStream = Resources.getResourceAsStream(resource);
              //通過(guò)XMLMapperBuilder解析XXXMapper.xml瘫拣,可以看到這里構(gòu)建的XMLMapperBuilde還傳入了configuration,所以之后肯定是會(huì)將mapper封裝到configuration對(duì)象中去的亿絮。
              XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
              //解析
              mapperParser.parse();
            } else if (resource == null && url != null && mapperClass == null) {
              //如果url!=null,那么通過(guò)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麸拄,那么通過(guò)加載類構(gòu)造Configuration
              Class<?> mapperInterface = Resources.classForName(mapperClass);
              configuration.addMapper(mapperInterface);
          } else {
                //如果都不滿足  則直接拋異常  如果配置了兩個(gè)或三個(gè)  直接拋異常
              throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
            }
          }
        }
      }
    }
    
    通過(guò)resource解析

    我們的配置文件中寫的是通過(guò)resource來(lái)加載mapper.xml的派昧,所以會(huì)通過(guò)XMLMapperBuilder來(lái)進(jìn)行解析,我們可以進(jìn)去他的parse方法中看一下:

    public void parse() {
        //判斷文件是否之前解析過(guò)
        if (!configuration.isResourceLoaded(resource)) {
          //解析mapper文件節(jié)點(diǎn)(主要)(下面貼了代碼)
          configurationElement(parser.evalNode("/mapper"));
          configuration.addLoadedResource(resource);
          //綁定Namespace里面的Class對(duì)象
          bindMapperForNamespace();
        }
        //重新解析之前解析不了的節(jié)點(diǎn)拢切,先不看蒂萎,最后填坑。
        parsePendingResultMaps();
        parsePendingCacheRefs();
        parsePendingStatements();
      }
    
    
    //解析mapper文件里面的節(jié)點(diǎn)
    // 拿到里面配置的配置項(xiàng) 最終封裝成一個(gè)MapperedStatemanet
    private void configurationElement(XNode context) {
      try {
          //獲取命名空間 namespace淮椰,這個(gè)很重要五慈,后期mybatis會(huì)通過(guò)這個(gè)動(dòng)態(tài)代理我們的Mapper接口
        String namespace = context.getStringAttribute("namespace");
        if (namespace == null || namespace.equals("")) {
            //如果namespace為空則拋一個(gè)異常
          throw new BuilderException("Mapper's namespace cannot be empty");
        }
        builderAssistant.setCurrentNamespace(namespace);
        //解析緩存節(jié)點(diǎn)
        cacheRefElement(context.evalNode("cache-ref"));
        cacheElement(context.evalNode("cache"));
          
        //解析parameterMap(過(guò)時(shí))和resultMap  <resultMap></resultMap>
        parameterMapElement(context.evalNodes("/mapper/parameterMap"));
        resultMapElements(context.evalNodes("/mapper/resultMap"));
        //解析<sql>節(jié)點(diǎn) 
        //<sql id="staticSql">select * from test</sql> (可重用的代碼段)
        //<select> <include refid="staticSql"></select>
        sqlElement(context.evalNodes("/mapper/sql"));
        //解析增刪改查節(jié)點(diǎn)<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);
      }
    }
    

    在這個(gè)parse()方法中纳寂,調(diào)用了一個(gè)configuationElement代碼,用于解析XXXMapper.xml文件中的各種節(jié)點(diǎn)泻拦,包括<cache>毙芜、<cache-ref><paramaterMap>(已過(guò)時(shí))聪轿、<resultMap>爷肝、<sql>、還有增刪改查節(jié)點(diǎn)陆错,和上面相同的是灯抛,我們也挑一個(gè)主要的來(lái)說(shuō),因?yàn)榻馕鲞^(guò)程都大同小異音瓷。

    毋庸置疑的是对嚼,我們?cè)赬XXMapper.xml中必不可少的就是編寫SQL,與數(shù)據(jù)庫(kù)交互主要靠的也就是這個(gè)绳慎,所以著重說(shuō)說(shuō)解析增刪改查節(jié)點(diǎn)的方法——buildStatementFromContext()纵竖。

    在沒(méi)貼代碼之前,根據(jù)這個(gè)名字就可以略知一二了杏愤,這個(gè)方法會(huì)根據(jù)我們的增刪改查節(jié)點(diǎn)靡砌,來(lái)構(gòu)造一個(gè)Statement,而用過(guò)原生Jdbc的都知道珊楼,Statement就是我們操作數(shù)據(jù)庫(kù)的對(duì)象通殃。

    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節(jié)點(diǎn)
          statementParser.parseStatementNode();
        } catch (IncompleteElementException e) {
          //xml語(yǔ)句有問(wèn)題時(shí) 存儲(chǔ)到集合中 等解析完能解析的再重新解析
          configuration.addIncompleteStatement(statementParser);
        }
      }
    }
    
    
    public void parseStatementNode() {
        //獲取<select id="xxx">中的id
        String id = context.getStringAttribute("id");
        //獲取databaseId 用于多數(shù)據(jù)庫(kù),這里為null
        String databaseId = context.getStringAttribute("databaseId");
    
        if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
          return;
        }
      //獲取節(jié)點(diǎn)名  select update delete insert
        String nodeName = context.getNode().getNodeName();
        //根據(jù)節(jié)點(diǎn)名厕宗,得到SQL操作的類型
        SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
        //判斷是否是查詢
        boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
        //是否刷新緩存 默認(rèn):增刪改刷新 查詢不刷新
        boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
        //是否使用二級(jí)緩存 默認(rèn)值:查詢使用 增刪改不使用
        boolean useCache = context.getBooleanAttribute("useCache", isSelect);
        //是否需要處理嵌套查詢結(jié)果 group by
    
        // 三組數(shù)據(jù) 分成一個(gè)嵌套的查詢結(jié)果
        boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
    
        // Include Fragments before parsing
        XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
        //替換Includes標(biāo)簽為對(duì)應(yīng)的sql標(biāo)簽里面的值
        includeParser.applyIncludes(context.getNode());
    
        //獲取parameterType名
        String parameterType = context.getStringAttribute("parameterType");
        //獲取parameterType的Class
        Class<?> parameterTypeClass = resolveClass(parameterType);
    
        //解析配置的自定義腳本語(yǔ)言驅(qū)動(dòng) 這里為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)
        //設(shè)置主鍵自增規(guī)則
        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(重要)  根據(jù)sql文本來(lái)判斷是否需要?jiǎng)討B(tài)解析 如果沒(méi)有動(dòng)態(tài)sql語(yǔ)句且 只有#{}的時(shí)候 直接靜態(tài)解析使用?占位 當(dāng)有 ${} 不解析
        SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
        //獲取StatementType画舌,可以理解為Statement和PreparedStatement
        StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
        //沒(méi)用過(guò)
        Integer fetchSize = context.getIntAttribute("fetchSize");
        //超時(shí)時(shí)間
        Integer timeout = context.getIntAttribute("timeout");
        //已過(guò)時(shí)
        String parameterMap = context.getStringAttribute("parameterMap");
        //獲取返回值類型名
        String resultType = context.getStringAttribute("resultType");
        //獲取返回值烈性的Class
        Class<?> resultTypeClass = resolveClass(resultType);
        //獲取resultMap的id
        String resultMap = context.getStringAttribute("resultMap");
        //獲取結(jié)果集類型
        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對(duì)象(代碼貼在下面)
        builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
            fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
            resultSetTypeEnum, flushCache, useCache, resultOrdered,
            keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
      }
    
    //將剛才獲取到的屬性已慢,封裝成MappedStatement對(duì)象
      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;
    
          //通過(guò)構(gòu)造者模式+鏈?zhǔn)阶兂汕簦瑯?gòu)造一個(gè)MappedStatement的構(gòu)造者
        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);
        }
    
          //通過(guò)構(gòu)造者構(gòu)造MappedStatement
        MappedStatement statement = statementBuilder.build();
         //將MappedStatement對(duì)象封裝到Configuration對(duì)象中
        configuration.addMappedStatement(statement);
        return statement;
    }
    

    這個(gè)代碼段雖然很長(zhǎng),但是一句話形容它就是繁瑣但不復(fù)雜佑惠,里面主要也就是對(duì)xml的節(jié)點(diǎn)進(jìn)行解析朋腋。舉個(gè)比上面簡(jiǎn)單的例子吧,假設(shè)我們有這樣一段配置:

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

    MyBatis需要做的就是膜楷,先判斷這個(gè)節(jié)點(diǎn)是用來(lái)干什么的乍丈,然后再獲取這個(gè)節(jié)點(diǎn)的id、parameterType把将、resultType等屬性,封裝成一個(gè)MappedStatement對(duì)象忆矛,由于這個(gè)對(duì)象很復(fù)雜察蹲,所以MyBatis使用了構(gòu)造者模式來(lái)構(gòu)造這個(gè)對(duì)象请垛,最后當(dāng)MappedStatement對(duì)象構(gòu)造完成后,將其封裝到Configuration對(duì)象中洽议。

    代碼執(zhí)行至此宗收,基本就結(jié)束了對(duì)Configuration對(duì)象的構(gòu)建,MyBatis的第一階段:構(gòu)造亚兄,也就到這里結(jié)束了混稽,現(xiàn)在再來(lái)回答我們?cè)谖恼麻_頭提出的那兩個(gè)問(wèn)題:MyBatis需要構(gòu)造什么對(duì)象?以及是否兩個(gè)配置文件對(duì)應(yīng)著兩個(gè)對(duì)象审胚?匈勋,似乎就已經(jīng)有了答案,這里做一個(gè)總結(jié):

    MyBatis需要對(duì)配置文件進(jìn)行解析膳叨,最終會(huì)解析成一個(gè)Configuration對(duì)象洽洁,但是要說(shuō)兩個(gè)配置文件對(duì)應(yīng)了兩個(gè)對(duì)象實(shí)際上也沒(méi)有錯(cuò):

    1. Configuration對(duì)象,保存了mybatis-config.xml的配置信息菲嘴。
    2. MappedStatement饿自,保存了XXXMapper.xml的配置信息。

但是最終MappedStatement對(duì)象會(huì)封裝到Configuration對(duì)象中龄坪,合二為一昭雌,成為一個(gè)單獨(dú)的對(duì)象,也就是Configuration健田。

最后給大家畫一個(gè)構(gòu)建過(guò)程的流程圖:

MyBatis構(gòu)建流程圖

填坑

  • SQL語(yǔ)句在哪解析烛卧?

    細(xì)心的同學(xué)可能已經(jīng)發(fā)現(xiàn)了,上文中只說(shuō)了去節(jié)點(diǎn)中獲取一些屬性從而構(gòu)建配置對(duì)象抄课,但是最重要的SQL語(yǔ)句并沒(méi)有提到唱星,這是因?yàn)檫@部分我想要和屬性區(qū)分開單獨(dú)說(shuō),由于MyBatis支持動(dòng)態(tài)SQL和${}跟磨、#{}的多樣的SQL间聊,所以這里單獨(dú)提出來(lái)說(shuō)會(huì)比較合適。

    首先可以確認(rèn)的是抵拘,剛才我們走完的那一整個(gè)流程中哎榴,包含了SQL語(yǔ)句的生成,下面貼代碼(這一段代碼相當(dāng)繞僵蛛,不好讀)尚蝌。

    //解析Sql(重要)  根據(jù)sql文本來(lái)判斷是否需要?jiǎng)討B(tài)解析 如果沒(méi)有動(dòng)態(tài)sql語(yǔ)句且 只有#{}的時(shí)候 直接靜態(tài)解析使用?占位 當(dāng)有 ${} 不解析
        SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    

    這里就是生成Sql的入口,以單步調(diào)試的角度接著往下看充尉。

    /*進(jìn)入createSqlSource方法*/
    @Override
    public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
        //進(jìn)入這個(gè)構(gòu)造
        XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
        //進(jìn)入parseScriptNode
        return builder.parseScriptNode();
    }
    /**
    進(jìn)入這個(gè)方法
    */
    public SqlSource parseScriptNode() {
        //#
        //會(huì)先解析一遍
        MixedSqlNode rootSqlNode = parseDynamicTags(context);
        SqlSource sqlSource;
        if (isDynamic) {
          //如果是${}會(huì)直接不解析飘言,等待執(zhí)行的時(shí)候直接賦值
          sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
        } else {
          //用占位符方式來(lái)解析  #{} --> ?
          sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
        }
        return sqlSource;
    }
    protected MixedSqlNode parseDynamicTags(XNode node) {
        List<SqlNode> contents = new ArrayList<>();
        //獲取select標(biāo)簽下的子標(biāo)簽
        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語(yǔ)句 這里是 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 {
              //如果不是,則直接生成靜態(tài)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);
      }
    
    /*從上面的代碼段到這一段中間需要經(jīng)過(guò)很多代碼驼侠,就不一段一段貼了*/
    public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
        ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
        //這里會(huì)生成一個(gè)GenericTokenParser姿鸿,傳入#{}作為開始和結(jié)束谆吴,然后調(diào)用其parse方法,即可將#{}換為 ?
        GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
        //這里可以解析#{} 將其替換為?
        String sql = parser.parse(originalSql);
        return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
      }
    
    //經(jīng)過(guò)一段復(fù)雜的解析過(guò)程
    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()方法苛预,這個(gè)方法是核心
              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
    //當(dāng)掃描到${}的時(shí)候調(diào)用此方法  其實(shí)就是不解析 在運(yùn)行時(shí)候在替換成具體的值
    @Override
    public String handleToken(String content) {
      this.isDynamic = true;
      return null;
    }
    //ParameterMappingTokenHandler的handleToken
    //全局掃描#{id} 字符串之后  會(huì)把里面所有 #{} 調(diào)用handleToken 替換為?
    @Override
    public String handleToken(String content) {
          parameterMappings.add(buildParameterMapping(content));
          return "?";
    }
    

    這段代碼相當(dāng)繞句狼,我們應(yīng)該站在一個(gè)宏觀的角度去看待它。所以我直接在這里概括一下:

    首先這里會(huì)通過(guò)<select>節(jié)點(diǎn)獲取到我們的SQL語(yǔ)句热某,假設(shè)SQL語(yǔ)句中只有${}腻菇,那么直接就什么都不做,在運(yùn)行的時(shí)候直接進(jìn)行賦值昔馋。

    而如果掃描到了#{}字符串之后筹吐,會(huì)進(jìn)行替換,將#{}替換為 ?绒极。

    那么他是怎么進(jìn)行判斷的呢骏令?

    這里會(huì)生成一個(gè)GenericTokenParser,這個(gè)對(duì)象可以傳入一個(gè)openToken和closeToken垄提,如果是#{}榔袋,那么openToken就是#{,closeToken就是 }铡俐,然后通過(guò)parse方法中的handler.handleToken()方法進(jìn)行替換凰兑。

    在這之前由于已經(jīng)進(jìn)行過(guò)SQL是否含有#{}的判斷了,所以在這里如果是只有${}审丘,那么handler就是BindingTokenParser的實(shí)例化對(duì)象吏够,如果存在#{},那么handler就是ParameterMappingTokenHandler的實(shí)例化對(duì)象滩报。

    分別進(jìn)行處理锅知。

  • 上文中提到的解析不了的節(jié)點(diǎn)是什么意思?

    根據(jù)上文的代碼我們可知脓钾,解析Mapper.xml文件中的每個(gè)節(jié)點(diǎn)是有順序的售睹。

    那么假設(shè)我寫了這么幾個(gè)節(jié)點(diǎn):

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

    select節(jié)點(diǎn)是需要獲取resultMap的,但是此時(shí)resultMap并沒(méi)有被解析到可训,所以解析到<select>這個(gè)節(jié)點(diǎn)的時(shí)候是無(wú)法獲取到resultMap的信息的昌妹。

    我們來(lái)看看MyBatis是怎么做的:

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

    當(dāng)解析到某個(gè)節(jié)點(diǎn)出現(xiàn)問(wèn)題的時(shí)候,會(huì)拋一個(gè)異常握截,然后會(huì)調(diào)用configuration的addIncompleteStatement方法飞崖,將這個(gè)解析對(duì)象先暫存到這個(gè)集合中,等到所有的節(jié)點(diǎn)都解析完畢之后谨胞,在對(duì)這個(gè)集合內(nèi)的解析對(duì)象繼續(xù)解析:

    public void parse() {
        //判斷文件是否之前解析過(guò)
        if (!configuration.isResourceLoaded(resource)) {
          //解析mapper文件
          configurationElement(parser.evalNode("/mapper"));
          configuration.addLoadedResource(resource);
          //綁定Namespace里面的Class對(duì)象
          bindMapperForNamespace();
        }
    
        //重新解析之前解析不了的節(jié)點(diǎn)
        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);
      }
    

結(jié)語(yǔ)

至此整個(gè)MyBatis的查詢前構(gòu)建的過(guò)程就基本說(shuō)完了固歪,簡(jiǎn)單地總結(jié)就是,MyBatis會(huì)在執(zhí)行查詢之前胯努,對(duì)配置文件進(jìn)行解析成配置對(duì)象:Configuration昼牛,以便在后面執(zhí)行的時(shí)候去使用术瓮,而存放SQL的xml又會(huì)解析成MappedStatement對(duì)象,但是最終這個(gè)對(duì)象也會(huì)加入Configuration中贰健,至于Configuration是如何被使用的,以及SQL的執(zhí)行部分恬汁,我會(huì)在下一篇說(shuō)SQL執(zhí)行的時(shí)候分享伶椿。

歡迎大家訪問(wèn)我的個(gè)人博客:Object's Blog

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市氓侧,隨后出現(xiàn)的幾起案子脊另,更是在濱河造成了極大的恐慌,老刑警劉巖约巷,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件偎痛,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡独郎,警方通過(guò)查閱死者的電腦和手機(jī)踩麦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)氓癌,“玉大人谓谦,你說(shuō)我怎么就攤上這事√巴瘢” “怎么了反粥?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)疲迂。 經(jīng)常有香客問(wèn)我才顿,道長(zhǎng),這世上最難降的妖魔是什么尤蒿? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任郑气,我火速辦了婚禮,結(jié)果婚禮上优质,老公的妹妹穿的比我還像新娘竣贪。我一直安慰自己,他們只是感情好巩螃,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布演怎。 她就那樣靜靜地躺著,像睡著了一般避乏。 火紅的嫁衣襯著肌膚如雪爷耀。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天拍皮,我揣著相機(jī)與錄音歹叮,去河邊找鬼跑杭。 笑死,一個(gè)胖子當(dāng)著我的面吹牛咆耿,可吹牛的內(nèi)容都是我干的德谅。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼萨螺,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼窄做!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起慰技,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤椭盏,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后吻商,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體掏颊,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年艾帐,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了乌叶。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡掩蛤,死狀恐怖枉昏,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情揍鸟,我是刑警寧澤兄裂,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站阳藻,受9級(jí)特大地震影響晰奖,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜腥泥,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一匾南、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蛔外,春花似錦蛆楞、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至矛纹,卻和暖如春臂聋,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工孩等, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留艾君,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓肄方,卻偏偏與公主長(zhǎng)得像冰垄,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子权她,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353