我正在参加「掘金·启航计划」
前言
前两篇文章已经将资源加载好了,剩下就该“主厨”登场了。这篇我们就来说说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的运行行为。