Mybatis3详解(十六)----Mybatis运行原理之SqlSessionFactory的构建过程
1、写在前面
前面的一系列文章已经详细的介绍了Mybatis的各种使用方法,所以这章我们来更加深入的了解Mybatis,讲述一下Mybatis的内部解析与运行原理,但是这章所讲的只涉及基本的框架和核心代码,并不会面面俱到,所以本章中的一些细节将会被忽略掉,需要仔细研究的可以自行查阅相关书籍或者问度娘。虽然这章不可能让你对Mybatis的所有知识点都了解,但是当我们掌握了Mybatis的运行原理,就可以知道Mybatis是怎么运行的,也为后面大家阅读Mybatis源码奠定一点基础吧。
Mybatis的运行分为两大部分:
- 一是SqlSessionFactory的创建过程,它主要是通过XMLConfigBuilder将我们的配置文件读取并且缓存到Configuration对象中,然后通过Configuration来创建SqlSessionFactory对象。
- 二是SqlSession的执行过程,这个过程是Mybatis中最复杂的,它包含了许多复杂的技术,包括反射技术和动态代理技术等,这是Mybatis底层架构的基础。
MyBatis的主要成员组件(成员):
在第一章的时候,简单的介绍了Mybatis的有哪些组件(成员),这里再次详细的介绍一下:
- SqlSessionFactoryBuilder:会根据XML配置或是Java配置来生成SqlSessionFactory对象。采用建造者模式(简单来说就是分步构建一个大的对象,例如建造一个大房子,采用购买砖头、砌砖、粉刷墙面的步骤建造,其中的大房子就是大对象,一系列的建造步骤就是分步构建)。
- SqlSessionFactory:用于生成SqlSession,可以通过 SqlSessionFactory.openSession() 方法创建 SqlSession 对象。使用工厂模式(简单来说就是我们获取对象是通过一个类,由这个类去创建我们所需的实例并返回,而不是我们自己通过new去创建)。
- Configuration:MyBatis所有的配置信息都保存在Configuration对象之中,配置文件中的大部分配置都会存储到该类中。
- SqlSession:相当于JDBC中的 Connection对象,可以用 SqlSession 实例来直接执行被映射的 SQL 语句,也可以获取对应的Mapper。
- Executor:MyBatis 中所有的 Mapper 语句的执行都是通过 Executor 执行的,负责SQL语句的生成和查询缓存的维护 。
- StatementHandler:封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数等。
- ParameterHandler:负责对用户传递的参数转换成JDBC Statement 所对应的数据类型。
- ResultSetHandler:负责将JDBC返回的ResultSet结果集对象转换成List类型的集合。
- TypeHandler:负责java数据类型和jdbc数据类型(也可以说是数据表列类型)之间的映射和转换。
- MappedStatement:作用是保存一个映射器节点<select|update|delete|insert>中的内容,主要用途是描述一条SQL语句。MappedStatement封装了Statement的相关信息,包括我们配置的SQL、SQL的id、缓存信息、resultMap、ParameterType、resultType、resultMap等重要配置内容等。Mybatis可以通过它来获取某条SQL配置的所有信息。它还有一个非常重要的属性是SqlSource。
- 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语句的一个中转站吧。
- 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,这里就不做讨论了,通过名字应该很容易理解它的用处。
以上主要组件(成员)在一次数据库操作中基本都会涉及。
注:图片来自《MyBatis 插件之拦截器(Interceptor)》我自己对其简单的加工了一下。
2、SqlSessionFactory的构建过程
SqlSessionFactory 是MyBatis的核心类之一, 其最重要的功能就是提供创建MyBatis的核心接口SqlSession,所以我们要先创建SqlSessionFactory,它是通过Builder(建造者)模式来创建的,所以在Mybatis中提供了SqlSessionFactoryBuilder类。其构建分为两步。
- 第 1 步: 通过 org.apache.ibatis.builder.xml.XMLConfigBuilder 解析配置的XML文件,读出所配置的参数,并将读取的内容存入org.apache.ibatis.session.Configuration类对象中。而Configuration采用的是单例模式,几乎所有的 MyBatis 配置内容都会存放在这个单例对象中,以便后续将这些内容读出。
- 第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。我们来看一下这个类构造方法:
通过查看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() { //用于标识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属性方式引入映射器的构造方法(它两共用一个):
XPathParser将mapper配置文件解析成Document对象后封装到一个XPathParser对象,再将XPathParser对象作为参数传给XMLMapperBuilder构造方法并构造出一个XMLMapperBuilder对象,XMLMapperBuilder对象的builderAssistant字段是一个MapperBuilderAssistant对象,同样也是BaseBuilder的一个子类,其作用是对MappedStatement对象进行封装。
有了XMLMapperBuilder对象后,就可以进入解析mapper映射文件的过程,进入parse()方法:
调用XMLMapperBuilder的configurationElement方法,对mapper映射文件进行解析
mapper映射文件必须有namespace属性值,否则抛出异常,将namespace属性保存到XMLMapperBuilder的MapperBuilderAssistant对象中,以便其他方法调用。
该方法对mapper映射文件每个标签逐一解析并保存到Configuration和MapperBuilderAssistant对象中,最后调用buildStatementFromContext方法解析select、insert、update和delete节点。
buildStatementFromContext方法中调用XMLStatementBuilder的parseStatementNode()方法来完成解析。
注意:解析所有的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构建过程中的时序图:
参考链接: