1 Star 0 Fork 29

柚子 / notebook

forked from JustryDeng / notebook 
加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
[02]Mybatis之一个SQL的运行过程.md 41.19 KB
一键复制 编辑 原始数据 按行查看 历史
JustryDeng 提交于 2021-08-10 04:09 . Mybatis之一个SQL的运行过程

Mybatis之一个SQL的运行过程


阅前必读

  • 本文测试项目及相关总结资料,均放置在文末链接处。强烈建议去拽下来,看xmind脑图并结合源码进行理解

  • 本人是先绘制的xmind脑图,然后根据xmind脑图发的此博文,无论是可读性、还是层次感,xmind脑图都由于文字。

  • Mybatis中逻辑很多,而本文重点关注的是Mybatis中SQL相关的逻辑,其余部分会简述或直接略过。

  • 本文主要分享的内容是:

    • 启动项目时,与SQL相关的逻辑
    • 启动项目后,执行CURD方法时,与SQL相关的逻辑
    • 六问Mybatis插件
  • 文末链接指向的本人的测试项目,是长这样的:

    在这里插入图片描述

启动项目时,与SQL相关的逻辑

1.1、首先,Mybatis会将Mapper接口中,每个方法对应的MappedStatement实例存入org.apache.ibatis.session.Configuration#mappedStatements中

  其中,key{全类名}.{方法名},如:com.aspire.ssm.mapper.SqlTestMapper.selectAll,value该方法对应的MappedStatement对象

注:其实同一个方法,同一个value,会存两次,一个长key(如上),一个短key(短key,只有方法名)。

注:对应源码可详见:org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#parseStatement

  • 1.1.1、 启动时,会先解析出xml中的SQL对应的MappedStatement实例对象,可详见源码:org.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode。 具体的调用栈为:
     "main@1" prio=5 tid=0x1 nid=NA runnable
       java.lang.Thread.State: RUNNABLE
          at org.apache.ibatis.session.Configuration.addMappedStatement(Configuration.java:686)
          at org.apache.ibatis.builder.MapperBuilderAssistant.addMappedStatement(MapperBuilderAssistant.java:296)
          at org.apache.ibatis.builder.xml.XMLStatementBuilder.parseStatementNode(XMLStatementBuilder.java:110)
          at org.apache.ibatis.builder.xml.XMLMapperBuilder.buildStatementFromContext(XMLMapperBuilder.java:137)
          at org.apache.ibatis.builder.xml.XMLMapperBuilder.buildStatementFromContext(XMLMapperBuilder.java:130)
          at org.apache.ibatis.builder.xml.XMLMapperBuilder.configurationElement(XMLMapperBuilder.java:120)
          at org.apache.ibatis.builder.xml.XMLMapperBuilder.parse(XMLMapperBuilder.java:94)
          at org.apache.ibatis.builder.annotation.MapperAnnotationBuilder.loadXmlResource(MapperAnnotationBuilder.java:182)
          at org.apache.ibatis.builder.annotation.MapperAnnotationBuilder.parse(MapperAnnotationBuilder.java:129)
          at org.apache.ibatis.binding.MapperRegistry.addMapper(MapperRegistry.java:72)
          at org.apache.ibatis.session.Configuration.addMapper(Configuration.java:759)
          at org.mybatis.spring.mapper.MapperFactoryBean.checkDaoConfig(MapperFactoryBean.java:80)
          at org.springframework.dao.support.DaoSupport.afterPropertiesSet(DaoSupport.java:44)
  • 1.1.2、 后解析出注解(@Select、@Delete、@Update、@Insert、@SelectProvider、@DeleteProvider、@UpdateProvider、@InsertProvider)中的SQL对应的MappedStatement实例对象,可详见源码:org.apache.ibatis.builder.annotation.MapperAnnotationBuilder.parseStatement。具体的调用栈为:
     "main@1" prio=5 tid=0x1 nid=NA runnable
       java.lang.Thread.State: RUNNABLE
          at org.apache.ibatis.session.Configuration.addMappedStatement(Configuration.java:686)
          at org.apache.ibatis.builder.MapperBuilderAssistant.addMappedStatement(MapperBuilderAssistant.java:296)
          at org.apache.ibatis.builder.annotation.MapperAnnotationBuilder.parseStatement(MapperAnnotationBuilder.java:356)
          at org.apache.ibatis.builder.annotation.MapperAnnotationBuilder.parse(MapperAnnotationBuilder.java:139)
          at org.apache.ibatis.binding.MapperRegistry.addMapper(MapperRegistry.java:72)
          at org.apache.ibatis.session.Configuration.addMapper(Configuration.java:759)
          at org.mybatis.spring.mapper.MapperFactoryBean.checkDaoConfig(MapperFactoryBean.java:80)
          at org.springframework.dao.support.DaoSupport.afterPropertiesSet(DaoSupport.java:44)

1.2、其中,MappedStatement对象包含了许多与SQL相关的信息

在这里插入图片描述

1.3、其中,sql信息在org.apache.ibatis.mapping.SqlSource#getBoundSql返回的BoundSql对象中

在这里插入图片描述

1.4、由于SqlSource是一个接口,所以在项目启动时,放入Map中作为value的MappedStatement实例里的SqlSource对象其实是RawSqlSource或StaticSqlSource或ProviderSqlSource或DynamicSqlSource中的一个

在这里插入图片描述

  • 1.4.1、 RawSqlSource:内部持有了一个SqlSource,该SqlSource是StaticSqlSource实例。因为RawSqlSource在启动时就会计算出mapping(即:sql的最终的样子),所以其性能优于DynamicSqlSource。

    在这里插入图片描述 普通的SQL,会被封装为RawSqlSource或者DynamicSqlSource

    • 在这里插入图片描述
    • 在这里插入图片描述
    • 在这里插入图片描述
    • 在这里插入图片描述
    • 在这里插入图片描述
    • 在这里插入图片描述
  • 1.4.2、 DynamicSqlSource:动态SQL处理器,会处理${}、#{}等一系列SQL,最终处理完毕后,会以最终的SQL信息等为参数,new一个StaticSqlSource来作为最终查询时用的SqlSource。

    除了${}占位的普通SQL外,动态SQL全都会被封装为DynamicSqlSource

    • 在这里插入图片描述
    • 在这里插入图片描述
  • 1.4.3、 ProviderSqlSource:处理通过注解@InsertProvider、@DeleteProvider、@UpdateProvider、@SelectProvider写的SQL;ProviderSqlSource会转换为DynamicSqlSource或RawSqlSource,最终都会与StaticSqlSource关系起来。

    以下SQL,会被封装为ProviderSqlSource

    在这里插入图片描述

  • 1.4.4、 ProviderSqlSource:StaticSqlSource:静态的SqlSource,无论是RawSqlSource、DynamicSqlSource还是ProviderSqlSource,最终都会与StaticSqlSource关系起来。都会“转换”成StaticSqlSource。确切的说:RowSqlSource持有StaticSqlSource实例;DynamicSqlSource执行查询时,处理完动态SQL后会创建StaticSqlSource实例;ProviderSqlSource执行查询时,会转换为RowSqlSource或DynamicSqlSource。所以,MappedStatement#getBoundSql方法里面的sqlSource.getBoundSql(parameterObject),最终其实还是StaticSqlSource#getBoundSql

    注:RowSqlSource与DynamicSqlSource的区别是:SQL的结构会不会因为程序或参数值而变动。RowSqlSource:不会。DynamicSqlSource:会。

    注:在项目启动后,执行CURD时,ProviderSqlSource会被转换为RowSqlSource或DynamicSqlSource。

  • 1.4.5、 总结来说,即:

    在这里插入图片描述

启动项目后,执行CURD方法时,与SQL相关的逻辑

2.1、假设,程序调用了此方法

在这里插入图片描述

2.2、那么,第一步:程序调用Mapper中的CURD方法

  如:主动调用List selectAll_xml();方法进行查询。

2.3、第二步:由Mapper的代理对象,调用目标方法

  即com.sun.proxy.$Proxy86.selectAll_xml,实际上是由org.apache.ibatis.binding.MapperProxy.invoke进行调用的。

2.4、第三步:......(一堆咱们此次并不关注的逻辑)

2.5、第四步:获取方法对应的MappedStatement实例。以{全类名}.{方法名}为key,获取启动时存到org.apache.ibatis.session.Configuration#mappedStatements中的MappedStatement实例

在这里插入图片描述

2.6、第五步:通过MappedStatement实例获取BoundSql对象(BoundSql对象中包含了SQL信息)

  首先,org.apache.ibatis.mapping.MappedStatement#getBoundSql:

在这里插入图片描述

注:MappedStatement#getBoundSql方法中,BoundSql boundSql = sqlSource.getBoundSql(parameterObject)返回的BoundSql 对象里面的SQL,是没有占位符#{xxx}的,原SQL中的#{xxx}会被?代替;MappedStatement#getBoundSql方法返回的BoundSql对象里面的SQL,也是没有占位符#{xxx}的,原SQL中的#{xxx}会被?代替。

注:MappedStatement#getBoundSql方法中,BoundSql boundSql = sqlSource.getBoundSql(parameterObject)返回的BoundSql 对象里面的SQL,是没有占位符${xxx}的,原SQL中的${xxx}会直接被具体的参数值代替MappedStatement#getBoundSql方法返回的BoundSql对象里面的SQL,也是没有占位符${xxx}的,原SQL中的${xxx}会直接被具体的参数值代替。

  其次,我们知道在启动时,SQL信息被封装进的SqlSource实现只有RawSqlSource或DynamicSqlSource或ProviderSqlSource这三种,下面一次对他们进行分析。

  • 2.6.1、 RawSqlSource#getBoundSql:

    在这里插入图片描述

    RawSqlSource#getBoundSql的调用方法栈为: 在这里插入图片描述

  • 2.6.2、 DynamicSqlSource#getBoundSql: 在这里插入图片描述

    • 2.6.2.1、 其中,org.apache.ibatis.scripting.xmltags.SqlNode#apply实现了动态SQL:

      在这里插入图片描述

      SqlNode接口有很多实现: 在这里插入图片描述   从这些类的名字就可看出,这些类的功能了。他们实现了对xml中if标签、choose标签、foreach等标签的动态判断处理。

    • 2.6.2.2、 以IfSqlNode为例,讲解是如何实现动态SQL的:

      在这里插入图片描述

    • 2.6.2.3、 以IfSqlNode为例,讲解是如何实现动态SQL的:

    "main@1" prio=5 tid=0x1 nid=NA runnable
      java.lang.Thread.State: RUNNABLE
          // 如果进一步跟踪, 会发现就是: 如果满足条件的话,就使用StringBuilder#append拼接SQL
         at org.apache.ibatis.scripting.xmltags.DynamicContext.appendSql(DynamicContext.java:66)
         at org.apache.ibatis.scripting.xmltags.StaticTextSqlNode.apply(StaticTextSqlNode.java:30)
         at org.apache.ibatis.scripting.xmltags.MixedSqlNode.lambda$apply$0(MixedSqlNode.java:32)
         at org.apache.ibatis.scripting.xmltags.MixedSqlNode$$Lambda$389.584643821.accept(Unknown Source:-1)
         at java.util.ArrayList.forEach(ArrayList.java:1257)
    
         // 这里实现了动态SQL
         at org.apache.ibatis.scripting.xmltags.MixedSqlNode.apply(MixedSqlNode.java:32)
         at org.apache.ibatis.scripting.xmltags.DynamicSqlSource.getBoundSql(DynamicSqlSource.java:39)
         at org.apache.ibatis.mapping.MappedStatement.getBoundSql(MappedStatement.java:297)
    
    
         at com.github.pagehelper.PageInterceptor.intercept(PageInterceptor.java:82)
         at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:61)
         at com.sun.proxy.$Proxy99.query(Unknown Source:-1)
         at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:147)
         at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:140)
         at sun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
         at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
         at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
         at java.lang.reflect.Method.invoke(Method.java:498)
         at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:433)
         at com.sun.proxy.$Proxy81.selectList(Unknown Source:-1)
  • 2.6.3、 ProviderSqlSource#getBoundSql:

    在这里插入图片描述

    在这里插入图片描述

    其中invokeProviderMethod方法长这样:

    在这里插入图片描述

      所以,不论是RawSqlSource#getBoundSql还是DynamicSqlSource#getBoundSql还是ProviderSqlSource#getBoundSql,最终获得的都是StaticSqlSource#getBoundSql的结果。

2.7、第六步:执行查询

2.8、第七步:......(一堆咱们此次并不关注的逻辑)

六问Mybatis插件

3.1、第一问:如何自定义插件?

在这里插入图片描述 注:本文的重点不是介绍如何自定义插件的,所以这里就简单介绍了。

3.2、第二问:如何将自定义的插件交由mybatis?

  • 3.2.1、 方式一: 自定义插件,然后只需要将插件注册进入Spring容器即可,MybatisAutoConfiguration的会自动感知到容器中的插件,让后将其记录进org.apache.ibatis.session.Configuration#interceptorChain。

    • 3.2.1.1、 首先,自定义插件并将其注册进容器:

      在这里插入图片描述

    • 3.2.1.2、 然后,MybatisAutoConfiguration的构造器会自动感知到容器中的插件:

      在这里插入图片描述

      提示:org.mybatis.spring.boot:mybatis-spring-boot-autoconfigure依赖下的spring.factories文件中,指定了org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration,这就意味着程序启动时,会(考虑)自动注册MybatisAutoConfiguration类。

    • 3.2.1.3、 然后,在后面的逻辑中,会将感知到的插件记录进org.apache.ibatis.session.Configuration#interceptorChain。

  • 3.2.2、 方式二: 获取到容器中所有的SqlSessionFactory,然后通过SqlSessionFactory实例获取到Configuration实例,然后调用Configuration#addInterceptor添加自定义的拦截器。

    在这里插入图片描述

  • 3.2.3、 方式n: ......

3.3、第三问:插件是什么时候绑定到四大对象的?

提示一: 所有的Mybatis插件都会在Mybatis启动时,记录进org.apache.ibatis.session.Configuration#interceptorChain。

提示二: Mybatis插件是SqlSession级别的,所以在执行SQL时,才会在SqlSession中真正应用给四大拦截对象(Executor或StatementHandler或ParameterHandler或ResultSetHandler)

  • 3.3.1、 启动Mybatis时,记录所有插件:org.apache.ibatis.session.Configuration的interceptorChain属性实例中,维护了一个ArrayList;Mybatis所有的插件,都会在启动时记录进这个ArrayList中。

    在这里插入图片描述

    在这里插入图片描述

  • 3.3.2、 启动Mybatis后,执行SQL方法时,绑定插件给四大对象:

    • 3.3.2.1、 插件应用给Executor的时机:org.mybatis.spring.SqlSessionTemplate.SqlSessionInterceptor#invoke中getSqlSession时。更准确的说,是【时机是】Configuration#newExecutor时,可以看一下相关方法调用栈细节:

      "main@1" prio=5 tid=0x1 nid=NA runnable
        java.lang.Thread.State: RUNNABLE
             at org.apache.ibatis.plugin.InterceptorChain.pluginAll(InterceptorChain.java:30) // 初始化插件(将插件与Executor关联起来)
             at org.apache.ibatis.session.Configuration.newExecutor(Configuration.java:599)
             at org.apache.ibatis.session.defaults.DefaultSqlSessionFactory.openSessionFromDataSource(DefaultSqlSessionFactory.java:96)
             at org.apache.ibatis.session.defaults.DefaultSqlSessionFactory.openSession(DefaultSqlSessionFactory.java:57)
             at org.mybatis.spring.SqlSessionUtils.getSqlSession(SqlSessionUtils.java:98) // 获取SqlSession时
             at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:428)
             at com.sun.proxy.$Proxy81.selectList(Unknown Source:-1)
             at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:230)
             at org.apache.ibatis.binding.MapperMethod.executeForMany(MapperMethod.java:147)
             at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:80)
             at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:58) // 代理对象调用方法
             at com.sun.proxy.$Proxy86.selectAll_xml(Unknown Source:-1)  // 代理对象调用方法
             at com.aspire.ssm.SsmApplicationTests.testOne(SsmApplicationTests.java:24) // 触发查询
    • 3.3.2.2、 插件应用给ParameterHandler、ResultSetHandler、StatementHandler的时机:org.mybatis.spring.SqlSessionTemplate.SqlSessionInterceptor#invoke中,getSqlSession后,查询动作完成之前。更准确的说,【时机分别是】Configuration#newParameterHandler时、Configuration#newParameterHandler时、Configuration#newParameterHandler时。可以看一下相关方法调用栈细节:

      "main@1" prio=5 tid=0x1 nid=NA runnable
        java.lang.Thread.State: RUNNABLE
          at org.apache.ibatis.plugin.InterceptorChain.pluginAll(InterceptorChain.java:30) // 应用插件(将插件与StatementHandler关联起来)
          /*
           * ************************************************************
           * 在这之间会(按顺序)完成【应用插件(将插件与ResultSetHandler关联起来)】、【应用插件(将插件与ParameterHandler关联起来)
           * 注: 可详见源码org.apache.ibatis.session.Configuration.newStatementHandler
           */
          at org.apache.ibatis.plugin.InterceptorChain.pluginAll(InterceptorChain.java:30)
          at org.apache.ibatis.session.Configuration.newResultSetHandler(Configuration.java:571) // 应用插件(将插件与ResultSetHandler关联起来)
          at org.apache.ibatis.executor.statement.BaseStatementHandler.<init>(BaseStatementHandler.java:70)
          at org.apache.ibatis.session.Configuration.newParameterHandler(Configuration.java:564) // 应用插件(将插件与ParameterHandler关联起来)
          at org.apache.ibatis.executor.statement.BaseStatementHandler.<init>(BaseStatementHandler.java:69)
          at org.apache.ibatis.executor.statement.PreparedStatementHandler.<init>(PreparedStatementHandler.java:41)
          at org.apache.ibatis.executor.statement.RoutingStatementHandler.<init>(RoutingStatementHandler.java:46)
          // ************************************************************
          at org.apache.ibatis.session.Configuration.newStatementHandler(Configuration.java:577)
          at org.apache.ibatis.executor.SimpleExecutor.doQuery(SimpleExecutor.java:61) // 触发查询方法
          at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:324)
          at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:156)
          at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:109)
          at com.github.pagehelper.PageInterceptor.intercept(PageInterceptor.java:108)
          at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:61)
          at com.sun.proxy.$Proxy99.query(Unknown Source:-1)
          at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:147)
          at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:140)
          at sun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
          at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
          at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
          at java.lang.reflect.Method.invoke(Method.java:498) // 获取SqlSession后, 触发查询方法
          at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:433)
          at com.sun.proxy.$Proxy81.selectList(Unknown Source:-1)
          at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:230)
          at org.apache.ibatis.binding.MapperMethod.executeForMany(MapperMethod.java:147)
          at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:80)
          at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:58) // 代理对象调用方法
          at com.sun.proxy.$Proxy86.selectAll_xml(Unknown Source:-1) // 代理对象调用方法
          at com.aspire.ssm.SsmApplicationTests.testOne(SsmApplicationTests.java:24) // 触发查询
    • 3.3.2.3、 将上面两个分支中涉及到的org.apache.ibatis.plugin.InterceptorChain#pluginAll(Object target)单独拿出来,继续钻细节:

      在这里插入图片描述

      InterceptorChain#pluginAll的关键调用栈:

      "main@1" prio=5 tid=0x1 nid=NA runnable
       java.lang.Thread.State: RUNNABLE
        at org.apache.ibatis.plugin.Plugin.wrap(Plugin.java:46)
      at com.aspire.ssm.plugins.MyExecutorPlugin.plugin(MyExecutorPlugin.java:46)
      at org.apache.ibatis.plugin.InterceptorChain.pluginAll(InterceptorChain.java:31)
      • 3.3.2.3.1、 target = interceptor.plugin(target)方法,即:我们自定义的插件里面的plugin方法:

      在这里插入图片描述

      • 3.3.2.3.2、 target = interceptor.plugin(target)方法,即:我们自定义的插件里面的plugin方法:

        在这里插入图片描述

      • 3.3.2.3.2.1、 A处的作用是:获取到当前插件对象上@Intercepts注解里,@Signature的信息,即:获取到下图自定义插件中这个位置里面的信息:

        在这里插入图片描述 注:源码可详见org.apache.ibatis.plugin.Plugin#getSignatureMap。

      • 3.3.2.3.2.2、 A返回的signatureMap里,数据大致长这样:

        在这里插入图片描述

      • 3.3.2.3.2.3、 B处的作用是:决定是否把当前interceptor插件,绑定到当前target对象上。若返回的interfaces长度大于0,则需要绑定,否者不需要绑定。 getAllInterfaces方法返回interfaces的逻辑是这样的:

        在这里插入图片描述

        逻辑一: 获取target对象的所有接口,使知道target代表了四大对象中的谁(可以只代表一个、也可以同时代表多个)。 问:为什么是获取target的所有接口? 答:我们知道,target实际上是四大对象的子类实现。四大对象分别是Executor、ParameterHandler、ResultSetHandler、StatementHandler,他们都是接口。所以,这里获取到target实现的所有接口后,就知道target是属于四大对象中的哪个(或哪些)对象了。简单的讲,就是让程序知道target代表了四大对象中的谁(可以只代表一个、也可以同时代表多个)。

        逻辑二:通过A处得到的signatureMap,进一步请Class交集,若存在,则添加至集合interfaces中,并返回。 问:signatureMap在getAllInterfaces方法中发挥的作用是什么?   答: 首先,signatureMap是我们在前面的A中获得的,这里面的信息代表了:当前Interceptor的绑定方向(或者说处理能力)。即:当前Interceptor实例只能用于,在这个signatureMap的keys中存在的类。因为我们在自定义插件时,@Signature指定的type为四大对象,所以这里signatureMap中的keys也只可能是四大对象。 在这里插入图片描述   这样一来,对这两者求交集,就能知道:是否应该把当前Interceptor插件绑定到当前target实例上了

      • 3.3.2.3.2.4、 如果需要绑定的话,就将当前interceptor插件,绑定到当前target对象上;并返回,作为新的target,继续遍历下一个插件。 绑定当前插件至此target,同时生成新的target并返回:

        在这里插入图片描述

        for循环下一个插件:

        在这里插入图片描述

        注:这里可以看到【装饰者模式】,插件对原target进行层层装饰,返回装饰后的新的target。

3.4、第四问:是在什么时候走插件中的逻辑的?

  在target对象(即:四大对象)执行@Signature指定的方法时

  • 3.4.1、 分析:

    • 3.4.1.1、 假设插件的@Signature是这样的: 在这里插入图片描述

    • 3.4.1.2、 那么,当target对象属于四大对象中的Executor,且当其执行Executor#query(MappedStatement, Object, RowBounds, ResultHandler)或Executor#query(MappedStatement, Object, RowBounds, ResultHandler, CacheKey, BoundSql)方法时: 在这里插入图片描述   根据当前对象执行的方法,判断是否触发当前插件的逻辑。可详见源码org.apache.ibatis.plugin.Plugin#invoke: 在这里插入图片描述

      注:在【第三问】中,我们知道了插件是何时绑定到对象上的。但是,程序运行毕竟是方法上的,光知道插件与对象的对应关系,还不行;还得知道插件和该对象的方法的对应关系。此处就是知道插件与对象的方法的对应关系的,如果对应,则执行插件的逻辑;否者不执行。

    • 3.4.1.3、 会触发该插件的逻辑,进入插件中的方法:

      在这里插入图片描述

      • 3.4.1.3.1、 参数Invocation说明: 方法Interceptor#intercept的参数Invocation,是下图这里传的:在这里插入图片描述

        可以看到,一个Invocation对象里,包含了: ​ target:真正要调用的对象。 ​ method:要调用的方法。 ​ args:方法的参数

      • 3.4.1.3.2、 A处是此插件的前处理逻辑。

      • 3.4.1.3.3、 B处invocation.proceed,表示:让程序继续往下执行: 提示一:这一步背后的逻辑,用到了【责任链模式】,也可以理解为逆向打开【装饰者模式】。 提示二:可以参考Filter的机制进行理解。 举例说明,这一步背后的逻辑假设:同时有3个插件针对当前对象(注:当前对象属于四大对象)的当前方法生效。那么,原target对象被这三个插件装饰(包裹)后的新的target是这样的:

      在这里插入图片描述

      此处背后的逻辑(方法栈)为: 在这里插入图片描述

      • 3.4.1.3.4、 C处是此插件的后处理逻辑。
  • 3.4.2、 结论:   当执行方法的对象是插件的@Signature注解里指定的对象,且执行的方法是@Signature注解里指定的对象的指定方法时;会先执行插件的前处理逻辑,然后再继续执行目标方法,然后再执行插件的后处理逻辑。当该对象的该方法同时被多个插件拦截时,会像链条一样(责任链),先按顺序执行所有插件的前处理逻辑,然后再继续执行目标方法,然后再按顺序执行所有插件的后处理逻辑。 注:前处理逻辑、后处理逻辑的位置如图所示:

    在这里插入图片描述

    多个插件时,执行顺序如图所示:

    在这里插入图片描述

    提示:哪个插件在外层,哪个插件在里层,可详见【第五问】。

3.5、第五问:当多个插件,同时绑定到了同一个对象上时,这些插件的执行先后顺序是什么?

  • 3.5.1、 分析:   在【插件绑定到四大对象】时,会调用org.apache.ibatis.plugin.InterceptorChain#pluginAll给target(注:当前target对象,为四大对象之一)应用上插件。 在这里插入图片描述   因为是通过foreach循环的有序列表ArrayList中的所有的插件。那么,ArrayList中,index越小的插件,就越先应用给target对象。

  • 3.5.2、 结论:

    • 3.5.2.1、 插件的Order值越小,就会越早注册进Spring容器,就会越早add进ArrayList,就会越早应用给target对象,插件就会越"贴近"target对象。
    • 3.5.2.2、 越先应用给target对象的插件,就越在里面;越在里面的插件,当【the way in】时,其的前处理逻辑,就越晚执行,但当【the way out】时其后处理逻辑就越早执行。 在这里插入图片描述
    • 3.5.2.3、 示例证明: 准备三个插件,并运行测试。
    1. MyExecutorPlugin:

    在这里插入图片描述

    1. MyExecutorPlugin2:

    在这里插入图片描述

    1. MyExecutorPlugin3:

    在这里插入图片描述

    1. 运行测试(随便执行一个SQL,观察日志输出):

      在这里插入图片描述 观察日志可知,结论正确。

3.6、第六问:以Executor为例,在很多实现(如BaseExecutor)里面存在内部调用的情况(如下面第一图),那么当插件里面如(下面第二张)图设置的时候,会不会走两遍重复的逻辑?

在这里插入图片描述

在这里插入图片描述

答:不会。因为Mybatis插件实际上是代理模式的一种实现。内部调用,是不会走代理的,所以如左图1中所示,当外部调用了BaseExecutor的4个参数的query方法时,虽然4个参数的query方法内部调用了6个参数的query方法,但是Mybatis插件(即:代理)的逻辑还是只走一遍。


Mybatis之一个SQL的运行过程,梳理完毕 !


相关资料

1
https://gitee.com/WY784755850/notebook.git
git@gitee.com:WY784755850/notebook.git
WY784755850
notebook
notebook
master

搜索帮助