User-Profile-Image
hankin
  • 5
  • Java
  • Kotlin
  • Spring
  • Web
  • SQL
  • MegaData
  • More
  • Experience
  • Enamiĝu al vi
  • 分类
    • Zuul
    • Zookeeper
    • XML
    • WebSocket
    • Web Notes
    • Web
    • Vue
    • Thymeleaf
    • SQL Server
    • SQL Notes
    • SQL
    • SpringSecurity
    • SpringMVC
    • SpringJPA
    • SpringCloud
    • SpringBoot
    • Spring Notes
    • Spring
    • Servlet
    • Ribbon
    • Redis
    • RabbitMQ
    • Python
    • PostgreSQL
    • OAuth2
    • NOSQL
    • Netty
    • MySQL
    • MyBatis
    • More
    • MinIO
    • MegaData
    • Maven
    • LoadBalancer
    • Kotlin Notes
    • Kotlin
    • Kafka
    • jQuery
    • JavaScript
    • Java Notes
    • Java
    • Hystrix
    • Git
    • Gateway
    • Freemarker
    • Feign
    • Eureka
    • ElasticSearch
    • Docker
    • Consul
    • Ajax
    • ActiveMQ
  • 页面
    • 归档
    • 摘要
    • 杂图
    • 问题随笔
  • 友链
    • Spring Cloud Alibaba
    • Spring Cloud Alibaba - 指南
    • Spring Cloud
    • Nacos
    • Docker
    • ElasticSearch
    • Kotlin中文版
    • Kotlin易百
    • KotlinWeb3
    • KotlinNhooo
    • 前端开源搜索
    • Ktorm ORM
    • Ktorm-KSP
    • Ebean ORM
    • Maven
    • 江南一点雨
    • 江南国际站
    • 设计模式
    • 熊猫大佬
    • java学习
    • kotlin函数查询
    • Istio 服务网格
    • istio
    • Ktor 异步 Web 框架
    • PostGis
    • kuangstudy
    • 源码地图
    • it教程吧
    • Arthas-JVM调优
    • Electron
    • bugstack虫洞栈
    • github大佬宝典
    • Sa-Token
    • 前端技术胖
    • bennyhuo-Kt大佬
    • Rickiyang博客
    • 李大辉大佬博客
    • KOIN
    • SQLDelight
    • Exposed-Kt-ORM
    • Javalin—Web 框架
    • http4k—HTTP包
    • 爱威尔大佬
    • 小土豆
    • 小胖哥安全框架
    • 负雪明烛刷题
    • Kotlin-FP-Arrow
    • Lua参考手册
    • 美团文章
    • Java 全栈知识体系
    • 尼恩架构师学习
    • 现代 JavaScript 教程
    • GO相关文档
    • Go学习导航
    • GoCN社区
    • GO极客兔兔-案例
    • 讯飞星火GPT
    • Hollis博客
    • PostgreSQL德哥
    • 优质博客推荐
    • 半兽人大佬
    • 系列教程
    • PostgreSQL文章
    • 云原生资料库
    • 并发博客大佬
Help?

Please contact us on our email for need any support

Support
    首页   ›   SQL   ›   MyBatis   ›   正文
MyBatis

Mybatis原理—SqlSessionFactory的构建过程

2022-03-04 14:09:02
541  0 1
参考目录 隐藏
1) Mybatis成员
2) SqlSessionFactory的构建过程
3) 解析Sql过程

阅读完需:约 27 分钟

Mybatis成员

Mybatis的运行分为两大部分:

  1. 一是SqlSessionFactory的创建过程,它主要是通过XMLConfigBuilder将我们的配置文件读取并且缓存到Configuration对象中,然后通过Configuration来创建SqlSessionFactory对象。
  2. 二是SqlSession的执行过程,这个过程是Mybatis中最复杂的,它包含了许多复杂的技术,包括反射技术和动态代理技术等,这是Mybatis底层架构的基础。

MyBatis的主要成员组件(成员):

  1. SqlSessionFactoryBuilder:会根据XML配置或是Java配置来生成SqlSessionFactory对象。采用建造者模式(简单来说就是分步构建一个大的对象,例如建造一个大房子,采用购买砖头、砌砖、粉刷墙面的步骤建造,其中的大房子就是大对象,一系列的建造步骤就是分步构建)。
  2. SqlSessionFactory:用于生成SqlSession,可以通过 SqlSessionFactory.openSession() 方法创建 SqlSession 对象。使用工厂模式(简单来说就是我们获取对象是通过一个类,由这个类去创建我们所需的实例并返回,而不是我们自己通过new去创建)。
  3. Configuration:MyBatis所有的配置信息都保存在Configuration对象之中,配置文件中的大部分配置都会存储到该类中。
  4. SqlSession:相当于JDBC中的 Connection对象,可以用 SqlSession 实例来直接执行被映射的 SQL 语句,也可以获取对应的Mapper。
  5. Executor:MyBatis 中所有的 Mapper 语句的执行都是通过 Executor 执行的,负责SQL语句的生成和查询缓存的维护 。
  6. StatementHandler:封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数等。
  7. ParameterHandler:负责对用户传递的参数转换成JDBC Statement 所对应的数据类型。
  8. ResultSetHandler:负责将JDBC返回的ResultSet结果集对象转换成List类型的集合。
  9. TypeHandler:负责java数据类型和jdbc数据类型(也可以说是数据表列类型)之间的映射和转换。
  10. MappedStatement:作用是保存一个映射器节点<select|update|delete|insert>中的内容,主要用途是描述一条SQL语句。MappedStatement封装了Statement的相关信息,包括我们配置的SQL、SQL的id、缓存信息、resultMap、ParameterType、resultType、resultMap等重要配置内容等。Mybatis可以通过它来获取某条SQL配置的所有信息。它还有一个非常重要的属性是SqlSource。
  11. SqlSource:负责提供BoundSql对象的地方。作用就是根据上下文和参数解析生成真正的SQL,然后将信息封装到BoundSql对象中,并返回。我们在Mapper映射文件中定义的SQL,这个SQL可以有占位符和一系列参数的(如select * from t_user where id = #{id}),也可以是动态SQL的形式,这里的SqlSource就是用来将它解析为真正的SQL(如:select * from t_user where id = ?)。注意:SqlSource是一个接口,而不是一个实现类。对它而言有这么几个重要的实现类:DynamicSQLSource、ProviderSQLSource、RawSQLSource、StaticSQLSource。例如前面动态SQL就采用了DynamicSQLSource配合参数解析解析后得到的。它算是起到生成真正SQL语句的一个中转站吧。
  12. BoundSql:它是一个结果对象,它是通过SqlSource来获取的。作用是通过SqlSource对映射文件的SQL和参数联合解析得到的真正SQL和参数。什么意思呢?就是BoundSql包含了真正的SQL语句(由SqlSource生成的,如select * from t_user where id = ?),而且还包含了SQL语句增删改查的参数,而SqlSource是负责将映射文件中定义的SQL生成真正的SQL语句(算是映射文件中的SQL生成真正的SQL语句的中转站)。BoundSql有3个常用的属性:sql、parameterObject、parameterMappings,这里就不做讨论了,通过名字应该很容易理解它的用处。


SqlSessionFactory的构建过程

SqlSessionFactory 是MyBatis的核心类之一, 其最重要的功能就是提供创建MyBatis的核心接口SqlSession,所以我们要先创建SqlSessionFactory,它是通过Builder(建造者)模式来创建的,所以在Mybatis中提供了SqlSessionFactoryBuilder类。其构建分为两步。

  1. 第 1 步: 通过 org.apache.ibatis.builder.xml.XMLConfigBuilder 解析配置的XML文件,读出所配置的参数,并将读取的内容存入org.apache.ibatis.session.Configuration类对象中。而Configuration采用的是单例模式,几乎所有的 MyBatis 配置内容都会存放在这个单例对象中,以便后续将这些内容读出。
  2. 第2步:使用Confinguration对象去创建SqlSessionFactory。MyBatis 中的 SqlSessionFactory 是一个接口,而不是一个实现类,为此MyBatis提供了一个默认的实现类org.apache.ibatis.session.defaults.DefaultSqlSessionFactory。在大部分情况下都没有必要自己去创建新的SqlSessionFactory 实现类,而是由系统创建。

这种创建的方式就是一种 Builder 模式,对于复杂的对象而言,使用构造参数很难实现。这时使用一个类(比如 Configuration)作为统领,一步步地构建所需的内容,然后通过它去创建最终的对象(比如 SqlSessionFactory),这样每一步都会很清晰,这种方式值得大家学习,并且在工作中使用。

下面我们就来学习一下SqlSessionFactory是如何构建的。程序入口代码如下:

//1、加载 mybatis 全局配置文件
InputStream is = MybatisTest.class.getClassLoader().getResourceAsStream("mybatis-config.xml");
//InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
//2、创建SqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
//3、根据 sqlSessionFactory 来创建sqlSession 对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//4、创建Mapper接口的的代理对象,getMapper方法底层会通过动态代理生成UserMapper的代理实现类
UserMapper mapper = sqlSession.getMapper(UserMapper.class);

①、首先会执行SqlSessionFactoryBuilder类中的build(InputStream inputStream)方法。

//最初调用SqlSessionFactoryBuilder类中的build
public SqlSessionFactory build(InputStream inputStream) {
    //然后调用了重载方法
    return build(inputStream, null, null);
}

②、上面的方法中调用了另一个重载的build方法。

//调用的重载方法
  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
  try {
    //XMLConfigBuilder是专门解析mybatis的配置文件的类
    XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
    //又调用了一个重载方法。parser.parse()的返回值是Configuration对象,这是解析配置文件最核心的方法,非常重要!
    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.
    }
  }
}

可以发现其内部定义了一个XMLConfigBuilder对象,然后通过这个对象调用自身的parse()方法对配置文件进行解析,这个parse()方法的返回值为Configuration对象,最后将返回的Configuration对象作为参数调用build()方法,从而完成SqlSessionFactory的创建。所以我们这里需要注意的就是这两行代码:XMLConfigBuilder对象和调用build(parser.parse())方法返回SqlSessionFactory。

(1)、XMLConfigBuilder从类名就可以看出,这是用来解析XML配置文件的类,其父类为BaseBuilder。我们来看一下这个类构造方法:

  private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
    /**
     * 调用父类的BaseBuilder的构造方法:给
     * configuration赋值
     * typeAliasRegistry别名注册器赋值
     * TypeHandlerRegistry赋值
     */
    super(new Configuration());
    ErrorContext.instance().resource("SQL Mapper Configuration");
    /**
     * 把props绑定到configuration的props属性上
     */
    this.configuration.setVariables(props);
    this.parsed = false;
    this.environment = environment;
    this.parser = parser;
  }

通过查看XMLConfigBuilder中构造方法的源码,可以得知XML配置文件最终是由org.apache.ibatis.parsing.XPathParser封装的XPath解析的。第一个构造方法通过XPathParser构造方法传入我们读取的XML流文件、Properites流文件和environment等参数得到了一个XpathParser实例对象parser,这里parser已包含全局XML配置文件解析后的所有信息,然后再将parser作为参数传给XMLConfigBuilder构造方法。其中XMLConfigBuilder 构造方法还调用了父类BaseBuilder的构造方法BaseBuilder(Configuration),这里传入了一个Configuration对象,用来初始化Configuration对象,我们来继续进入看一下:

注意:这里的重点是创建了一个Configuration 对象,并且完成了初始化,这个Configuration是用来封装所有配置文件的类,所以非常非常重要!!!同时还初始化了别名和类型处理器,所以我们默认可以使用这些特性。额外这个父类BaseBuilder还包含了MapperBuilderAssistant, SqlSourceBuilder, XMLConfigBuilder, XMLMapperBuilder, XMLScriptBuilder, XMLStatementBuilder等子类,这些子类都是用来解析MyBatis各个配置文件,他们通过BaseBuilder父类共同维护一个全局的Configuration对象。只是XMLConfigBuilder的作用就是解析全局配置文件,调用BaseBuilder其他子类解析其他配置文件,生成最终的Configuration对象。

(2)、然后我们重点来看一下parse()方法,这是最核心的方法。进入parse.parse()方法:

  public Configuration parse() {
    /**
     * 若已经解析过了 就抛出异常
     */
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    /**
     * 设置解析标志位
     */
    parsed = true;
    /**
     * 解析我们的mybatis-config.xml的
     * 节点
     * <configuration>
     *
     *
     * </configuration>
     */
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

注意:XMLConfigBuilder的 parsed 属性,默认值是false(上面的构造方法中可以看到),它是用来标识XMLConfigBuilder对象的。当创建了一个XMLConfigBuilder对象,并进行解析配置文件的时候,parsed的值就变成了true。如果第二次进行解析的时候就会抛出BuilderException异常,提示每个XMLConfigBuilder只能使用一次,从而确保了Configuration对象是单例的。因为Configuration对象是通过XMLConfigBuilder的parse()去解析的。

Configuration对象的具体解析是通过parseConfiguration(XNode root)方法来完成的。这个方法用于解析MyBatis 全局配置文件与SQL 映射文件中的相关配置,参数中”/configuration” 就是对应全局配置文件中的<configuration> 标签,parser 是XPathParser 类的实例(前面已经介绍过了),通过该对象解析XML 配置文件然后把它们解析并保存在Configuration单例中。所以下面我们来看一下这个方法的具体源码:

// 方法实现说明:解析我们mybatis-config.xml的 configuration节点  
private void parseConfiguration(XNode root) {
    try {
      /**
       * 解析 properties节点
       *     <properties resource="mybatis/db.properties" />
       *     解析到org.apache.ibatis.parsing.XPathParser#variables
       *           org.apache.ibatis.session.Configuration#variables
       */
      propertiesElement(root.evalNode("properties"));
      /**
       * 解析我们的mybatis-config.xml中的settings节点
       * 具体可以配置哪些属性:http://www.mybatis.org/mybatis-3/zh/configuration.html#settings
       * <settings>
            <setting name="cacheEnabled" value="true"/>
            <setting name="lazyLoadingEnabled" value="true"/>
           <setting name="mapUnderscoreToCamelCase" value="false"/>
           <setting name="localCacheScope" value="SESSION"/>
           <setting name="jdbcTypeForNull" value="OTHER"/>
            ..............
           </settings>
       *
       */
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      /**
       * 基本没有用过该属性
       * VFS含义是虚拟文件系统;主要是通过程序能够方便读取本地文件系统、FTP文件系统等系统中的文件资源。
         Mybatis中提供了VFS这个配置,主要是通过该配置可以加载自定义的虚拟文件系统应用程序
         解析到:org.apache.ibatis.session.Configuration#vfsImpl
       */
      loadCustomVfs(settings);
      /**
       * 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。
       * SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING
       * 解析到org.apache.ibatis.session.Configuration#logImpl
       */
      loadCustomLogImpl(settings);
      /**
       * 解析我们的别名
       * <typeAliases>
           <typeAlias alias="Author" type="cn.tulingxueyuan.pojo.Author"/>
        </typeAliases>
       <typeAliases>
          <package name="cn.tulingxueyuan.pojo"/>
       </typeAliases>
       解析到oorg.apache.ibatis.session.Configuration#typeAliasRegistry.typeAliases
       */
      typeAliasesElement(root.evalNode("typeAliases"));
      /**
       * 解析我们的插件(比如分页插件)
       * mybatis自带的
       * Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
         ParameterHandler (getParameterObject, setParameters)
         ResultSetHandler (handleResultSets, handleOutputParameters)
         StatementHandler (prepare, parameterize, batch, update, query)
        解析到:org.apache.ibatis.session.Configuration#interceptorChain.interceptors
       */
      pluginElement(root.evalNode("plugins"));

      /**
       * todo
       */
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      // 设置settings 和默认值
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631

      /**
       * 解析我们的mybatis环境
         <environments default="dev">
           <environment id="dev">
             <transactionManager type="JDBC"/>
             <dataSource type="POOLED">
             <property name="driver" value="${jdbc.driver}"/>
             <property name="url" value="${jdbc.url}"/>
             <property name="username" value="root"/>
             <property name="password" value="Zw726515"/>
             </dataSource>
           </environment>

         <environment id="test">
           <transactionManager type="JDBC"/>
           <dataSource type="POOLED">
           <property name="driver" value="${jdbc.driver}"/>
           <property name="url" value="${jdbc.url}"/>
           <property name="username" value="root"/>
           <property name="password" value="123456"/>
           </dataSource>
         </environment>
       </environments>
       *  解析到:org.apache.ibatis.session.Configuration#environment
       *  在集成spring情况下由 spring-mybatis提供数据源 和事务工厂
       */
      environmentsElement(root.evalNode("environments"));
      /**
       * 解析数据库厂商
       *     <databaseIdProvider type="DB_VENDOR">
                <property name="SQL Server" value="sqlserver"/>
                <property name="DB2" value="db2"/>
                <property name="Oracle" value="oracle" />
                <property name="MySql" value="mysql" />
             </databaseIdProvider>
       *  解析到:org.apache.ibatis.session.Configuration#databaseId
       */
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      /**
       * 解析我们的类型处理器节点
       * <typeHandlers>
            <typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
          </typeHandlers>
          解析到:org.apache.ibatis.session.Configuration#typeHandlerRegistry.typeHandlerMap
       */
      typeHandlerElement(root.evalNode("typeHandlers"));
      /**
       * 最最最最最重要的就是解析我们的mapper
       *
       resource:来注册我们的class类路径下的
       url:来指定我们磁盘下的或者网络资源的
       class:
       若注册Mapper不带xml文件的,这里可以直接注册
       若注册的Mapper带xml文件的,需要把xml文件和mapper文件同名 同路径
       -->
       <mappers>
          <mapper resource="mybatis/mapper/EmployeeMapper.xml"/>
          <mapper class="com.tuling.mapper.DeptMapper"></mapper>


            <package name="com.tuling.mapper"></package>
          -->
       </mappers>
       * package 1.解析mapper接口 解析到:org.apache.ibatis.session.Configuration#mapperRegistry.knownMappers
                 2.
       */
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

以上操作会把MyBatis 全局配置文件与SQL 映射文件中每一个节点的信息都读取出来,然后保存在Configuration单例中,Configuration分别对以下内容做出了初始化:properties 属性 ;typeAliases 类型别名;plugins 插件;objectFactory 对象工厂;settings 设置;environments 环境;databaseIdProvider 数据库厂商标识;typeHandlers 类型处理器;mappers 映射器等。这其中还涉及到了很多的方法,在这里就不一一讲述了,大家可以自己进行查看。这里主要来看一下mappers映射器,因为我们需要频繁的访问它,因此它算是这里最重要的内容了吧。

进入mapperElement(XNode parent):

  private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      /**
       * 获取我们mappers节点下的一个一个的mapper节点
       */
      for (XNode child : parent.getChildren()) {
        /**
         * 判断我们mapper是不是通过批量注册的
         * <package name="com.tuling.mapper"></package>
         */
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          /**
           * 判断从classpath下读取我们的mapper
           * <mapper resource="mybatis/mapper/EmployeeMapper.xml"/>
           */
          String resource = child.getStringAttribute("resource");
          /**
           * 判断是不是从我们的网络资源读取(或者本地磁盘得)
           * <mapper url="D:/mapper/EmployeeMapper.xml"/>
           */
          String url = child.getStringAttribute("url");
          /**
           * 解析这种类型(要求接口和xml在同一个包下)
           * <mapper class="com.tuling.mapper.DeptMapper"></mapper>
           *
           */
          String mapperClass = child.getStringAttribute("class");

          /**
           * 我们得mappers节点只配置了
           * <mapper resource="mybatis/mapper/EmployeeMapper.xml"/>
           */
          if (resource != null && url == null && mapperClass == null) {
            ErrorContext.instance().resource(resource);
            /**
             * 把我们的文件读取出一个流
             */
            InputStream inputStream = Resources.getResourceAsStream(resource);
            /**
             * 创建读取XmlMapper构建器对象,用于来解析我们的mapper.xml文件
             */
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            /**
             * 真正的解析我们的mapper.xml配置文件(说白了就是来解析我们的sql)
             */
            mapperParser.parse();
          } else if (resource == null && url != null && mapperClass == null) {
            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.");
          }
        }
      }
    }
  }

上面的代码遍历mappers标签下所有子节点,其中:

  • 如果遍历到package子节点,是以包名引入映射器,则将该包下所有Class注册到Configuration的mapperRegistry中。
  • 如果遍历到mapper子节点的class属性,是以class的方式引入映射器,则将制定的Class注册到注册到Configuration的mapperRegistry中。
  • 如果遍历到mapper子节点的resource或者url属性,是通过resource或者url方式引入映射器,则直接对资源文件进行解析:

所以在通过resource或者url方式引入映射器的代码中,可以注意到定义了一个XMLMapperBuilder 类,然后调用了parse()方法,这目的就很明显了,如果遍历到是以mapper子节点的resource或者url属性方式引入的映射器,那么所有的Sql映射文件都是用XMLMapperBuilder 类的parse()方法来进行解析。

注意:其实到这里就已经可以不用往下看了,如果你想更加深入了解一下,那就继续往下滑吧!!!

我们先分别来看一下resource或者url属性方式引入映射器的构造方法(它两共用一个):

XPathParser将mapper配置文件解析成Document对象后封装到一个XPathParser对象,再将XPathParser对象作为参数传给XMLMapperBuilder构造方法并构造出一个XMLMapperBuilder对象,XMLMapperBuilder对象的builderAssistant字段是一个MapperBuilderAssistant对象,同样也是BaseBuilder的一个子类,其作用是对MappedStatement对象进行封装。

有了XMLMapperBuilder对象后,就可以进入解析mapper映射文件的过程,进入parse()方法:

  /**
   * 方法实现说明:真正的去解析我们的Mapper.xml(EmployeeMapper.xml)
   * @author:xsls
   * @return:
   * @exception:
   * @date:2019/8/30 16:43
   */
  public void parse() {
    /**
     * 判断当前的Mapper是否被加载过
     */
    if (!configuration.isResourceLoaded(resource)) {
      /**
       * 真正的解析我们的 <mapper namespace="com.tuling.mapper.EmployeeMapper">
       *
       */
      configurationElement(parser.evalNode("/mapper"));
      /**
       * 把资源保存到我们Configuration中
       */
      configuration.addLoadedResource(resource);

      bindMapperForNamespace();
    }

    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }

调用XMLMapperBuilder的configurationElement方法,对mapper映射文件进行解析

  /**
   * 方法实现说明:解析我们的<mapper></mapper>节点
   * @author:xsls
   * @param context document节点
   * @return:
   * @exception:
   * @date:2019/8/31 13:34
   */
  private void configurationElement(XNode context) {
    try {
      /**
       * 解析我们的namespace属性
       * <mapper namespace="com.tuling.mapper.EmployeeMapper">
       */
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      /**
       * 保存我们当前的namespace  并且判断接口完全类名==namespace
       */
      builderAssistant.setCurrentNamespace(namespace);
      /**
       * 解析我们的缓存引用
       * 说明我当前的缓存引用和DeptMapper的缓存引用一致
       * <cache-ref namespace="com.tuling.mapper.DeptMapper"></cache-ref>
            解析到org.apache.ibatis.session.Configuration#cacheRefMap<当前namespace,ref-namespace>
            异常下(引用缓存未使用缓存):org.apache.ibatis.session.Configuration#incompleteCacheRefs
       */
      cacheRefElement(context.evalNode("cache-ref"));
      /**
       * 解析我们的cache节点
       * <cache type="org.mybatis.caches.ehcache.EhcacheCache"></cache>
          解析到:org.apache.ibatis.session.Configuration#caches
                 org.apache.ibatis.builder.MapperBuilderAssistant#currentCache
       */
      cacheElement(context.evalNode("cache"));
      /**
       * 解析paramterMap节点(该节点mybaits3.5貌似不推荐使用了)
       */
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      /**
       * 解析我们的resultMap节点
       * 解析到:org.apache.ibatis.session.Configuration#resultMaps
       *    异常 org.apache.ibatis.session.Configuration#incompleteResultMaps
       *
       */
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      /**
       * 解析我们通过sql节点
       *  解析到org.apache.ibatis.builder.xml.XMLMapperBuilder#sqlFragments
       *   其实等于 org.apache.ibatis.session.Configuration#sqlFragments
       *   因为他们是同一引用,在构建XMLMapperBuilder 时把Configuration.getSqlFragments传进去了
       */
      sqlElement(context.evalNodes("/mapper/sql"));
      /**
       * 解析我们的select | insert |update |delete节点
       * 解析到org.apache.ibatis.session.Configuration#mappedStatements
       */
      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);
    }
  }

mapper映射文件必须有namespace属性值,否则抛出异常,将namespace属性保存到XMLMapperBuilder的MapperBuilderAssistant对象中,以便其他方法调用。

该方法对mapper映射文件每个标签逐一解析并保存到Configuration和MapperBuilderAssistant对象中,最后调用buildStatementFromContext方法解析select、insert、update和delete节点。

  private void buildStatementFromContext(List<XNode> list) {
    /**
     * 判断有没有配置数据库厂商ID
     */
    if (configuration.getDatabaseId() != null) {
      buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
  }
  /**
   * 方法实现说明:解析我们得得select|update|delte|insert节点然后
   * 创建我们得mapperStatment对象
   * @author:xsls
   * @param list:所有的select|update|delte|insert节点
   * @param requiredDatabaseId:判断有没有数据库厂商Id
   * @return:
   * @exception:
   * @date:2019/9/5 21:35
   */
  private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    /**
     * 循环我们的select|delte|insert|update节点
     */
    for (XNode context : list) {
      /**
       * 创建一个xmlStatement的构建器对象
       */
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }

buildStatementFromContext方法中调用XMLStatementBuilder的parseStatementNode()方法来完成解析。


  public void parseStatementNode() {
    /**
     * 我们的insert|delte|update|select 语句的sqlId
     */
    String id = context.getStringAttribute("id");
    /**
     * 判断我们的insert|delte|update|select  节点是否配置了
     * 数据库厂商标注
     */
    String databaseId = context.getStringAttribute("databaseId");

    /**
     * 匹配当前的数据库厂商id是否匹配当前数据源的厂商id
     */
    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }

    /**
     * 获得节点名称:select|insert|update|delete
     */
    String nodeName = context.getNode().getNodeName();
    /**
     * 根据nodeName 获得 SqlCommandType枚举
     */
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    /**
     * 判断是不是select语句节点
     */
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    /**
     *  获取flushCache属性
     *  默认值为isSelect的反值:查询:默认flushCache=false   增删改:默认flushCache=true
     */
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    /**
     * 获取useCache属性
     * 默认值为isSelect:查询:默认useCache=true   增删改:默认useCache=false
     */
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);

    /**
     * resultOrdered:  是否需要处理嵌套查询结果 group by (使用极少)
     * 可以将比如 30条数据的三组数据  组成一个嵌套的查询结果
     */
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    /**
     * 解析我们的sql公用片段
     *     <select id="qryEmployeeById" resultType="Employee" parameterType="int">
              <include refid="selectInfo"></include>
              employee where id=#{id}
          </select>
        将 <include refid="selectInfo"></include> 解析成sql语句 放在<select>Node的子节点中
     */
    // Include Fragments before parsing
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    /**
     * 解析我们sql节点的参数类型
     */
    String parameterType = context.getStringAttribute("parameterType");
    // 把参数类型字符串转化为class
    Class<?> parameterTypeClass = resolveClass(parameterType);

    /**
     * 查看sql是否支撑自定义语言
     * <delete id="delEmployeeById" parameterType="int" lang="tulingLang">
     <settings>
          <setting name="defaultScriptingLanguage" value="tulingLang"/>
     </settings>
     */
    String lang = context.getStringAttribute("lang");
    /**
     * 获取自定义sql脚本语言驱动 默认:class org.apache.ibatis.scripting.xmltags.XMLLanguageDriver
     */
    LanguageDriver langDriver = getLanguageDriver(lang);

    // Parse selectKey after includes and remove them.
    /**
     * 解析我们<insert 语句的的selectKey节点, 还记得吧,一般在oracle里面设置自增id
     */
    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    /**
     * 我们insert语句 用于主键生成组件
     */
    KeyGenerator keyGenerator;
    /**
     * selectById!selectKey
     * id+!selectKey
     */
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    /**
     * 把我们的命名空间拼接到keyStatementId中
     * com.tuling.mapper.Employee.saveEmployee!selectKey
     */
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    /**
     *<insert id="saveEmployee" parameterType="com.tuling.entity.Employee" useGeneratedKeys="true" keyProperty="id">
     *判断我们全局的配置类configuration中是否包含以及解析过的组件生成器对象
     */
    if (configuration.hasKeyGenerator(keyStatementId)) {
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {

      /**
       * 若我们配置了useGeneratedKeys 那么就去除useGeneratedKeys的配置值,
       * 否者就看我们的mybatis-config.xml配置文件中是配置了
       * <setting name="useGeneratedKeys" value="true"></setting> 默认是false
       * 并且判断sql操作类型是否为insert
       * 若是的话,那么使用的生成策略就是Jdbc3KeyGenerator.INSTANCE
       * 否则就是NoKeyGenerator.INSTANCE
       */
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }

    /**
     * 通过class org.apache.ibatis.scripting.xmltags.XMLLanguageDriver来解析我们的
     * sql脚本对象  .  解析SqlNode. 注意, 只是解析成一个个的SqlNode, 并不会完全解析sql,因为这个时候参数都没确定,动态sql无法解析
     */
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    /**
     * STATEMENT,PREPARED 或 CALLABLE 中的一个。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED
     */
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    /**
     * 这是一个给驱动的提示,尝试让驱动程序每次批量返回的结果行数和这个设置值相等。 默认值为未设置(unset)(依赖驱动)
     */
    Integer fetchSize = context.getIntAttribute("fetchSize");
    /**
     * 这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为未设置(unset)(依赖驱动)。
     */
    Integer timeout = context.getIntAttribute("timeout");
    /**
     * 将会传入这条语句的参数类的完全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过类型处理器(TypeHandler) 推断出具体传入语句的参数,默认值为未设置
     */
    String parameterMap = context.getStringAttribute("parameterMap");
    /**
     * 从这条语句中返回的期望类型的类的完全限定名或别名。 注意如果返回的是集合,那应该设置为集合包含的类型,而不是集合本身。
     * 可以使用 resultType 或 resultMap,但不能同时使用
     */
    String resultType = context.getStringAttribute("resultType");
    /**解析我们查询结果集返回的类型     */
    Class<?> resultTypeClass = resolveClass(resultType);
    /**
     * 外部 resultMap 的命名引用。结果集的映射是 MyBatis 最强大的特性,如果你对其理解透彻,许多复杂映射的情形都能迎刃而解。
     * 可以使用 resultMap 或 resultType,但不能同时使用。
     */
    String resultMap = context.getStringAttribute("resultMap");

    String resultSetType = context.getStringAttribute("resultSetType");
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    if (resultSetTypeEnum == null) {
      resultSetTypeEnum = configuration.getDefaultResultSetType();
    }

    /**
     * 解析 keyProperty  keyColumn 仅适用于 insert 和 update
     */
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    String resultSets = context.getStringAttribute("resultSets");

    /**
     * 为我们的insert|delete|update|select节点构建成我们的mappedStatment对象
     */
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

注意:解析所有的Sql语句会封装一个MappedStatement中,MappedStatement中包含了许多我们配置的SQL、SQL的id、缓存信息、resultMap、ParameterType、resultType、resultMap等重要配置内容。最重要的是它还有一个属性sqlSource。

通过上面的方法可以看到SQL语句封装到一个SqlSource对象,SqlSource是个接口,如果是动态SQL就创建DynamicSqlSource实现类,否则创建StaticSqlSource实现类。

SqlSource是MappedStatement的一个属性,它只是一个接口。它的主要作用是根据上下文和参数解析生成需要的Sql。

SqlSource接口中有一个getBoundSql方法,这个方法就是用来获取BoundSql的:

SqlSource接口中还有如下这几个重要的实现类:

BoundSql是一个结果集对象,也就是SqlSource通过对映射文件的SQL和参数解析得到的真正的SQL和参数。

注:MappedStatement、SqlSource和BoundSql在最上面已经详细的介绍了,自行滑到上面查看。

 ③、Mybatis的配置文件解析完成后,会将信息保存在Configuration对象中,之后通过XMLConfigBuilder类中的parse()方法返回,然后再将Configuration对象作为参数传递到build(Configuraction config)方法。进入这个build方法:

// SqlSessionFactoryBuilder另一个build方法
public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
}

可以看到最后接着返回一个DefaultSqlSessionFactory对象,DefaultSqlSessionFactory就是SqlSessionFactory的一个实现类,到这里SqlSessionFactory对象就完成了创建的全部过程。

SqlSessionFactory构建过程中的时序图:


解析Sql过程

如本文“对您有用”,欢迎随意打赏作者,让我们坚持创作!

1 打赏
Enamiĝu al vi
不要为明天忧虑.因为明天自有明天的忧虑.一天的难处一天当就够了。
543文章 68评论 294点赞 594205浏览

随机文章
MyBatis笔记12—动态 SQL
5年前
SpringCloud—GateWay(一)
5年前
Spring—WebApplicationContext介绍
5年前
SpringSecurity—OAuth 2(二)授权码模式
5年前
ElasticSearch—索引基本操作(五)
5年前
博客统计
  • 日志总数:543 篇
  • 评论数目:68 条
  • 建站日期:2020-03-06
  • 运行天数:1927 天
  • 标签总数:23 个
  • 最后更新:2024-12-20
Copyright © 2025 网站备案号: 浙ICP备20017730号 身体没有灵魂是死的,信心没有行为也是死的。
主页
页面
  • 归档
  • 摘要
  • 杂图
  • 问题随笔
博主
Enamiĝu al vi
Enamiĝu al vi 管理员
To be, or not to be
543 文章 68 评论 594205 浏览
测试
测试
看板娘
赞赏作者

请通过微信、支付宝 APP 扫一扫

感谢您对作者的支持!

 支付宝 微信支付