我正在参加「掘金·启航计划」
前言
前两篇文章已经将资源加载好了,剩下就该“主厨”登场了。这篇我们就来说说MyBatis
是如何执行一条语句的。
1. SqlSession
在了解如何执行前,得先知道SqlSession
是个啥东东。先举个生活中的例子吧!我们去大一点的餐厅吃饭的时候,都是找服务员点菜而不是直接去后厨跟厨师联系,这种情况下,服务员就可以看做是SqlSession
角色。
通过服务员可以点菜,结账等。映射成SqlSession
可以执行命令,管理事务等。
SqlSession
由SessionFactory
创建,默认实现是DefaultSqlSessionFactory
。由工厂名称就能知道创建的DefaultSqlSession
。我们就以此为路线进行展开,先看下类图:
咋还有个Executor
?别急,这就是我们接下来要说的
2. Executor
前文说过,SqlSession
类似于服务员,那服务员有了,那厨师呢?没错,正是Executor
。服务员在点完菜后,交由厨师处理;SqlSession
和Executor
间的关系也一样。
大点饭店肯定不止一个厨师,我们的Executor
也肯定不止一个。
上面就是我们所有“厨师们”,每个厨师的拿手菜也不一样,对应的就是每个
Executor
功能也不一样。至于厨师是哪个拿手菜,后续会提到的~
做个小总结:SqlSession
是MyBatis
开放给用户的入口,通过该入口可以执行sql
命令和管理事务;SqlSession
会把收到的任务交给Executor
。
3. 创建
熟悉以上说明后,我们看看对应对象都是怎么创建的。核心方法在DefaultSqlSessionFactory#openSessionFromDataSource()
,代码如下图所示:
在使用无参创建时参数值都是默认的,
level
的参数值null
,ExecutorType.Type
为ExecutorType.SIMPLE
,autoCommit
为false
。 通过之前在environments
标签配置的属性创建对应对象,本文使用的环境是JDBC
和POOLED
。
3.1 configuration.newExecutor()
从上述逻辑看,MyBatis
是先招“厨师”,再和“服务员”搭配。后面就比较正经了,不说啥比喻了。
分析上图代码,根据不同的
ExecutorType
创建不同的Executor
,这里我们就走默认的Executor
,默认的分析完,其他的都是信手拈来的事。
cacheEnabled
默认true
,也就是说默认会对SimpleExecutor
装饰一个CachingExecutor
,这玩意就是二级缓存。这时候就有人要说了:啊,我都没在mapper
文件中配置<cache/>
标签,咋还会配置二级缓存?我们稍安勿躁,后续答案自然浮出水面。
接下来就是装载拦截器了,至于拦截器的如何使用,可参阅文档,我们关注的是如何拦截。
前置知识:MyBatis
会对Executor
,ParameterHandler
...等类对不同的方法进行拦截,此处是Executor
。Interceptor
接口有一个默认方法plugin()
,该方法使用的是Plugin.wrap()
方法,而Plugin
是一个InvocationHandler
类,看到InvocationHandler
就知道是代理了。看看核心方法:
核心代码已有注释,节省篇幅就不详细分析了,这里做个总结:
getSignatureMap()
是获取所有注解内容,然后反射获取方法结构,形成key
为class
对象,value
为该class
对象中所有需要拦截的方法。type
实现的接口和其父类实现的接口在signatureMap中的保存到一个数组中。- 对于步骤3有结果的,会返回代理对象。
假设我们有一个Interceptor
的实现,此时targe
代表的是CachingExecutor
。如果该Interceptor
配置了对Executor
拦截则会返回代理对象,否则返回CachingExecutor
,过滤器会对剩下的依次执行上述步骤,但是如果有两个过滤器都符合生成代理对象条件,则处理第二个过滤器时,target
为上一个生成的代理对象。
如果有多个拦截器拦截Executor
的话interceptorChain.pluginAll(executor)
生成的就是代理的代理。后添加的拦截器先执行。 不知道大家有没有get
到(debug
起来吧)。
针对Executor
的创建就完成了,代理的代理需要理解,不然调用Executor
时不知道跑哪去了。我们下面的流程就以两个拦截Executor
的拦截器为例吧。
3.2 new DefaultSqlSession()
SqlSession
的创建非常朴实无华,直接new
的,不做过多讲解了。
4. getMapper()
mapper
文件内容解析成对象保存在configuration
中了,SqlSession
也创建好了,接下来就可以通过使用SqlSession
提供的方法进行各种操作了。
一般我们使用MyBatis
都是获取接口实现,然后再通过接口去使用我们的方法,这节就看看MyBatis
如何为我们生成接口的实现。
还记得上篇文章中knownMappers.put(type, new MapperProxyFactory<>(type))
这句代码吗?MyBatis
创建接口实现靠的就是MapperProxyFactory
。
核心代码如下:
现在先不用管那么多,只需知道:
MapperProxy
是一个代理对象,内部保存了SqlSession
,对应接口class
对象,methodCache
先不管size
是0。
小总结:通过MapperProxyFactory
创建接口代理对象MapperProxy
。
5. 方法匹配
接口实现都有了,那就可以调用我们写的方法了。而接口实现又是一个代理对象,我们的方法调用都会到MapperProxy
中去执行。所以接下来就可以到代理对象中看看,方法是怎样执行的。
invoke
方法如下:
对于Object
中的方法是不做任何处理的。else
才是处理我们自定义方法的。先看cachedInvoker()
方法,看名字就知道是和缓存有关的,该方法会从methodCache
中获取一个MapperMethodInvoker
,如果没有获取到则新建一个PlainMethodInvoker
并保存在该缓存中。
methodCache
在proxy
创建时就已经有了。PlainMethodInvoker
是接口MapperMethodInvoker
的实现。cachedInvoker()
返回的就是PlainMethodInvoker
(大部分情况)。而PlainXXx
又将方法的调用委托给了MapperMethod
对象。可能有点乱,看下关系图
MapperMethodInvoker
就是一个方法执行器,用这个执行器,将方法处理委托给不同对象。而MapperMethod
类则将一个数据库操作语句和一个Java
方法绑定在了一起。
知道了以这些内容,那我们关注点自然就是MapperMethod
了。SqlCommand
可以表示一条操作语句,内部保存了sql
类型和对应的MappedStatement.Id
。MethodSignature
表示的就是对应方法签名的信息,例如返回类型,方法上的注解等等。
小总结:MyBatis
使用MapperProxy
生成保存接口和标签对应关系的类MapperMethod
,并且会使用缓存保存这种对应关系。
6. 执行
上文已经讲和如何讲接口和mapper
标签对应起来。接下来就可以看看是咋执行的了。先看MapperMethod
中的execute
方法(简化后):
这不是一眼就懂?根据不同标签类型进行不同处理。在省略的
SELECT
匹配中还进行了返回值和参数之类的判断,例如参数是否包含ResultHandler
,是否返回Map
等等。我们就以最后else
为例。看看是如何执行的,代码如下:
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()&& (result == null ||!method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
method.converArgsToSqlCommandParam()
是解析参数对象,生成合适的参数名和值的对应关系,逻辑挺简单的,感兴趣的可以自行看看。
核心方法自然就是sqlSession.selectOne()
了。代码如下:
看来用的也是
selectList()
方法,在结果大于1时抛出异常。最终方法其实是下面这个:
对于
RowBounds
和ResultHandler
不了解的同学,可以在网上找篇文章学习下。
wrapException()
用来处理参数是数组,集合,map
的情况。executor
还记得是被我们使用了两个拦截器进行生成的Plugin
代理吗?所以接下来会跑到Plugin
执行invoke
方法。
在invoke()
方法内会判断当前执行的和拦截器注解定义的是否匹配,匹配则执行。依次执行完所有拦截器后,最终会执行CachingExecutor
中的query()
方法。
在这执行之前还生成了BoundSql
和CacheKey
对象,BoundSql
可以看成是SqlSource和参数真实值的整合,毕竟之前只是保存了解析crud
标签内容。CacheKey
通过各个参数值进行计算生成该对象作为缓存键。
以上就是执行的关键点了。上述代码逻辑简单,相信大家一看就能看懂。值得注意的是,如果使用了
ResultHandler
的话,是不能使用缓存的。 同时此处也不是查询完就保存至缓存,而是存储在了一个待保存的Map
中,关于这点,网上也很多文章,有兴趣的也可以了解下TransactionalCacheManager
类。
随后会到BaseExecutor
执行一级缓存有关的内容,利用模板方法又会回到我们SimpleExecutor
的doQuery()
方法中。
看似只有三行代码,但内部流程还是挺多的,节省篇幅,做个总结:
- 生成
RoutingStatementHandler
对象。 prepareStatement()
会获取对象连接,在RoutingStatementHandler
中根据statementType
属性创建不同XXXStatementHandler
(策略模式的体现)。通过Handler
创建对应的Statement
并设置一些Timeout
之类的参数。- 使用
DefaultParameterHandler
设置PreparedStatement
中的?
,内部会推断参数类似使用合适的TypeHandler
进行设置。
上述只是概述了整体流程,要是细说一篇文章又来了。如果日志是debug
级别还会使用带日志输出的代理对象如:ConnectionLogger
,PreparedStatementLogger
,StatementLogger
。
接下来就是处理结果映射了,但其内部实现流程较多(支持的映射功能多),要是写起来,不知又会绕到哪里去,所以本篇就不写了~
7. 尾声
前面说过Executor
有多个实现,每个实现都有不同的用处,这边就说明一下。首先是ReuseExecutor
,使用了一个HashMap
保存创建的Statement
对象,key
为sql
字符串。BatchExecutor
可批量执行的Executor
,对同MappedStatement
下的相同sql
可以进行批量处理(只针对更新类操作)。
8. 总结
在阅读源码前,至少需要把各个功能都能熟练使用,不然看到一块代码是处理啥的都不清楚。
MyBatis
提供的扩展点还是挺多的,像各种XXXFactory
之类的,但较为强大的还是Plugin
机制,这使得开发人员能够改变MyBatis
的运行行为。