Mybatis3详解(十九)----SqlSession下的四大对象(Executor、StatementHandler、ParameterHandler和ResultSetHandler)
1、SqlSession下的四大对象介绍
通过前面的分析,我们应该知道在Mybatis中的,首先是通过SqlSessionFactoryBuilder加载全局配置文件(包括SQL映射器),这些配置都会封装在Configuration中,其中每一条SQL语句的信息都会封装在MappedStatement中。然后创建SqlSession,这时还会初始化Executor执行器。最后通过调用sqlSession.getMapper()来动态代理执行Mapper中对应的SQL语句。而当一个动态代理对象进入到了MapperMethod的execute()方法后,它经过简单地判断就进入了SqlSession的delete、update、insert、select等方法,这里是真正执行SQL语句的地方。那么这些方法是如何执行呢?答:实际上SqlSession的执行过程是通过Executor、StatementHandler、ParameterHandler和ResultSetHandler来完成数据库操作和结果返回的,它们简称为四大对象:
- Executor:代表执行器,由它调度StatementHandler、ParameterHandler、ResultSetHandler等来执行对应的SQL,其中StatementHandler是最重要的。
- StatementHandler:作用是使用数据库的Statement(PreparedStatement)执行操作,它是四大对象的核心,起到承上启下的作用,许多重要的插件都是通过拦截它来实现的。
- ParameterHandler:是用来处理SQL参数的。
- ResultSetHandler:是进行数据集(ResultSet)的封装返回处理的,它非常的复杂,好在不常用。
这四个对象都是通过Mybatis的插件来完成的,在实例化Executor、StatementHandler、ParameterHandler、ResultSetHandler四大接口对象的时候都是调用interceptorChain.pluginAll() 方法插入进去的(Mybatis的插件实现原理可以参考:链接1 ,链接2 )。下面依次分析这四大对象的生成和运作原理。
2、四大对象的生成和运作原理的引入
这里来分析一条查询SQL的执行过程:首先肯定是要构建SqlSessionFactory、SqlSession和动态代理Mapper对象的,前面已经介绍过了,所以不多说,主要来看具体是怎么样执行的。当动态代理对象获取MapperMethod对象后,通过其内部的execute()方法调用sqlSession.selectList()方法来真正执行SQL,所以继续从这里来跟踪代码:
SqlSession的实现类为DefaultSqlSession,所以去DefaultSqlSession中查看selectList()方法:
***********************************************************
可以看到这里获取了MappedStatement对象,并且调用了executor对象的query()方法来执行SQL。所以我们来看看Executor类。
3、Executor对象
Executor表示执行器,它是真正执行Java和数据库交互的对象,所以它十分重要,每一个SqlSession都会拥有一个Executor对象,这个对象负责增删改查的具体操作,我们可以简单的将它理解为JDBC中Statement的封装版。。Executor的关系图如下:
- BaseExecutor:是一个抽象类,采用模板方法的设计模式。它实现了Executor接口,实现了执行器的基本功能。
- SimpleExecutor:最简单的执行器,根据对应的SQL直接执行即可,不会做一些额外的操作;拼接完SQL之后,直接交给 StatementHandler 去执行。
- BatchExecutor:批处理执行器,用于将多个SQL一次性输出到数据库,通过批量操作来优化性能。通常需要注意的是批量更新操作,由于内部有缓存的实现,使用完成后记得调用flushStatements来清除缓存。
- ReuseExecutor :可重用的执行器,重用的对象是Statement,也就是说该执行器会缓存同一个sql的Statement,省去Statement的重新创建,优化性能。内部的实现是通过一个HashMap来维护Statement对象的。由于当前Map只在该session中有效,所以使用完成后记得调用flushStatements来清除Map。调用实现的四个抽象方法时会调用 prepareStatement()
- CachingExecutor:启用于二级缓存时的执行器;采用静态代理;代理一个 Executor 对象。执行 update 方法前判断是否清空二级缓存;执行 query 方法前先在二级缓存中查询,命中失败再通过被代理类查询。
我们来看看Mybatis是如何创建Executor的,其实在前面已经介绍过了,它是在Configuration类中完成的,这里不看可以跳过:
Executor对象会在MyBatis加载全局配置文件时初始化,它会根据配置的类型去确定需要创建哪一种Executor,我们可以在全局配置文件settings元素中配置Executor类型,setting属性中有个defaultExecutorType,可以配置如下3个参数:
- SIMPLE: 简易执行器,它没有什么特别的,默认执行器
- REUSE:是一种能够执行重用预处理语句的执行器
- BATCH:执行器重用语句和批量更新,批量专用的执行器
默认使用SimpleExecutor。而如果开启了二级缓存,则用CachingExecutor进行包装,SqlSession会调用CachingExecutor执行器的query()方法,先从二级缓存获取数据,当无法从二级缓存获取数据时,则委托给BaseExecutor的子类进行操作,CachingExecutor执行过程代码如下:
如果没有使用二级缓存并且没有配置其它的执行器,那么MyBatis默认使用SimpleExecutor,调用父类BaseExecutor的query()方法:
注意:query方法有两种形式,一种是直接查询;一种是从缓存中查询,下面来看一下源码:
当有一个查询请求访问的时候,如果开启了二级缓存,首先会经过Executor的实现类CachingExecutor,先从二级缓存中查询SQL是否是第一次执行,如果是第一次执行的话,那么就直接执行SQL语句,并创建缓存,如果第二次访问相同的SQL语句的话,那么就会直接从缓存中提取。如果没有开启二级缓存,是第一次执行,直接执行SQL语句,并创建缓存,再次执行则从一级缓存中获取数据,如下源代码所示:
而如果一级缓存也没有数据,则调用queryFromDatabase()从数据库中获取数据:
在queryFromDatabase()方法中调用SimpleExecutor的 doQuery() 方法(注意:这里说是调用了SimpleExecutor的方法,但是还在BaseExecutor类中是因为SimpleExecutor继承了它,所以SimpleExecutor对象中也有这个方法,而doQuery()方法在子类SimpleExecutor实现的,所以说是调用SimpleExecutor的 doQuery() 方法。),其方法代码如下:
这里显然是根据Configuration对象来构建StatementHandler,然后使用prepareStatement()方法对SQL编译和参数进行初始化。实现过程是:它调用了StatementHandler的prepare() 进行了预编译和基础的设置,然后通过StatementHandler的parameterize()来设置参数,这个parameterize()方法实际是通过ParameterHandler来对参数进行设置。最后使用 StatementHandler的query()方法,把ResultHandler传递进去,执行查询后再通过ResultSetHandler封装结果并将结果返回给调用者来完成一次查询,这样焦点又转移到了 StatementHandler 对象上。所以通过以上流程发现,MyBatis核心工作实际上是由Executor、StatementHandler、ParameterHandler和ResultSetHandler四个接口完成的,掌握这四个接口的工作原理,对理解MyBatis底层工作原理有很大帮助。
4、StatementHandler对象
StatementHandler是数据库会话器,顾名思义,数据库会话器就是专门处理数据库会话的,相当于JDBC中的Statement(PreparedStatement)。StatementHandler的关系图如下:
StatementHandler接口设计采用了适配器模式,其实现类RoutingStatementHandler根据上下文来选择适配器生成相应的StatementHandler。三个适配器分别是SimpleStatementHandler、PreparedStatementHandler和CallableStatementHandler。
- BaseStatementHandler: 是一个抽象类,它实现了StatementHandler接口,用于简化StatementHandler接口实现的难度,采用适配器设计模式,它主要有三个实现类SimpleStatementHandler、PreparedStatementHandler和CallableStatementHandler。
- SimpleStatementHandler: 最简单的StatementHandler,处理不带参数运行的SQL,对应JDBC的Statement
- PreparedStatementHandler: 预处理Statement的handler,处理带参数允许的SQL, 对应JDBC的PreparedStatement(预编译处理)
- CallableStatementHandler:存储过程的Statement的handler,处理存储过程SQL,对应JDBC的CallableStatement(存储过程处理)
- RoutingStatementHandler:RoutingStatementHandler根据上下文来选择适配器生成相应的StatementHandler。三个适配器分别是SimpleStatementHandler、PreparedStatementHandler和CallableStatementHandler。
StatementHandler 接口中的方法如下:
StatementHandler的初始化过程如下(它也是在Configuration对象中完成的):
当调用到doQuery()方法时内部会通过configuration.newStatementHandler()方法来创建StatementHandler对象。
可以发现MyBatis生成StatementHandler代码中,创建的真实对象是一个RoutingStatementHandler的对象,而不是其它三个对象中的。但是RoutingStatementHandler并不是真实的服务对象,它是通过适配器模式来找到对应的StatementHandler来执行的。在初始化RoutingStatementHandler对象时,它会根据上下文环境来决定创建哪个具体的StatementHandler。RoutingStatementHandler 的构造方法如下:
它内部定义了一个对象的适配器delegate,它是一个StatementHandler接口对象,然后构造方法根据配置来配置对应的StatementHandler对象。它的作用是给3个接口对象的使用提供一个统一且简单的适配器。此为对象的配适,可以使用对象配适器来尽可能地使用己有的类对外提供服务,可以根据需要对外屏蔽或者提供服务,甚至是加入新的服务。我们以常用的 PreparedStatementHandler 为例,看看Mybatis是怎么执行查询的。
继续跟踪到SimpleExecutor对象中的prepareStatement()方法:
可以发现Executor 执行查询时会执行 StatementHandler 的 prepare() 和 parameterize() 方法来对SQL进行预编译和参数的设置, 其中 PreparedStatementHandler 的 prepare 方法如下:
注意:这个 prepare 方法是先调用到 StatementHandler 的实现类 RoutingStatementHandler,再由RoutingStatementHandler 调用 BaseStatementHandler 中的 prepare 方法。
=====================================
通过prepare()方法,可知其中最重要的方法就是 instantiateStatement() 方法了,因为它要完成对SQL的预编译。在得到 Statement 对象的时候,会去调用 instantiateStatement() 方法,这个方法位于 BaseStatementHandler 中,是一个抽象方法由子类去实现,实际执行的是三种 StatementHandler 中的一种,我们以 PreparedStatementHandler 中的为例:
从上面的代码我们可以看到,instantiateStatement() 方法最终返回的也是Statement对象,所以经过一系列的调用会把创建好的 Statement 对象返回到 SimpleExecutor 简单执行器中,为后面设置参数的 parametersize 方法所用。也就是说,prepare 方法负责生成 Statement 实例对象,而 parameterize 方法用于处理 Statement 实例对应的参数。所以我们来看看parameterize 方法:
可以看到这里通过调用了ParameterHandler对象来设置参数,所以下面我们来介绍一下ParameterHandler对象。
5、ParameterHandler对象
ParameterHandler 是参数处理器,它的作用是完成对预编译的参数的设置,也就是负责为 PreparedStatement 的 SQL 语句参数动态赋值。ParameterHandler 相比于其他的组件就简单很多了,这个接口很简单只有两个方法:
这两个方法的含义为:
- getParameterObject: 用于获取参数对象。
- setParameters:用于设置预编译SQL的参数
ParameterHandler对象的创建,ParameterHandler参数处理器对象是在创建 StatementHandler 对象的同时被创建的,同样也是由 Configuration 对象负责创建:
可以发现在创建 ParameterHandler 对象时,传入了三个参数分别是:mappedStatement、parameterObject、boundSql。mappedStatement保存了一个映射器节点<select|update|delete|insert>中的内容,包括我们配置的SQL、SQL的id、缓存信息、resultMap、ParameterType、resultType、resultMap等重要配置内容等。parameterObject表示读取的参数。boundSql表示要实际执行的SQL语句,它是通过SqlSource 对象来生成的,就是根据传入的参数对象,动态计算这个 BoundSql, 也就是 Mapper 文件中节点的计算,是由 SqlSource 完成的,SqlSource 最常用的实现类是 DynamicSqlSource。然后就我们进入newParameterHandler()方法中:
上面是在 Configuration 对象创建 ParameterHandler 的过程,它实际上是交由 LanguageDriver 来创建具体的参数处理器,LanguageDriver 默认的实现类是 XMLLanguageDriver,由它调用 DefaultParameterHandler 中的构造方法完成 ParameterHandler 的创建工作:
上面创建完成之后,该进行具体的解析工作,那么 ParameterHandler 如何解析SQL中的参数呢?ParameterHandler 由实现类DefaultParameterHandler执行,使用TypeHandler将参数对象类型转换成jdbcType,完成预编译SQL的参数设置,这是在setParameters()方法中完成的,setParameters()方法的实现如下:
至此,我们的参数就处理完成了。一切都准备就绪之后就肯定可以执行了呗!在SimpleExecutor 的 doQuery()方法中最后会调用query()方法来执行SQL语句。并且把创建好的Statement和结果处理器以参数传入进去,我们进入query()方法:
可以看到这里执行了我们的SQL语句,然后对执行的结果进行处理,这里用到的是MyBatis 四大对象的最后一个神器也就是 ResultSetHandler,所以下面我们继续来介绍ResultSetHandler对象。
6、ResultSetHandler对象
ResultSetHandler 是结果处理器,它是用来组装结果集的。ResultSetHandler 接口的定义也挺简单的,只有三个方法:
ResultSetHandler 对象的创建,ResultSetHandler 对象是在处理查询请求时创建 StatementHandler 对象同时被创建的,同样也是由 Configuration 对象负责创建,示例如下:
Configuration对象中的newResultSetHandler()方法:
ResultSetHandler 创建好之后就可以处理结果映射了。还记得在前面Executor的doQuery()方法中,我们最后是通过调用handler.query()方法来完成结果集的处理,如下:
进入query()方法,它是在PreparedStatementHandler实现的:
ResultSetHandler接口只有一个默认的实现类是DefaultResultSetHandler,我们通过SELECT语句执行得到的结果集由其 handleResultSets() 方法处理,方法如下:
上面涉及的主要对象有:
ResultSetWrapper:结果集的包装器,主要针对结果集进行的一层包装。这个类中的主要属性有:
- ResultSet : Java JDBC ResultSet接口表示数据库查询的结果。 有关查询的文本显示了如何将查询结果作为java.sql.ResultSet返回。 然后迭代此ResultSet以检查结果。
- TypeHandlerRegistry: 类型注册器,TypeHandlerRegistry 在初始化的时候会把所有的 Java类型和类型转换器进行注册。
- ColumnNames: 字段的名称,也就是查询操作需要返回的字段名称
- ClassNames: 字段的类型名称,也就是 ColumnNames 每个字段名称的类型
- JdbcTypes: JDBC 的类型,也就是java.sql.Types 类型
ResultMap:负责处理更复杂的映射关系
multipleResults:用于封装处理好的结果集。其中的主要方法是 handleResultSet:
可以看到,handleResultSet() 方法中又分为:嵌套和不嵌套处理这两种方法,这里我们只管理不嵌套的处理,嵌套的虽然会比不嵌套复杂一点,但总体类似,差别并不大。
最后 handleResultSets() 方法返回的是 collapseSingleResultList(multipleResults) ,我们来看一下:
它是判断的 multipleResults 的数量,如果数量是 1 ,就直接取位置为0的元素,如果不是1,那就返回 multipleResults 的真实数量。
以上在 DefaultResultSetHandler 中处理完结果映射,并把上述得到的结果返回给调用的客户端,从而执行完成一条完整的SQL语句。结果集的处理就看到这里了,因为ResultSetHandler的实现非常复杂,它涉及了CGLIB或者JAVASSIST作为延迟加载,然后通过typeHandler和ObjectFactory解析组装结果在返回,由于实际工作需要改变它的几率不高加上他比较复杂,所以这里就不在论述了,有兴趣的可自行去百度信息。
8、小结
一条SQL语句在Mybatis中的执行过程小结:首先是创建Mapper的动态代理对象MapperProxy,然后将Mappe接口中的方法封装至MapperMethod对象,通过MapperMethod对象中的execute()方法来执行SQL,其本质是通过SqlSession下的方法来实现的,SQL语句的具体的执行则是通过SqlSession下的四大对象来完成。Executor先调用StatementHandler的prepare()方法预编译SQL,然后用parameterize()方法启用ParameterHandler设置参数,完成预编译,执行查询,update()也是这样的。如果是查询,MyBatis则会使用ResultSetHandler来封装结果并返回给调用者,从而完成查询。其执行的完整流程图如下:
一个超级详细解析图 (图片来自《Mybatis:执行一个Sql命令的完整流程》):
参考链接:
- 《Java EE 互联网轻量级框架整合开发》
- https://www.cnblogs.com/abcboy/p/9656302.html
- https://www.cnblogs.com/cxuanBlog/tag/MyBatis/