Mybatis之一个SQL的运行过程

阅前必读

  • 本文测试项目及相关总结资料,均放置在文末链接处。强烈建议去拽下来,看xmind脑图并结合源码进行理解
  • 本人是先绘制的xmind脑图,然后根据xmind脑图发的此博文,无论是可读性、还是层次感,xmind脑图都由于文字。
  • Mybatis中逻辑很多,而本文重点关注的是Mybatis中SQL相关的逻辑,其余部分会简述或直接略过。
  • 本文主要分享的内容是:
    • 启动项目时,与SQL相关的逻辑
    • 启动项目后,执行CURD方法时,与SQL相关的逻辑
    • 六问Mybatis插件
  • 文末链接指向的本人的测试项目,是长这样的:
    Mybatis之一个SQL的运行过程_第1张图片

(一)启动项目时,与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相关的信息

Mybatis之一个SQL的运行过程_第2张图片

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

Mybatis之一个SQL的运行过程_第3张图片

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

Mybatis之一个SQL的运行过程_第4张图片

  • 1.4.1 RawSqlSource:内部持有了一个SqlSource,该SqlSource是StaticSqlSource实例。因为RawSqlSource在启动时就会计算出mapping(即:sql的最终的样子),所以其性能优于DynamicSqlSource。
    Mybatis之一个SQL的运行过程_第5张图片
    普通的SQL,会被封装为RawSqlSource或者DynamicSqlSource

    • 在这里插入图片描述
    • Mybatis之一个SQL的运行过程_第6张图片
    • Mybatis之一个SQL的运行过程_第7张图片
    • Mybatis之一个SQL的运行过程_第8张图片
    • Mybatis之一个SQL的运行过程_第9张图片
    • Mybatis之一个SQL的运行过程_第10张图片
  • 1.4.2 DynamicSqlSource:动态SQL处理器,会处理${}、#{}等一系列SQL,最终处理完毕后,会以最终的SQL信息等为参数,new一个StaticSqlSource来作为最终查询时用的SqlSource。

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

    • Mybatis之一个SQL的运行过程_第11张图片
    • Mybatis之一个SQL的运行过程_第12张图片
  • 1.4.3 ProviderSqlSource:处理通过注解@InsertProvider、@DeleteProvider、@UpdateProvider、@SelectProvider写的SQL;ProviderSqlSource会转换为DynamicSqlSource或RawSqlSource,最终都会与StaticSqlSource关系起来。

    以下SQL,会被封装为ProviderSqlSource
    Mybatis之一个SQL的运行过程_第13张图片

  • 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 总结来说,即:
    Mybatis之一个SQL的运行过程_第14张图片


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

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

Mybatis之一个SQL的运行过程_第15张图片

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实例

Mybatis之一个SQL的运行过程_第16张图片

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

  首先,org.apache.ibatis.mapping.MappedStatement#getBoundSql:
Mybatis之一个SQL的运行过程_第17张图片
注: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:
    Mybatis之一个SQL的运行过程_第18张图片
    RawSqlSource#getBoundSql的调用方法栈为:
    Mybatis之一个SQL的运行过程_第19张图片

  • 2.6.2 DynamicSqlSource#getBoundSql:
    Mybatis之一个SQL的运行过程_第20张图片

    • 2.6.2.1 其中,org.apache.ibatis.scripting.xmltags.SqlNode#apply实现了动态SQL:
      在这里插入图片描述
      SqlNode接口有很多实现:
      Mybatis之一个SQL的运行过程_第21张图片
        从这些类的名字就可看出,这些类的功能了。他们实现了对xml中if标签、choose标签、foreach等标签的动态判断处理。

    • 2.6.2.2 以IfSqlNode为例,讲解是如何实现动态SQL的:
      Mybatis之一个SQL的运行过程_第22张图片

    • 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:
    Mybatis之一个SQL的运行过程_第23张图片
    Mybatis之一个SQL的运行过程_第24张图片
    其中invokeProviderMethod方法长这样:
    Mybatis之一个SQL的运行过程_第25张图片
      所以,不论是RawSqlSource#getBoundSql还是DynamicSqlSource#getBoundSql还是ProviderSqlSource#getBoundSql,最终获得的都是StaticSqlSource#getBoundSql的结果。

2.7、第六步:执行查询。

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


(三)六问Mybatis插件

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

Mybatis之一个SQL的运行过程_第26张图片
注:本文的重点不是介绍如何自定义插件的,所以这里就简单介绍了。

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

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

    • 3.2.1.1 首先,自定义插件并将其注册进容器:
      Mybatis之一个SQL的运行过程_第27张图片

    • 3.2.1.2 然后,MybatisAutoConfiguration的构造器会自动感知到容器中的插件:
      Mybatis之一个SQL的运行过程_第28张图片
      提示: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添加自定义的拦截器。
    Mybatis之一个SQL的运行过程_第29张图片

  • 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中。
    Mybatis之一个SQL的运行过程_第30张图片
    Mybatis之一个SQL的运行过程_第31张图片

  • 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)单独拿出来,继续钻细节:
      Mybatis之一个SQL的运行过程_第32张图片
      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方法:
        Mybatis之一个SQL的运行过程_第33张图片
      • 3.3.2.3.2 target = interceptor.plugin(target)方法,即:我们自定义的插件里面的plugin方法:
        Mybatis之一个SQL的运行过程_第34张图片
        • 3.3.2.3.2.1 A处的作用是:获取到当前插件对象上@Intercepts注解里,@Signature的信息,即:获取到下图自定义插件中这个位置里面的信息:
          Mybatis之一个SQL的运行过程_第35张图片
          注:源码可详见org.apache.ibatis.plugin.Plugin#getSignatureMap。

        • 3.3.2.3.2.2 A返回的signatureMap里,数据大致长这样:
          Mybatis之一个SQL的运行过程_第36张图片

        • 3.3.2.3.2.3 B处的作用是:决定是否把当前interceptor插件,绑定到当前target对象上。若返回的interfaces长度大于0,则需要绑定,否者不需要绑定。
          getAllInterfaces方法返回interfaces的逻辑是这样的:
          Mybatis之一个SQL的运行过程_第37张图片
          逻辑一:获取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也只可能是四大对象。
          Mybatis之一个SQL的运行过程_第38张图片
            这样一来,对这两者求交集,就能知道:是否应该把当前Interceptor插件绑定到当前target实例上了

        • 3.3.2.3.2.4 如果需要绑定的话,就将当前interceptor插件,绑定到当前target对象上;并返回,作为新的target,继续遍历下一个插件。
          绑定当前插件至此target,同时生成新的target并返回:
          Mybatis之一个SQL的运行过程_第39张图片
          for循环下一个插件:
          Mybatis之一个SQL的运行过程_第40张图片
          注:这里可以看到【装饰者模式】,插件对原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)方法时:
      Mybatis之一个SQL的运行过程_第41张图片
        根据当前对象执行的方法,判断是否触发当前插件的逻辑。可详见源码org.apache.ibatis.plugin.Plugin#invoke:
      Mybatis之一个SQL的运行过程_第42张图片
      注:在【第三问】中,我们知道了插件是何时绑定到对象上的。但是,程序运行毕竟是方法上的,光知道插件与对象的对应关系,还不行;还得知道插件和该对象的方法的对应关系。此处就是知道插件与对象的方法的对应关系的,如果对应,则执行插件的逻辑;否者不执行。

    • 3.4.1.3 会触发该插件的逻辑,进入插件中的方法:
      Mybatis之一个SQL的运行过程_第43张图片

      • 3.4.1.3.1 参数Invocation说明:
        方法Interceptor#intercept的参数Invocation,是下图这里传的:
        Mybatis之一个SQL的运行过程_第44张图片
        可以看到,一个Invocation对象里,包含了:
        target:真正要调用的对象
        method:要调用的方法
        args:方法的参数

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

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

      • 3.4.1.3.4 C处是此插件的后处理逻辑。

  • 3.4.2 结论:
      当执行方法的对象是插件的@Signature注解里指定的对象,且执行的方法是@Signature注解里指定的对象的指定方法时;会先执行插件的前处理逻辑,然后再继续执行目标方法,然后再执行插件的后处理逻辑。当该对象的该方法同时被多个插件拦截时,会像链条一样(责任链),先按顺序执行所有插件的前处理逻辑,然后再继续执行目标方法,然后再按顺序执行所有插件的后处理逻辑。
    注:前处理逻辑、后处理逻辑的位置如图所示:
    Mybatis之一个SQL的运行过程_第47张图片
    多个插件时,执行顺序如图所示:
    Mybatis之一个SQL的运行过程_第48张图片
    提示:哪个插件在外层,哪个插件在里层,可详见【第五问】。

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

  • 3.5.1 分析:
      在【插件绑定到四大对象】时,会调用org.apache.ibatis.plugin.InterceptorChain#pluginAll给target(注:当前target对象,为四大对象之一)应用上插件。
    Mybatis之一个SQL的运行过程_第49张图片
      因为是通过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】时其后处理逻辑就越早执行。
      Mybatis之一个SQL的运行过程_第50张图片
    • 3.5.2.3 示例证明:
      准备三个插件,并运行测试。
    1. MyExecutorPlugin:
      Mybatis之一个SQL的运行过程_第51张图片
    2. MyExecutorPlugin2:
      Mybatis之一个SQL的运行过程_第52张图片
    3. MyExecutorPlugin3:
      Mybatis之一个SQL的运行过程_第53张图片
    4. 运行测试(随便执行一个SQL,观察日志输出):
      Mybatis之一个SQL的运行过程_第54张图片
      观察日志可知,结论正确。

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

Mybatis之一个SQL的运行过程_第55张图片
Mybatis之一个SQL的运行过程_第56张图片

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


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


^_^ 如有不当之处,欢迎指正

^_^ 参考资料
        《Mybatis源码》

^_^ 测试代码托管链接
         https://github.com/JustryDeng…Mybatis…

^_^ 本文已经被收录进《程序员成长笔记(七)》,笔者JustryDeng

你可能感兴趣的:(Java知识大杂烩)