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:
      在這裏插入圖片描述
    2. MyExecutorPlugin2:
      在這裏插入圖片描述
    3. MyExecutorPlugin3:
      在這裏插入圖片描述
    4. 運行測試(隨便執行一個SQL,觀察日誌輸出):
      在這裏插入圖片描述
      觀察日誌可知,結論正確。

3.6、第六問:以Executor爲例,在很多實現(如BaseExecutor)裏面存在內部調用的情況(如下面第一圖),那麼當插件裏面如(下面第二張)圖設置的時候,會不會走兩遍重複的邏輯?

在這裏插入圖片描述
在這裏插入圖片描述

答:不會。因爲Mybatis插件實際上是代理模式的一種實現。內部調用,是不會走代理的,所以如左圖1中所示,當外部調用了BaseExecutor的4個參數的query方法時,雖然4個參數的query方法內部調用了6個參數的query方法,但是Mybatis插件(即:代理)的邏輯還是隻走一遍。


Mybatis之一個SQL的運行過程,梳理完畢 !


^_^ 如有不當之處,歡迎指正

^_^ 參考資料
        《Mybatis源碼》

^_^ 測試代碼託管鏈接
         https://github.com/JustryDeng…Mybatis…

^_^ 本文已經被收錄進《程序員成長筆記(七)》,筆者JustryDeng

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章