閱前必讀:
- 本文測試項目及相關總結資料,均放置在文末鏈接處。強烈建議去拽下來,看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.3.2.3.1、 target = interceptor.plugin(target)方法,即:我們自定義的插件裏面的plugin方法:
-
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、 示例證明:
準備三個插件,並運行測試。
- MyExecutorPlugin:
- MyExecutorPlugin2:
- MyExecutorPlugin3:
- 運行測試(隨便執行一個SQL,觀察日誌輸出):
觀察日誌可知,結論正確。
3.6、第六問:以Executor爲例,在很多實現(如BaseExecutor)裏面存在內部調用的情況(如下面第一圖),那麼當插件裏面如(下面第二張)圖設置的時候,會不會走兩遍重複的邏輯?
答:不會。因爲Mybatis插件實際上是代理模式的一種實現。內部調用,是不會走代理的,所以如左圖1中所示,當外部調用了BaseExecutor的4個參數的query方法時,雖然4個參數的query方法內部調用了6個參數的query方法,但是Mybatis插件(即:代理)的邏輯還是隻走一遍。
Mybatis之一個SQL的運行過程,梳理完畢 !
^_^ 如有不當之處,歡迎指正
^_^ 參考資料
《Mybatis源碼》
^_^ 測試代碼託管鏈接
https://github.com/JustryDeng…Mybatis…
^_^ 本文已經被收錄進《程序員成長筆記(七)》,筆者JustryDeng