MyBatis原理(三) —— 执行

75 阅读9分钟

我正在参加「掘金·启航计划」

前言

前两篇文章已经将资源加载好了,剩下就该“主厨”登场了。这篇我们就来说说MyBatis是如何执行一条语句的。

1. SqlSession

在了解如何执行前,得先知道SqlSession是个啥东东。先举个生活中的例子吧!我们去大一点的餐厅吃饭的时候,都是找服务员点菜而不是直接去后厨跟厨师联系,这种情况下,服务员就可以看做是SqlSession角色。

通过服务员可以点菜,结账等。映射成SqlSession可以执行命令,管理事务等。

SqlSessionSessionFactory创建,默认实现是DefaultSqlSessionFactory。由工厂名称就能知道创建的DefaultSqlSession。我们就以此为路线进行展开,先看下类图:

SqlSession.png

咋还有个Executor?别急,这就是我们接下来要说的

2. Executor

前文说过,SqlSession类似于服务员,那服务员有了,那厨师呢?没错,正是Executor。服务员在点完菜后,交由厨师处理;SqlSessionExecutor间的关系也一样。

大点饭店肯定不止一个厨师,我们的Executor也肯定不止一个。

Executor.png 上面就是我们所有“厨师们”,每个厨师的拿手菜也不一样,对应的就是每个Executor功能也不一样。至于厨师是哪个拿手菜,后续会提到的~

做个小总结:SqlSessionMyBatis开放给用户的入口,通过该入口可以执行sql命令和管理事务;SqlSession会把收到的任务交给Executor

3. 创建

熟悉以上说明后,我们看看对应对象都是怎么创建的。核心方法在DefaultSqlSessionFactory#openSessionFromDataSource(),代码如下图所示:

openSessionFromDataSource() 在使用无参创建时参数值都是默认的,level的参数值nullExecutorType.TypeExecutorType.SIMPLEautoCommitfalse。 通过之前在environments标签配置的属性创建对应对象,本文使用的环境是JDBCPOOLED

3.1 configuration.newExecutor()

从上述逻辑看,MyBatis是先招“厨师”,再和“服务员”搭配。后面就比较正经了,不说啥比喻了。

newExecutor() 分析上图代码,根据不同的ExecutorType创建不同的Executor,这里我们就走默认的Executor,默认的分析完,其他的都是信手拈来的事。

cacheEnabled默认true,也就是说默认会对SimpleExecutor装饰一个CachingExecutor,这玩意就是二级缓存。这时候就有人要说了:啊,我都没在mapper文件中配置<cache/>标签,咋还会配置二级缓存?我们稍安勿躁,后续答案自然浮出水面。

接下来就是装载拦截器了,至于拦截器的如何使用,可参阅文档,我们关注的是如何拦截。

前置知识:MyBatis会对ExecutorParameterHandler...等类对不同的方法进行拦截,此处是ExecutorInterceptor接口有一个默认方法plugin(),该方法使用的是Plugin.wrap()方法,而Plugin是一个InvocationHandler类,看到InvocationHandler就知道是代理了。看看核心方法:

Plugin.wrap() 核心代码已有注释,节省篇幅就不详细分析了,这里做个总结:

  1. getSignatureMap()是获取所有注解内容,然后反射获取方法结构,形成keyclass对象,value为该class对象中所有需要拦截的方法。
  2. type实现的接口和其父类实现的接口在signatureMap中的保存到一个数组中。
  3. 对于步骤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() 现在先不用管那么多,只需知道:MapperProxy是一个代理对象,内部保存了SqlSession,对应接口class对象,methodCache先不管size是0。

小总结:通过MapperProxyFactory创建接口代理对象MapperProxy

5. 方法匹配

接口实现都有了,那就可以调用我们写的方法了。而接口实现又是一个代理对象,我们的方法调用都会到MapperProxy中去执行。所以接下来就可以到代理对象中看看,方法是怎样执行的。

invoke方法如下:

MapperProxy#invoke()

对于Object中的方法是不做任何处理的。else才是处理我们自定义方法的。先看cachedInvoker()方法,看名字就知道是和缓存有关的,该方法会从methodCache中获取一个MapperMethodInvoker,如果没有获取到则新建一个PlainMethodInvoker并保存在该缓存中。

methodCacheproxy创建时就已经有了。PlainMethodInvoker是接口MapperMethodInvoker的实现。cachedInvoker()返回的就是PlainMethodInvoker(大部分情况)。而PlainXXx又将方法的调用委托给了MapperMethod对象。可能有点乱,看下关系图

MapperProxy.png

MapperMethodInvoker就是一个方法执行器,用这个执行器,将方法处理委托给不同对象。而MapperMethod类则将一个数据库操作语句和一个Java方法绑定在了一起。

知道了以这些内容,那我们关注点自然就是MapperMethod了。SqlCommand可以表示一条操作语句,内部保存了sql类型和对应的MappedStatement.IdMethodSignature表示的就是对应方法签名的信息,例如返回类型,方法上的注解等等。

小总结:MyBatis使用MapperProxy生成保存接口和标签对应关系的类MapperMethod,并且会使用缓存保存这种对应关系。

6. 执行

上文已经讲和如何讲接口和mapper标签对应起来。接下来就可以看看是咋执行的了。先看MapperMethod中的execute方法(简化后):

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()了。代码如下:

sqlSession.selectOne() 看来用的也是selectList()方法,在结果大于1时抛出异常。最终方法其实是下面这个:

selectList 对于RowBoundsResultHandler不了解的同学,可以在网上找篇文章学习下。

wrapException()用来处理参数是数组,集合,map的情况。executor还记得是被我们使用了两个拦截器进行生成的Plugin代理吗?所以接下来会跑到Plugin执行invoke方法。

invoke()方法内会判断当前执行的和拦截器注解定义的是否匹配,匹配则执行。依次执行完所有拦截器后,最终会执行CachingExecutor中的query()方法。

在这执行之前还生成了BoundSqlCacheKey对象,BoundSql可以看成是SqlSource和参数真实值的整合,毕竟之前只是保存了解析crud标签内容。CacheKey通过各个参数值进行计算生成该对象作为缓存键。

query() 以上就是执行的关键点了。上述代码逻辑简单,相信大家一看就能看懂。值得注意的是,如果使用了ResultHandler的话,是不能使用缓存的。 同时此处也不是查询完就保存至缓存,而是存储在了一个待保存的Map中,关于这点,网上也很多文章,有兴趣的也可以了解下TransactionalCacheManager类。

随后会到BaseExecutor执行一级缓存有关的内容,利用模板方法又会回到我们SimpleExecutordoQuery()方法中。

doQuery() 看似只有三行代码,但内部流程还是挺多的,节省篇幅,做个总结:

  1. 生成RoutingStatementHandler对象。
  2. prepareStatement()会获取对象连接,在RoutingStatementHandler中根据statementType属性创建不同XXXStatementHandler(策略模式的体现)。通过Handler创建对应的Statement并设置一些Timeout之类的参数。
  3. 使用DefaultParameterHandler设置PreparedStatement中的?,内部会推断参数类似使用合适的TypeHandler进行设置。

上述只是概述了整体流程,要是细说一篇文章又来了。如果日志是debug级别还会使用带日志输出的代理对象如:ConnectionLoggerPreparedStatementLoggerStatementLogger

接下来就是处理结果映射了,但其内部实现流程较多(支持的映射功能多),要是写起来,不知又会绕到哪里去,所以本篇就不写了~

7. 尾声

前面说过Executor有多个实现,每个实现都有不同的用处,这边就说明一下。首先是ReuseExecutor,使用了一个HashMap保存创建的Statement对象,keysql字符串。BatchExecutor可批量执行的Executor,对同MappedStatement下的相同sql可以进行批量处理(只针对更新类操作)。

8. 总结

在阅读源码前,至少需要把各个功能都能熟练使用,不然看到一块代码是处理啥的都不清楚。

MyBatis提供的扩展点还是挺多的,像各种XXXFactory之类的,但较为强大的还是Plugin机制,这使得开发人员能够改变MyBatis的运行行为。