SQL解析
Mybatis在初始化的時候,會讀取xml中的SQL,解析后會生成SqlSource對象,SqlSource對象分為兩種。
-
DynamicSqlSource
,動態SQL,獲取SQL(getBoundSQL
方法中)的時候生成參數化SQL。 -
RawSqlSource
,原始SQL,創建對象時直接生成參數化SQL。
因為RawSqlSource
不會重復去生成參數化SQL,調用的時候直接傳入參數并執行,而DynamicSqlSource
則是每次執行的時候參數化SQL,所以RawSqlSource
是DynamicSqlSource
的性能要好的。
解析的時候會先解析include
標簽和selectkey
標簽,然后判斷是否是動態SQL,判斷取決于以下兩個條件:
-
SQL中有動態拼接字符串,簡單來說就是是否使用了
${}
表達式。注意這種方式存在SQL注入,謹慎使用。 -
SQL中有
trim
、where
、set
、foreach
、if
、choose
、when
、otherwise
、bind
標簽
相關代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
protected MixedSqlNode parseDynamicTags(XNode node) { // 創建 SqlNode 數組 List<SqlNode> contents = new ArrayList<>(); // 遍歷 SQL 節點的所有子節點 NodeList children = node.getNode().getChildNodes(); for ( int i = 0 ; i < children.getLength(); i++) { // 當前子節點 XNode child = node.newXNode(children.item(i)); // 如果類型是 Node.CDATA_SECTION_NODE 或者 Node.TEXT_NODE 時 if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) { // 獲得內容 String data = child.getStringBody( "" ); // 創建 TextSqlNode 對象 TextSqlNode textSqlNode = new TextSqlNode(data); // 如果是動態的 TextSqlNode 對象(是否使用了${}表達式) if (textSqlNode.isDynamic()) { // 添加到 contents 中 contents.add(textSqlNode); // 標記為動態 SQL isDynamic = true ; // 如果是非動態的 TextSqlNode 對象 } else { // 創建 StaticTextSqlNode 添加到 contents 中 contents.add( new StaticTextSqlNode(data)); } // 如果類型是 Node.ELEMENT_NODE,其實就是XMl中<where>等那些動態標簽 } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628 // 根據子節點的標簽,獲得對應的 NodeHandler 對象 String nodeName = child.getNode().getNodeName(); NodeHandler handler = nodeHandlerMap.get(nodeName); if (handler == null ) { // 獲得不到,說明是未知的標簽,拋出 BuilderException 異常 throw new BuilderException( "Unknown element <" + nodeName + "> in SQL statement." ); } // 執行 NodeHandler 處理 handler.handleNode(child, contents); // 標記為動態 SQL isDynamic = true ; } } // 創建 MixedSqlNode 對象 return new MixedSqlNode(contents); } |
參數解析
Mybais中用于解析Mapper方法的參數的類是ParamNameResolver
,它主要做了這些事情:
-
每個Mapper方法第一次運行時會去創建
ParamNameResolver
,之后會緩存 - 創建時會根據方法簽名,解析出參數名,解析的規則順序是
如果參數類型是RowBounds
或者ResultHandler
類型或者他們的子類,則不處理。
如果參數中有Param
注解,則使用Param
中的值作為參數名
如果配置項useActualParamName
=true,argn
(n>=0)標作為參數名,如果你是Java8以上并且開啟了
-parameters`,則是實際的參數名
如果配置項useActualParamName
=false,則使用n
(n>=0)作為參數名
相關源代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
public ParamNameResolver(Configuration config, Method method) { final Class<?>[] paramTypes = method.getParameterTypes(); final Annotation[][] paramAnnotations = method.getParameterAnnotations(); final SortedMap<Integer, String> map = new TreeMap<Integer, String>(); int paramCount = paramAnnotations.length; // 獲取方法中每個參數在SQL中的參數名 for ( int paramIndex = 0 ; paramIndex < paramCount; paramIndex++) { // 跳過RowBounds、ResultHandler類型 if (isSpecialParameter(paramTypes[paramIndex])) { continue ; } String name = null ; // 遍歷參數上面的所有注解,如果有Param注解,使用它的值作為參數名 for (Annotation annotation : paramAnnotations[paramIndex]) { if (annotation instanceof Param) { hasParamAnnotation = true ; name = ((Param) annotation).value(); break ; } } // 如果沒有指定注解 if (name == null ) { // 如果開啟了useActualParamName配置,則參數名為argn(n>=0),如果是Java8以上并且開啟-parameters,則為實際的參數名 if (config.isUseActualParamName()) { name = getActualParamName(method, paramIndex); } // 否則為下標 if (name == null ) { name = String.valueOf(map.size()); } } map.put(paramIndex, name); } names = Collections.unmodifiableSortedMap(map); } |
而在使用這個names
構建xml中參數對象和值的映射時,還進行了進一步的處理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
public Object getNamedParams(Object[] args) { final int paramCount = names.size(); // 無參數,直接返回null if (args == null || paramCount == 0 ) { return null ; } else if (!hasParamAnnotation && paramCount == 1 ) { // 一個參數,并且沒有注解,直接返回這個對象 return args[names.firstKey()]; } else { // 其他情況則返回一個Map對象 final Map<String, Object> param = new ParamMap<Object>(); int i = 0 ; for (Map.Entry<Integer, String> entry : names.entrySet()) { // 先直接放入name的鍵和對應位置的參數值,其實就是構造函數中存入的值 param.put(entry.getValue(), args[entry.getKey()]); // add generic param names (param1, param2, ...) final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1 ); // 防止覆蓋 @Param 的參數值 if (!names.containsValue(genericParamName)) { // 然后放入GENERIC_NAME_PREFIX + index + 1,其實就是param1,params2,paramn param.put(genericParamName, args[entry.getKey()]); } i++; } return param; } } |
另外值得一提的是,對于集合類型,最后還有一個特殊處理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
private Object wrapCollection( final Object object) { // 如果對象是集合屬性 if (object instanceof Collection) { StrictMap<Object> map = new StrictMap<Object>(); // 加入一個collection參數 map.put( "collection" , object); // 如果是一個List集合 if (object instanceof List) { // 額外加入一個list屬性使用 map.put( "list" , object); } return map; } else if (object != null && object.getClass().isArray()) { // 數組使用array StrictMap<Object> map = new StrictMap<Object>(); map.put( "array" , object); return map; } return object; } |
由此我們可以得出使用參數的結論:
-
如果參數加了
@Param
注解,則使用注解的值作為參數 -
如果只有一個參數,并且不是集合類型和數組,且沒有加注解,則使用對象的屬性名作為參數如果只有一個參數,并且是集合類型,則使用
collection
參數,如果是List
對象,可以額外使用list
參數。 -
如果只有一個參數,并且是數組,則可以使用
array
參數如果有多個參數,沒有加@Param
注解的可以使用argn
或者n
(n>=0,取決于useActualParamName
配置項)作為參數,加了注解的使用注解的值。 -
如果有多個參數,任意參數只要不是和
@Param
中的值覆蓋,都可以使用paramn
(n>=1)
延遲加載
Mybatis是支持延遲加載的,具體的實現方式根據resultMap
創建返回對象時,發現fetchType=“lazy”,則使用代理對象,默認使用Javassist
(MyBatis 3.3 以上,可以修改為使用CgLib
)。代碼處理邏輯在處理返回結果集時,具體代碼調用關系如下:
PreparedStatementHandler.query
=> handleResultSets
=>handleResultSet
=>handleRowValues
=>handleRowValuesForNestedResultMap
=>getRowValue
在getRowValue
中,有一個方法createResultObject
創建返回對象,其中的關鍵代碼創建了代理對象:
1
2
3
|
if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) { resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs); } |
另一方面,getRowValue
會調用applyPropertyMappings
方法,其內部會調用getPropertyMappingValue
,繼續追蹤到getNestedQueryMappingValue
方法,在這里,有幾行關鍵代碼:
1
2
3
4
5
6
7
8
9
10
|
// 如果要求延遲加載,則延遲加載 if (propertyMapping.isLazy()) { // 如果該屬性配置了延遲加載,則將其添加到 `ResultLoader.loaderMap` 中,等待真正使用時再執行嵌套查詢并得到結果對象。 lazyLoader.addLoader(property, metaResultObject, resultLoader); // 返回已定義 value = DEFERED; // 如果不要求延遲加載,則直接執行加載對應的值 } else { value = resultLoader.loadResult(); } |
這幾行的目的是跳過屬性值的加載,等真正需要值的時候,再獲取值。
Executor
Executor是一個接口,其直接實現的類是BaseExecutor
和CachingExecutor
,BaseExecutor
又派生了BatchExecutor
、ReuseExecutor
、SimpleExecutor
、ClosedExecutor
。其繼承結構如圖:
其中ClosedExecutor
是一個私有類,用戶不直接使用它。
-
BaseExecutor
:模板類,里面有各個Executor的公用的方法。 -
SimpleExecutor
:最常用的Executor
,默認是使用它去連接數據庫,執行SQL語句,沒有特殊行為。ReuseExecutor
:SQL語句執行后會進行緩存,不會關閉Statement
,下次執行時會復用,緩存的key
值是BoundSql
解析后SQL,清空緩存使用doFlushStatements
。其他與SimpleExecutor
相同。 -
BatchExecutor
:當有連續的Insert
、Update
、Delete
的操作語句,并且語句的BoundSql
相同,則這些語句會批量執行。使用doFlushStatements
方法獲取批量操作的返回值。 -
CachingExecutor
:當你開啟二級緩存的時候,會使用CachingExecutor
裝飾SimpleExecutor
、ReuseExecutor
和BatchExecutor
,Mybatis通過CachingExecutor
來實現二級緩存。
緩存
一級緩存
Mybatis一級緩存的實現主要是在BaseExecutor
中,在它的查詢方法里,會優先查詢緩存中的值,如果不存在,再查詢數據庫,查詢部分的代碼如下,關鍵代碼在17-24行:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
@Override public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { ErrorContext.instance().resource(ms.getResource()).activity( "executing a query" ).object(ms.getId()); // 已經關閉,則拋出 ExecutorException 異常 if (closed) { throw new ExecutorException( "Executor was closed." ); } // 清空本地緩存,如果 queryStack 為零,并且要求清空本地緩存。 if (queryStack == 0 && ms.isFlushCacheRequired()) { clearLocalCache(); } List<E> list; try { // queryStack + 1 queryStack++; // 從一級緩存中,獲取查詢結果 list = resultHandler == null ? (List<E>) localCache.getObject(key) : null ; // 獲取到,則進行處理 if (list != null ) { handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); // 獲得不到,則從數據庫中查詢 } else { list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); } } finally { // queryStack - 1 queryStack--; } if (queryStack == 0 ) { // 執行延遲加載 for (DeferredLoad deferredLoad : deferredLoads) { deferredLoad.load(); } // issue #601 // 清空 deferredLoads deferredLoads.clear(); // 如果緩存級別是 LocalCacheScope.STATEMENT ,則進行清理 if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { // issue #482 clearLocalCache(); } } return list; } |
而在queryFromDatabase
中,則會將查詢出來的結果放到緩存中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// 從數據庫中讀取操作 private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { List<E> list; // 在緩存中,添加占位對象。此處的占位符,和延遲加載有關,可見 `DeferredLoad#canLoad()` 方法 localCache.putObject(key, EXECUTION_PLACEHOLDER); try { // 執行讀操作 list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); } finally { // 從緩存中,移除占位對象 localCache.removeObject(key); } // 添加到緩存中 localCache.putObject(key, list); // 暫時忽略,存儲過程相關 if (ms.getStatementType() == StatementType.CALLABLE) { localOutputParameterCache.putObject(key, parameter); } return list; } |
而一級緩存的Key,從方法的參數可以看出,與調用方法、參數、rowBounds分頁參數、最終生成的sql有關。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
@Override public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) { if (closed) { throw new ExecutorException( "Executor was closed." ); } // 創建 CacheKey 對象 CacheKey cacheKey = new CacheKey(); // 設置 id、offset、limit、sql 到 CacheKey 對象中 cacheKey.update(ms.getId()); cacheKey.update(rowBounds.getOffset()); cacheKey.update(rowBounds.getLimit()); cacheKey.update(boundSql.getSql()); // 設置 ParameterMapping 數組的元素對應的每個 value 到 CacheKey 對象中 List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry(); // mimic DefaultParameterHandler logic 這塊邏輯,和 DefaultParameterHandler 獲取 value 是一致的。 for (ParameterMapping parameterMapping : parameterMappings) { if (parameterMapping.getMode() != ParameterMode.OUT) { Object value; String propertyName = parameterMapping.getProperty(); if (boundSql.hasAdditionalParameter(propertyName)) { value = boundSql.getAdditionalParameter(propertyName); } else if (parameterObject == null ) { value = null ; } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { value = parameterObject; } else { MetaObject metaObject = configuration.newMetaObject(parameterObject); value = metaObject.getValue(propertyName); } cacheKey.update(value); } } // 設置 Environment.id 到 CacheKey 對象中 if (configuration.getEnvironment() != null ) { // issue #176 cacheKey.update(configuration.getEnvironment().getId()); } return cacheKey; } |
通過查看一級緩存類的實現,可以看出一級緩存是通過HashMap結構存儲的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
/** * 一級緩存的實現類,部分源代碼 */ public class PerpetualCache implements Cache { /** * 緩存容器 */ private Map<Object, Object> cache = new HashMap<>(); @Override public void putObject(Object key, Object value) { cache.put(key, value); } @Override public Object getObject(Object key) { return cache.get(key); } @Override public Object removeObject(Object key) { return cache.remove(key); } } |
通過配置項,我們可以控制一級緩存的使用范圍,默認是Session級別的,也就是SqlSession的范圍內有效。也可以配制成Statement級別,當本次查詢結束后立即清除緩存。
當進行插入、更新、刪除操作時,也會在執行SQL之前清空以及緩存。
二級緩存
Mybatis二級緩存的實現是依靠CachingExecutor
裝飾其他的Executor
實現。原理是在查詢的時候先根據CacheKey查詢緩存中是否存在值,如果存在則返回緩存的值,沒有則查詢數據庫。
在CachingExecutor
中query
方法中,就有緩存的使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { Cache cache = ms.getCache(); if (cache != null ) { // 如果需要清空緩存,則進行清空 flushCacheIfRequired(ms); if (ms.isUseCache() && resultHandler == null ) { // 暫時忽略,存儲過程相關 ensureNoOutParams(ms, boundSql); @SuppressWarnings ( "unchecked" ) // 從二級緩存中,獲取結果 List<E> list = (List<E>) tcm.getObject(cache, key); if (list == null ) { // 如果不存在,則從數據庫中查詢 list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); // 緩存結果到二級緩存中 tcm.putObject(cache, key, list); // issue #578 and #116 } // 如果存在,則直接返回結果 return list; } } // 不使用緩存,則從數據庫中查詢 return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); } |
那么這個Cache
是在哪里創建的呢?通過調用的追溯,可以找到它的創建:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public Cache useNewCache(Class<? extends Cache> typeClass, Class<? extends Cache> evictionClass, Long flushInterval, Integer size, boolean readWrite, boolean blocking, Properties props) { // 創建 Cache 對象 Cache cache = new CacheBuilder(currentNamespace) .implementation(valueOrDefault(typeClass, PerpetualCache. class )) .addDecorator(valueOrDefault(evictionClass, LruCache. class )) .clearInterval(flushInterval) .size(size) .readWrite(readWrite) .blocking(blocking) .properties(props) .build(); // 添加到 configuration 的 caches 中 configuration.addCache(cache); // 賦值給 currentCache currentCache = cache; return cache; } |
從方法的第一行可以看出,Cache對象的范圍是namespace,同一個namespace下的所有mapper方法共享Cache對象,也就是說,共享這個緩存。
另一個創建方法是通過CacheRef里面的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public Cache useCacheRef(String namespace) { if (namespace == null ) { throw new BuilderException( "cache-ref element requires a namespace attribute." ); } try { unresolvedCacheRef = true ; // 標記未解決 // 獲得 Cache 對象 Cache cache = configuration.getCache(namespace); // 獲得不到,拋出 IncompleteElementException 異常 if (cache == null ) { throw new IncompleteElementException( "No cache for namespace '" + namespace + "' could be found." ); } // 記錄當前 Cache 對象 currentCache = cache; unresolvedCacheRef = false ; // 標記已解決 return cache; } catch (IllegalArgumentException e) { throw new IncompleteElementException( "No cache for namespace '" + namespace + "' could be found." , e); } } |
這里的話會通過CacheRef
中的參數namespace
,找到那個Cache
對象,且這里使用了unresolvedCacheRef
,因為Mapper文件的加載是有順序的,可能當前加載時引用的那個namespace
的Mapper文件還沒有加載,所以用這個標記一下,延后加載。
二級緩存通過TransactionalCache
來管理,內部使用的是一個HashMap。Key是Cache對象,默認的實現是PerpetualCache
,一個namespace下共享這個對象。Value是另一個Cache的對象,默認實現是TransactionalCache
,是前面那個Key值的裝飾器,擴展了事務方面的功能。
通過查看TransactionalCache
的源碼我們可以知道,默認查詢后添加的緩存保存在待提交對象里。
1
2
3
4
|
public void putObject(Object key, Object object) { // 暫存 KV 到 entriesToAddOnCommit 中 entriesToAddOnCommit.put(key, object); } |
只有等到commit
的時候才會去刷入緩存。
1
2
3
4
5
6
7
8
9
10
|
public void commit() { // 如果 clearOnCommit 為 true ,則清空 delegate 緩存 if (clearOnCommit) { delegate.clear(); } // 將 entriesToAddOnCommit、entriesMissedInCache 刷入 delegate 中 flushPendingEntries(); // 重置 reset(); } |
查看clear
代碼,只是做了標記,并沒有真正釋放對象。在查詢時根據標記直接返回空,在commit
才真正釋放對象:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
public void clear() { // 標記 clearOnCommit 為 true clearOnCommit = true ; // 清空 entriesToAddOnCommit entriesToAddOnCommit.clear(); } public Object getObject(Object key) { // issue #116 // 從 delegate 中獲取 key 對應的 value Object object = delegate.getObject(key); // 如果不存在,則添加到 entriesMissedInCache 中 if (object == null ) { entriesMissedInCache.add(key); } // issue #146 // 如果 clearOnCommit 為 true ,表示處于持續清空狀態,則返回 null if (clearOnCommit) { return null ; // 返回 value } else { return object; } } |
rollback
會清空這些臨時緩存:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public void rollback() { // 從 delegate 移除出 entriesMissedInCache unlockMissedEntries(); // 重置 reset(); } private void reset() { // 重置 clearOnCommit 為 false clearOnCommit = false ; // 清空 entriesToAddOnCommit、entriesMissedInCache entriesToAddOnCommit.clear(); entriesMissedInCache.clear(); } |
根據二級緩存代碼可以看出,二級緩存是基于namespace
的,可以跨SqlSession。也正是因為基于namespace
,如果在不同的namespace
中修改了同一個表的數據,會導致臟讀的問題。
插件
Mybatis的插件是通過代理對象實現的,可以代理的對象有:
-
Executor
:執行器,執行器是執行過程中第一個代理對象,它內部調用StatementHandler
返回SQL結果。 -
StatementHandler
:語句處理器,執行SQL前調用ParameterHandler
處理參數,執行SQL后調用ResultSetHandler
處理返回結果 -
ParameterHandler
:參數處理器 -
ResultSetHandler
:返回對象處理器
這四個對象的接口的所有方法都可以用插件攔截。
插件的實現代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
// 創建 ParameterHandler 對象 public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { // 創建 ParameterHandler 對象 ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql); // 應用插件 parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler); return parameterHandler; } // 創建 ResultSetHandler 對象 public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) { // 創建 DefaultResultSetHandler 對象 ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds); // 應用插件 resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler); return resultSetHandler; } // 創建 StatementHandler 對象 public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { // 創建 RoutingStatementHandler 對象 StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql); // 應用插件 statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); return statementHandler; } /** * 創建 Executor 對象 * * @param transaction 事務對象 * @param executorType 執行器類型 * @return Executor 對象 */ public Executor newExecutor(Transaction transaction, ExecutorType executorType) { // 獲得執行器類型 executorType = executorType == null ? defaultExecutorType : executorType; // 使用默認 executorType = executorType == null ? ExecutorType.SIMPLE : executorType; // 使用 ExecutorType.SIMPLE // 創建對應實現的 Executor 對象 Executor executor; if (ExecutorType.BATCH == executorType) { executor = new BatchExecutor( this , transaction); } else if (ExecutorType.REUSE == executorType) { executor = new ReuseExecutor( this , transaction); } else { executor = new SimpleExecutor( this , transaction); } // 如果開啟緩存,創建 CachingExecutor 對象,進行包裝 if (cacheEnabled) { executor = new CachingExecutor(executor); } // 應用插件 executor = (Executor) interceptorChain.pluginAll(executor); return executor; } |
可以很明顯的看到,四個方法內都有interceptorChain.pluginAll()
方法的調用,繼續查看這個方法:
1
2
3
4
5
6
7
8
9
10
11
12
|
/** * 應用所有插件 * * @param target 目標對象 * @return 應用結果 */ public Object pluginAll(Object target) { for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; } |
這個方法比較簡單,就是遍歷interceptors
列表,然后調用器plugin
方法。interceptors
是在解析XML配置文件是通過反射創建的,而創建后會立即調用setProperties
方法
我們通常配置插件時,會在interceptor.plugin
調用Plugin.wrap
,這里面通過Java的動態代理,攔截方法的實現:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
/** * 創建目標類的代理對象 * * @param target 目標類 * @param interceptor 攔截器對象 * @return 代理對象 */ public static Object wrap(Object target, Interceptor interceptor) { // 獲得攔截的方法映射 Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); // 獲得目標類的類型 Class<?> type = target.getClass(); // 獲得目標類的接口集合 Class<?>[] interfaces = getAllInterfaces(type, signatureMap); // 若有接口,則創建目標對象的 JDK Proxy 對象 if (interfaces.length > 0 ) { return Proxy.newProxyInstance( type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); // 因為 Plugin 實現了 InvocationHandler 接口,所以可以作為 JDK 動態代理的調用處理器 } // 如果沒有,則返回原始的目標對象 return target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { // 獲得目標方法是否被攔截 Set<Method> methods = signatureMap.get(method.getDeclaringClass()); if (methods != null && methods.contains(method)) { // 如果是,則攔截處理該方法 return interceptor.intercept( new Invocation(target, method, args)); } // 如果不是,則調用原方法 return method.invoke(target, args); } catch (Exception e) { throw ExceptionUtil.unwrapThrowable(e); } } |
而攔截的參數傳了Plugin
對象,Plugin本身是實現了InvocationHandler
接口,其invoke
方法里面調用了interceptor.intercept
,這個方法就是我們實現攔截處理的地方。
注意到里面有個getSignatureMap
方法,這個方法實現的是查找我們自定義攔截器的注解,通過注解確定哪些方法需要被攔截:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) { Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts. class ); // issue #251 if (interceptsAnnotation == null ) { throw new PluginException( "No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName()); } Signature[] sigs = interceptsAnnotation.value(); Map<Class<?>, Set<Method>> signatureMap = new HashMap<>(); for (Signature sig : sigs) { Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>()); try { Method method = sig.type().getMethod(sig.method(), sig.args()); methods.add(method); } catch (NoSuchMethodException e) { throw new PluginException( "Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e); } } return signatureMap; } |
通過源代碼我們可以知道,創建一個插件需要做以下事情:
-
創建一個類,實現
Interceptor
接口。 -
這個類必須使用
@Intercepts
、@Signature
來表明要攔截哪個對象的哪些方法。 -
這個類的
plugin
方法中調用Plugin.wrap(target, this)
。 -
(可選)這個類的
setProperties
方法設置一些參數。 -
XML中
<plugins>
節點配置<plugin interceptor="你的自定義類的全名稱"></plugin>
。
可以在第三點中根據具體的業務情況不進行本次SQL操作的代理,畢竟動態代理還是有性能損耗的。
到此這篇關于通過源代碼分析Mybatis的功能的文章就介紹到這了,更多相關源代碼分析Mybatis內容請搜索服務器之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持服務器之家!
原文鏈接:https://www.cnblogs.com/Weilence/p/13416986.html