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>中的内容。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语句的中转站),这里搞得我有点昏 Mybatis3详解(十六)——Mybatis运行原理之SqlSessionFactory的构建过程Mybatis3详解(十六)——Mybatis运行原理之SqlSessionFactory的构建过程Mybatis3详解(十六)——Mybatis运行原理之SqlSessionFactory的构建过程BoundSql有3个常用的属性:sql、parameterObject、parameterMappings,这里就不做讨论了,通过名字应该很容易理解它的用处。

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

       注:图片来自《深入理解mybatis原理》 MyBatis的架构设计以及实例分析

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

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

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

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

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

       注意:这里的重点是创建了一个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属性方式引入映射器的构造方法(它两共用一个):

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

       参考链接:

  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
上一篇:MyBatis 原理 及 源码分析( SqlSessionFactory、SqlSession、代理接口,一二级缓存 )


下一篇:20.05.03学习记录