Mybatis源码篇(二)配置文件解析流程

Mybatis的整个底层流程其实可以分为两个大的部分:一个是配置文件加载解析的过程;另一个是方法执行的流程。前者是后者的基础,只有配置文件都加载好了,后面我们执行方法的时候,才能及时且方便的拿到我们需要拿到的信息。本文我们就从源码的角度来分析分析整个Mybatis的加载流程。文末会有彩蛋哦。

从SqlSessionFactory的构建说起

首先我们需要知道的是,Mybatis对外提供的一个主要接口就是SqlSession,当我们拿到了SqlSession就可以通过生产Mapper的代理对象,然后进行数据库操作了。但是SqlSession的获取是需要SqlSessionFactory来进行创建的,那我们需要怎样获取到SqlSessionFactory呢?在官方文档中,为我们提供了两种方式:一种是基于XML配置文件的方式来进行创建;另一种是基于java API的方式来进行创建,我们分别来看一看吧:
方式一:基于XML的方式生产SqlSessionFactory

    String resource = "org/mybatis/example/mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

其中mybatis-config.xml demo如下:

<?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>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="org/mybatis/example/BlogMapper.xml"/>
  </mappers>
</configuration>

方式二:基于java代码的方式生成SqlSessionFactory

    DataSource dataSource = BlogDataSourceFactory.getBlogDataSource();
    TransactionFactory transactionFactory = new JdbcTransactionFactory();
    Environment environment = new Environment("development", transactionFactory, dataSource);
    Configuration configuration = new Configuration(environment);
    configuration.addMapper(BlogMapper.class);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);

其实JavaAPI的方式是将xml配置文件中的信息封装成了Configuration而已,而且我们使用XML的方式在底层也是将XML文件中的信息封装到了Configuration中,最终我们操作的还是Configuration这个重要的配置类,这个配置类的生命周期贯穿整个Mybatis的生命周期,说它是最重要的类也不为过。
下面我们就以经典的XML配置的这种方式来进行讲述整个配置文件解析的全流程,其实java配置类的方式大差不大,会了XML方式的底层原理,那么Java API方式的底层原理你也就懂了。

Mybatis配置文件的解析

其实,这个解析流程就是对:properties、settings、typeAliases、plugins、objectFactory、objectWrapperFactory、reflectorFactory、environments、databaseIdProvider、typeHandlers、mappers这几个标签的解析,解析的入口在XMLConfigBuilder类中的parseConfiguration方法中进行,代码如下:

private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      // mappers标签的解析
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

对于前面几个标签的解析都不是很难,我们就不多说了,可以自己看一看,而精彩的是最后一个对于mappers标签的解析,也就是对于我们写的Mapper接口进行解析,这也是下面我们需要详细分析的部分。
在mapperElement方法中,我们也能看到对官方文档中所说的四种配置映射器的方法的分别解析过程:
Mybatis中支持的四种映射器的配置方法:

1.使用相对于类路径的资源引用:
<mappers>
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>  
</mappers>
2.使用完全限定资源定位符(URL)
<mappers> 
 <mapper url="file:///var/mappers/AuthorMapper.xml"/> 
</mappers>
3.使用映射器接口实现类的完全限定类名
<mappers> 
 <mapper class="org.mybatis.builder.AuthorMapper"/>
</mappers>
4.将包内的映射器接口实现全部注册为映射器
<mappers>
  <package name="org.mybatis.builder"/>
</mappers>

再来看看源码中对着四种方法的解析过程:

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        // 类型一:将包内的映射器接口实现全部注册为映射器
        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");
          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)
            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<?> 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.");
          }
        }
      }
    }
  }

虽然有四种配置方式,但这也仅仅是对外的四种方式,最终我们还是要拿到所有的映射器的Class对象,然后执行循环遍历执行mapperRegistry.addMapper(Class<T> type)方法,该方法中主要干了两件事:
1. 为映射器Mapper创建了一个映射器代理工厂MapperProxyFactory,并存储起来,我们都知道mybatis底层是基于jdk动态代理的方式的,而这个代理工厂就是为了后面执行时生成MapperProxy的代理对象;
2. Mybatis开始解析对应的XxxMapper.xml文件。代码如下:

    // 生成Mapper代理工厂
    knownMappers.put(type, new MapperProxyFactory<>(type));
    MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
    // 在此解析XxxMapper.xml文件
    parser.parse();

解析XxxMapper.xml其实也就是挨个的标签进行解析,并将其赋值给相应的Configuration的属性,而XxxMpper.xml文件中的select|insert|delete|update等这些我们写的sql逻辑地方的解析使我们需要重点掌握的。在Mybatis底层,每一个select|insert|delete|update里面的内容都会被封装为MappedStatement对象,而在MappedStatement中保存sql相关信息的属性是SqlSource,所以我们来看下SqlSource的创建逻辑(还是比较重要的),SqlSource的构建是借助于XMLScriptBuilder来进行XML解析的,解析的过程中会通过parseDynamicTags方法(代码如下):

protected MixedSqlNode parseDynamicTags(XNode node) {
    List<SqlNode> contents = new ArrayList<>();
    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) {
        String data = child.getStringBody("");
        TextSqlNode textSqlNode = new TextSqlNode(data);
        // 检测TextSqlNode中是否有${variable}的占位符,有的话就是动态的
        if (textSqlNode.isDynamic()) {
          // SQL 语句中含有 ${} 占位符
          contents.add(textSqlNode);
          isDynamic = true;
        } else {
          // 纯 SQL 语句和 #{} 占位符,不包含任何动态 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);
        // 是动态sql标签的话也是动态sql
        isDynamic = true;
      }
    }
    return new MixedSqlNode(contents);
  }

从解析的代码中,我们会发现Mybatis会把整个select|insert|delete|update标签中的内容,解析为SqlNode的集合(也就是源码中的 List<SqlNode> contents = new ArrayList<>()),封装在MixedSqlNode对象中,其中在Mybatis中,sqlNode的类型有如下几类:

  1. StaticTextSqlNode:纯SQL语句和#{}占位符,不包含任何动态SQL语句(包含${}占位符 )
  2. TextSqlNode: SQL语句中含有${}占位符;
  3. IfSqlNode:if/when子标签里面的SQL语句;
  4. ChooseSqlNode:choose子标签里面的SQL语句;
  5. ForEachSqlNode:foreach子标签里面的SQL语句;
  6. VarDecSqlNode:bind子标签里面的SQL语句;
  7. TrimSqlNode:trim子标签里面的SQL语句;
  8. WhereSqlNode:where子标签里面的SQL语句;
  9. SetSqlNode:set 子标签里面的 SQL 语句;
  10. MixedSqlNode: 如果insert/update/delete/select标签的SQL文本不止一行,则把所有的SqlNode组装在一起的SqlNode。

围绕sqlNode的解析,我们给出如下demo:

demo1:
sql:

  SELECT * from employee_info where employee_code = #{code}

MixedSqlNode:这个就仅仅解析为一个StaticTextSqlNode

demo2:

sql:

  SELECT
ei.department_id as departmentId,
ei.department_name as departmentName
FROM
`crm_employee_info` AS ei
where ei.department_id = ${req.departmentId}
  <if test=" req.departmentId != null ">
       AND ei.department_id = #{req.departmentId}
   </if>
  <if test=" req.employeeStatus != null ">
     AND ei.employee_status = #{req.employeeStatus}
</if>
GROUP BY
ei.department_id,
ei.department_name

MixedSqlNode:这时候解析的就有5个sqlNode分别是:

  1. TextSqlNode
  2. IfSqlNode
  3. StaticTextSqlNode(这是一个换行符)
  4. IfSqlNode
  5. StaticTextSqlNode

除此之外,我们还可以在parseDynamicTags方法中发现了,Mybatis所定义的动态sql的含义:

  1. 动态的sql:

    • 包含${}的占位符
    • 包含trim/where/set/foreach/if/choose/when/otherwise/bind 这些动态的标签的sql
  2. 静态sql:

    • 包含#{}的占位符
    • 纯静态的sql

好了,在解析完所有的sqlNode返回MixedSqlNode后,Mybatis会根据是否是动态sql而分别构建出DynamicSqlSource和RawSqlSource,两者的构建又有什么不同呢?

  1. 动态的SqlSource在此时的构建是比较简单的,直接保存了MixedSqlNode就好,处理的过程留到了后面执行的时候(逻辑在DynamicSqlSource#getBoundSql中);

  2. 而静态的SqlSource在构建时就做一些处理:

    • 将#{}中的参数配置信息有序的解析封装成了ParameterMapping对象,并保存在parameterMappings的List集合中;
    • 将#{}替换成了占位符"?";
    • 最终封装成StaticSqlSource;

其实也就是说静态的sql在此时就已经解析好了;这也是动态sql和静态sql解析不同的地方,解析的时机不同。

到此配置文件的解析也就差不多了,整个解析的过程最重要就是MappedStatement的构建过程了,而MappedStatement的构建过程中最重要的就是SqlSource的构建过程,应当着重理解,这个的理解对后面的执行流程也是有很大相关性的,
最后,其实说的再多也不过一张图来的一目了然,希望能帮助你更好的理解Mybatis。

配置文件解析.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容