专栏首页Java研发军团深入剖析mybatis原理(二)

深入剖析mybatis原理(二)

作者:莫那鲁道

链接:http://thinkinjava.cn/article/80

这一章节接着上一章节继续讲解

5. SqlSession 创建过程

我们接下来要看看 SqlSession 的创建过程和运行过程,首先调用了 sqlSessionFactory.openSession() 方法。该方法默认实现类是 DefaultSqlSessionFactory ,我们看看该方法如何被重写的。

调用了自身的 openSessionFromDataSource 方法,注意,参数中 configuration 获取了默认的执行器 “SIMPLE”,自动提交我们没有配置,默认是false,我们进入到 openSessionFromDataSource 方法查看:

该方法以下几个步骤:

1、获取配置文件中的环境,也就是我们配置的 标签,并根据环境获取事务工厂,事务工厂会创建一个事务对象,而 configurationye 则会根据事务对象和执行器类型创建一个执行器。最后返回一个默认的 DefaultSqlSession 对象。

2、可以说,这段代码,就是根据配置文件创建 SqlSession 的核心地带。我们一步步看代码,首先从配置文件中取出刚刚解析的环境对象。

然后根据环境对象获取事务工厂,如果配置文件中没有配置,则创建一个 ManagedTransactionFactory 对象直接返回。否则调用环境对象的 getTransactionFactory 方法,该方法和我们配置的一样返回了一个 JdbcTransactionFactory,而实际上,TransactionFactory 只有2个实现类,一个是 ManagedTransactionFactory ,一个是 JdbcTransactionFactory。

我们回到 openSessionFromDataSource 方法,获取了 JdbcTransactionFactory 后,调用 JdbcTransactionFactory 的 newTransaction 方法创建一个事务对象,参数是数据源,level 是null, 自动提交还是false。newTransaction 创建了一个 JdbcTransaction 对象,我们看看该类的构造:

可以看到,该类都是有关连接和事务的方法,比如commit,openConnection,rollback,和JDBC 的connection 功能很相似。而我们刚刚看到的level是什么呢?在源码中我们看到了答案:

就是 “事务的隔离级别”。并且该事务对象还包含了JDBC 的Connection 对象和 DataSource 数据源对象,好亲切啊,可见这个事务对象就是JDBC的事务的封装。

继续回到 openSessionFromDataSource 方,法此时已经创建好事务对象。接下来将事务对象执行器作为参数执行 configuration 的 newExecutor 方法来获取一个 执行器类。

我们看看该方法实现:

首先,该方法判断给定的执行类型是否为null,如果为null,则使用默认的执行器, 也就是 ExecutorType.SIMPLE,然后根据执行的类型来创建不同的执行器,默认是 SimpleExecutor 执行器,这里楼主需要解释以下执行器:

Mybatis有三种基本的Executor执行器,SimpleExecutor、ReuseExecutor、BatchExecutor。

1、SimpleExecutor:每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。

2、ReuseExecutor:执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map<String, Statement>内,供下一次使用。简言之,就是重复使用Statement对象。

3、BatchExecutor:执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相同。

作用范围:Executor的这些特点,都严格限制在SqlSession生命周期范围内。

我们再看看默认执行器的构造方法,2个参数,一个是 Configuration, 一个是事务对象。该构造器调用了父类 BaseExecutor 的构造器,我们看看该方法实现:

该类包装了事务对象,延迟加载的队列,本地缓存,永久缓存,配置对象,还包装了自己。

回到 newExecutor 方法,判断是否使用缓存,默认是true, 则将刚刚的执行器包装到新的 CachingExecutor 缓存执行器中。最后将执行器添加到所有的拦截器中(如果配置了话),我们这里没有配置。

现在,我们回到 openSessionFromDataSource 方法,我们已经有了执行器,此时创建 DefaultSqlSession 对象,携带 configuration, executor, autoCommit 三个参数,该构造器就是简单的赋值过程。我们有必要看看该类的结构:

该类包含了常用的所有方法,包括事务方法,可以说,该类封装了执行器和事务类。而执行器才是具体的执行工作人员。至此,我们已经完成了 SqlSession 的创建过程。

接下来,就要看看他的执行过程。

6. SqlSession 执行过程

我们创建了一个map,并放入了参数,重点看红框部分,我们钻进去看看。selectOne 方法:

该方法实际上还是调用了selectList方法,最后取得了List中的第一个,如果返回值长度大于1,则抛出异常。啊,原来,经常出现的异常就是这么来的啊,终于知道你是怎么回事了。我们也看的出来,重点再 selectList 方法中,我们进入看看:

该方法携带了3个参数,SQL 声明的key,参数Map,默认分页对象(不分页),注意,mybatis 分页是假分页,即一次返回所有到内存中,再进行提取,如果数据过多,可能引起OOM。我们继续向下走:

该方法首先根据 key或者说 id 从 configuration 中取出 SQL 声明对象, 那么是如何取出的呢?我们知道,我们的SQL语句再XML中编辑的时候,都有一个key,加上我们全限定类名,就成了一个唯一的id,我们进入到该方法查看:

该方法调用了自身的 getMappedStatement 方法,默认需要验证SQL语句是否正确,也就是 buildAllStatements 方法,最后从继承了 HashMap 的StrictMap 中取出 value,这个StrictMap 有个注意的地方,他基本扩展了HashMap 的方法,我们重点看看他的get方法:

如何扩展呢?如果返回值是null,则抛出异常,JDK中HashMap 可是不抛出异常的,如果 value是 Ambiguity 类型,也抛出异常,说明 key 值不够清晰。

那么 buildAllStatements 方法做了什么呢?

注意看注释(大意):解析缓存中所有未处理的语句节点。当所有的映射器都被添加时,建议调用这个方法,因为它提供了快速失败语句验证。意思是如果链表中任何一个不为空,则抛出异常,是一种快速失败的机制。那么这些是什么时候添加进链表的呢?答案是catch的时候,看代码:

这个时候会将错误的语句添加进该链表中。

我们回到 selectList 方法,此时已经返回了 MappedStatement 对象,这个时候该执行器出场了,调用执行器的query方法,携带映射声明,包装过的参数对象,分页对象。那么如何包装参数对象呢?我们看看 wrapCollection 方法:

该方法首先判断是否是集合类型,如果是,则创建一个自定义Map,key是collection,value是集合,如果不是,并且还是数组,则key为array,都不满足则直接返回该对象。那么我们该进入 query 一探究竟:

进入 CachingExecutor 的query 方法,首先根据参数获取 BoundSql 对象,最终会调用 StaticSqlSource 的 getBoundSql 方法,该方法会构造一个 BoundSql 对象,构造过程是什么样子的呢?

会有5个属性被赋值,sql语句,参数,参数是我们刚刚传递的,那么SQL 是怎么来的呢,答案是在 XMLConfigBuilder 的 parseConfiguration 方法中,通过层层调用。

最终执行 StaticSqlSource 的构造方法,将mapper 文件中的Sql解析到该类中,最后会将XML 中的 #{id} 构造成一个ParameterMapping 对象,格式入下:

并将配置对象赋值给该类。

回到 BoundSql 的构造器,首先赋值SQL, 参数映射对象数组,参数对象,默认的额外参数,还有一个元数据参数。

回到我们的 getBoundSql 方法:

我们已经有了参数绑定对象,该对象中有SQL语句,参数。继续向下执行,从该对象获取参数映射集合,如果为空,则再次创建一个 BoundSql 对象。

接着循环参数,先获取 resultMap id,如果有,则从配置对下中获取resultMap 对象,如果不为null,则修改 hasNestedResultMaps 为 true。最后返回 BoundSql 对象。

我们回到 CachingExecutor 的 query 方法, 我们已经有了sql绑定对象, 接下来创建一个缓存key,根据sql绑定对象,方法声明对象,参数对象,分页对象。

注意:mybatis 一级缓存默认为true,二级缓存默认false。创建缓存的过程很简单,就是将所有的参数的key或者id构造该 CacheKey 对象,使该对象唯一。最后执行query方法:

该方法步骤:

1、获取缓存,如果没u偶,则执行代理执行器的query方法,如果有,且需要清空了,则清空缓存(也就是Map)。

2、如果该方法声明使用缓存并且结果处理器为null,则校验参数,如果方法声明使存储过程,且所有参数有任意一个不是输入类型,则抛出异常。意思是当为存储过程时,确保不能有输出参数。

3、调用 TransactionalCacheManager 事务缓存处理器执行 getObject 方法,如果返回值时null,则调用代理执行器的query方法,最后添加进事务缓存处理器。

我们重点关注代理执行器的query方法,也就是我们 SimpleExecutor 执行器。该方法如下:

1、首先判断执行器状态是否关闭。

2、判断是否需要清除缓存。

3、判断结果处理器是否为null,如果不是null,则返回null,如果不是,则从本地缓存中取出。

4、如果返回的list不是null,则处理缓存和参数。否则调用queryFromDatabase 方法从数据库查询。

5、如果需要延迟加载,则开始加载,最后清空加载队列。

6、如果配置文件中的缓存范围是声明范围,则清空本地缓存。

7、最后返回list。

可以看出,我们重点要关注的是 queryFromDatabase 方法,其余的方法都是和缓存相关,但如果没有从数据库取出来,缓存也没什么用。进入该方法查看:

我们关注红框部分。

该方法创建了一个声明处理器,然后调用了 prepareStatement 方法,最后调用了声明处理器的query方法,注意,这个声明处理器有必要说一下:

mybatis 的SqlSession 有4大对象:

1、Executor代表执行器,由它调度StatementHandler、ParameterHandler、ResultSetHandler等来执行对应的SQL。其中StatementHandler是最重要的。

2、StatementHandler的作用是使用数据库的Statement(PreparedStatement)执行操作,它是四大对象的核心,起到承上启下的作用,许多重要的插件都是通过拦截它来实现的。

3、ParamentHandler是用来处理SQL参数的。

4、ResultSetHandler是进行数据集的封装返回处理的,它相当复杂,好在我们不常用它。

好,我们继续查看 configuration 是如何创建 StatementHandler 对象的。我们看看他的 newStatementHandler 方法:

首先根据方法声明类型创建一个声明处理器,有最简单的,有预编译的,有存储过程的,在我们这个方法中,创建了一个预编译的方法声明对象,这个对象的构造器对 configuration 等很多参数进行的赋值。我们还是看看吧:

我们看到了刚刚提到了parameterHandler和resultSetHandler。

回到 newStatementHandler 方法,需要执行下面的拦截器链的pluginAll方法,由于我们这里没有配置拦截器,该方法也就结束了。

拦截器就是实现了Interceptor接口的类,国内著名的分页插件pagehelper就是这个原理,在mybais 源码里,有一个插件使用的例子,我们可以随便看看:

执行了Plugin 的静态 wrap 方法,包装目标类(也就是方法声明处理器),该静态方法如下:

这里就是动态代理的知识了,获取目标类的接口,最后执行拦截器的invoke方法。有机会和大家再一起探讨如何编写拦截器插件。这里由于篇幅原因就不展开了。

我们回到 newStatementHandler 方法,此时,如果我们有拦截器,返回的应该是被层层包装的代理类,但今天我们没有。返回了一个普通的方法声明器。

执行 prepareStatement 方法,携带方法声明器,日志对象。

第一行,获取连接器。

从事务管理器中获取连接器(该方法中还需要设置是否自动提交,隔离级别)。如果我们的事务日志是debug级别,则创建一个日志代理对象,代理Connection。

回到 prepareStatement 方法,看第二行,开始让预编译处理器预编译sql(也就是让connection预编译),我看看看是如何执行的。注意,我们没有配置timeout。因此返回null。

进入 RoutingStatementHandler 的 prepare 方法,调用了代理类的 PreparedStatementHandler 的prepare方法,该方法实现入下:

该方法以下几个步骤:

  1. 实例化SQL,也就是调用connection 启动 prepareStatement 方法。我们熟悉的JDBC方法。
  2. 设置超时时间。
  3. 设置fetchSize ,作用是,执行查询时,一次从服务器端拿多少行的数据到本地jdbc客户端这里来。
  4. 最后返回映射声明处理器。

我们主要看看第一步:

有没有很亲切,我们看到我们在刚开始回忆JDBC编程的 connection.prepareStatement 代码,由此证明mybatis 就是封装了 JDBC。

首先判断是否含有返回主键的功能,如果有,则看 keyColumnNames 是否存在,如果不存在,取第一个列为主键。最后执行else 语句,开始预编译。

注意:此connection 已经被动态代理封装过了,因此会调用 invoke 方法打印日志。最后返回声明处理器对象。

我们回到 SimpleExecutor 的 prepareStatement 方法, 执行第三行 handler.parameterize(stmt),该方法其实也是委托了 PreparedStatementHandler 来执行,而 PreparedStatementHandler 则委托了 DefaultParameterHandler 执行 setParameters 方法,我们看看该方法:

首先获取参数映射集合,然后从配置对象创建一个元数据对象,最后从元数据对象取出参数值。再从参数映射对象中取出类型处理器,最后将类型处理器和参数处理器关联。

我们看看最后一行代码:

还是JDBC。而这个下标的顺序则就是参数映射的数组下标。

终于,在准备了那么多之后,我们回到 doQuery 方法,有了预编译好的声明处理器,接下来就是执行了。当然还是调用了PreparedStatementHandler 的query方法。

可以看到,直接执行JDBC 的 execute 方法,注意,该对象也被日志对象代理了,做打印日志工作,和清除工作。如果方法名称是 “executeQuery” 则返回 ResultSet 并代理该对象。 否则直接执行。

我们继续看看DefaultResultSetHandler 的 handleResultSets 是如何执行的:

首先调用 getFirstResultSet 方法获取包装过的 ResultSet ,然后从映射器中获取 resultMap 和resultSet,如果不为null,则调用 handleResultSet 方法,将返回值和resultMaps处理添加进multipleResults list中 ,然后做一些清除工作。

最后调用 collapseSingleResultList 方法,该方法内容如下:

如果返回值长度等于1,返回第一个值,否则返回本身。

至此,终于返回了一个List。不容易啊!!!!最后在返回值的时候执行关闭 Statement 等操作。

我们还需要关注一下 SqlSession 的 close 方法,该方法是事务最后是否生效的关键,当然真正的执行者是executor,在 CachingExecutor 的close 方法中:

该方法决定了到底是commit 还是rollback,最后执行代理执行器的 close 方法,也就是 SimpleExecutor 的close方法,该方法内容入下:

首先执行rollback方法,该方法内部主要是清除缓存,校验是否清除 Statements。然后执行 transaction.close()方法,重置事务(重置事务的autoCommit 属性为true),最后调用 connection.close() 方法,和我们JDBC 一样,关闭连接。

但实际上,该connection 被代理了,被 PooledConnection 连接池代理了,在该代理的invoke方法中,会将该connection从连接池集合删除,在创建一个新的连接放在集合中。

最后回到 SimpleExecurtor 的 close 方法中,在执行完事务的close 方法后,在finally块中将所有应用置为null,等待GC回收。清除工作也就完毕了。

到这里 SqlSession的运行就基本结束了。

最后返回到我们的main方法,打印输出。

我们再看看这行代码,这么一行简单的代码里面 mybatis 为我们封装了无数的调用。可不简单。

UserInfo userInfo1 = sqlSession.selectOne(“org.apache.ibatis.mybatis.UserInfoMapper.selectById”, parameter);

7. 总结

今天我们从一个小demo开始 debug mybatis 源码,从如何加载配置文件,到如何创建SqlSedssionFactory,再到如何创建 SqlSession,再到 SqlSession 是如何执行的,我们知道了他们的生命周期。

其中创建SqlSessionFactory 和 SqlSession 是比较简单的,执行SQL并封装返回值是比较复杂的,因为还需要配置事务,日志,插件等工作。

还记得我们刚开始说的吗?mybatis 做的什么工作?

  1. 根据 JDBC 规范 建立与数据库的连接。
  2. 通过反射打通Java对象和数据库参数和返回值之间相互转化的关系。

还有Mybatis 的运行过程?

  1. 读取配置文件创建 Configuration 对象, 用以创建 SqlSessionFactroy.
  2. SQLSession 的执行过程.

我们也知道了其实在mybatis 层层封装下,真正做事情的是 StatementHandler,他下面的各个实现类分别代表着不同的SQL声明,我们看看他有哪些属性就知道了:

该类可以说囊括了所有执行SQL的必备属性:配置,对象工厂,类型处理器,结果集处理器,参数处理器,SQL执行器,映射器(保存这个SQL 所有相关属性的地方,比放入SQL语句,参数,返回值类型,配置,id,声明类型等等), 分页对象, 绑定SQL与参数对象。有了这些东西,还有什么SQL执行不了的呢?

当然,StatementHandler 只是 SqlSession 4 大对象的其中之一,还有Executor 执行器,他负责调度 StatementHandler,ParameterHandler,ResultHandler 等来执行对应的SQL,而 StatementHandler 的作用是使用数据库的 Statement(PreparedStatement ) 执行操作。

它是4大对象的核心,起到承上启下的作用。ParameterHandler 就是封装了对参数的处理,ResultHandler 封装了对结果级别的处理。

到这里,我们这篇文章就结束了,当然,大家肯定还想知道 getMapper 的原理是怎么回事,其实我们开始说过,getMapper 更加的面向对象,但也是对上面的代码的封装。

END

本文分享自微信公众号 - Java研发军团(ityuancheng),作者:莫那·鲁道

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2018-09-14

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Java的方法详解

    在前面几个章节中我们经常使用到 System.out.println(),那么它是什么呢?

    用户5224393
  • Java面试题-基础篇二

    不可以。因为非static方法是要与对象关联在一起的,必须创建一个对象后,才可以在该对象上进行方法调用,而static方法调用时不需要创建对象,可以直接调用。

    用户5224393
  • 初学者第70节网络编程-Socket(一)

    java.net 包中 J2SE 的 API 包含有类和接口,它们提供低层次的通信细节。你可以直接使用这些类和接口,来专注于解决问题,而不用关注通信细节。

    用户5224393
  • 《Effective Java》——读后总结

    这本书在Java开发的行业里,颇有名气。今天总算是粗略的看完了...后面线程部分和序列化部分由于心浮气躁看的不仔细。这个月还剩下一周,慢慢总结消化。

    Java后端工程师
  • PHP strtotime(date('Y-m-d') . ' 00:00:00')获取时间戳不准确的问题

    今天遇到一个BUG,在使用strtotime(date('Y-m-d') . ' 00:00:00') 获取当天零点时间戳会出现不准确的问题,有时候获取的是正常...

    用户2475223
  • 持续集成之代码质量管理———Sonar

    Sonar是一个用于代码质量管理的开放平台,通过插件机制,Sonar可以集成不同的测试工具、代码分析工具以及持续集成工具。与持续集成工具(如Hudson/Jen...

    小手冰凉
  • GitHub 标星 3.2w!史上最全技术面试手册!

    简历怎么写才能吸引 HR 的眼光,可能会被技术老大问到哪些常见问题,拿到 Offer 之后怎样才能让自己的优势最大化然后优中选优?

    GitHubDaily
  • PHP 日期相关函数

    设置时区 date_default_timezone_get(); date_default_timezone_set('PRC'); 时间戳 time();...

    康怀帅
  • Python任务调度模块 – APScheduler,Flask-APScheduler实现定时任务

      看代码,定义一个函数,然后定义一个scheduler类型,添加一个job,然后执行,就可以了,代码是不是超级简单,而且非常清晰。看看结果吧。

    用户1214487
  • C# 中的 Out 和 Ref 及Params 参数

    跟着阿笨一起玩NET

扫码关注云+社区

领取腾讯云代金券