• Mybatis3详解(十六)——Mybatis运行原理之SqlSessionFactory的构建过程


    1、写在前面

           前面的一系列文章已经详细的介绍了Mybatis的各种使用方法,所以这章我们来更加深入的了解Mybatis,讲述一下Mybatis的内部解析与运行原理,但是这章所讲的只涉及基本的框架和核心代码,并不会面面俱到,所以本章中的一些细节将会被忽略掉,需要仔细研究的可以自行查阅相关书籍或者问度娘。虽然这章不可能让你对Mybatis的所有知识点都了解,但是当我们掌握了Mybatis的运行原理,就可以知道Mybatis是怎么运行的,也为后面大家阅读Mybatis源码奠定一点基础吧。

           Mybatis的运行分为两大部分:

    1. 一是SqlSessionFactory的创建过程,它主要是通过XMLConfigBuilder将我们的配置文件读取并且缓存到Configuration对象中,然后通过Configuration来创建SqlSessionFactory对象。
    2. 二是SqlSession的执行过程,这个过程是Mybatis中最复杂的,它包含了许多复杂的技术,包括反射技术和动态代理技术等,这是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语句的中转站),这里搞得我有点昏 imageimageimageBoundSql有3个常用的属性:sql、parameterObject、parameterMappings,这里就不做讨论了,通过名字应该很容易理解它的用处。

           以上主要组件(成员)在一次数据库操作中基本都会涉及。

           注:图片来自《MyBatis 插件之拦截器(Interceptor)

    image

    2、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。我们来看一下这个类构造方法:

    image

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

    image

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


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

      public Configuration parse() {
        //用于标识XMLConfigBuilder
        if (parsed) {
          throw new BuilderException("Each XMLConfigBuilder can only be used once.");
        }
        parsed = true;
        //用于解析MyBatis全局配置文件<configuraction>标签中的相关配置
        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单例中。所以下面我们来看一下这个方法的具体源码:

      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);
        }
      }

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

      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);
    		// 如果是mapper引入的方式
            } else {
              String resource = child.getStringAttribute("resource");
              String url = child.getStringAttribute("url");
              String mapperClass = child.getStringAttribute("class");
    		  // 如果是resource
              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();
    			// 如果是url
              } 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();
    			// 如果是mapperClass
              } else if (resource == null && url == null && mapperClass != null) {
                Class<?> mapperInterface = Resources.classForName(mapperClass);
    			// 添加至Configuration对象中
                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属性方式引入映射器的构造方法(它两共用一个):

    image

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

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

    image

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

    image

           mapper映射文件必须有namespace属性值,否则抛出异常,将namespace属性保存到XMLMapperBuilder的MapperBuilderAssistant对象中,以便其他方法调用。
           该方法对mapper映射文件每个标签逐一解析并保存到Configuration和MapperBuilderAssistant对象中,最后调用buildStatementFromContext方法解析select、insert、update和delete节点。

    image

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

    image

    image

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

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

    image

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

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

    image

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

    image


           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构建过程中的时序图:

    image

           参考链接:

    1. 《Java EE 互联网轻量级框架整合开发》
    2. https://www.cnblogs.com/abcboy/p/9618419.html
    3. https://blog.csdn.net/codejas/article/details/79570068
    4. https://blog.csdn.net/qq_37776015/article/details/90249931
  • 相关阅读:
    二进制插入 牛客网 程序员面试金典 C++ Python java
    二进制小数 牛客网 程序员面试金典 C++ Python
    二叉树中和为某一值的路径 牛客网 程序员面试金典 C++ Python
    Python matplotlib pylab 画张图
    Python matplotlib pylot和pylab的区别
    Ubuntu 16.04 curl 安装 使用
    Ubuntu 16.04 菜单栏 换位置 挪到左边 挪到下边
    Python 模块feedparser安装使用
    Ubuntu 16.04 下 旋转显示器屏幕 竖屏显示
    从 callback 到 promise
  • 原文地址:https://www.cnblogs.com/tanghaorong/p/14024260.html
Copyright © 2020-2023  润新知