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
標籤
相關程式碼如下:
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)作為引數名
-
相關原始碼:
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中引數物件和值的對映時,還進行了進一步的處理。
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;
}
}
另外值得一提的是,對於集合型別,最後還有一個特殊處理
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
建立返回物件,其中的關鍵程式碼建立了代理物件:
if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
}
另一方面,getRowValue
會呼叫applyPropertyMappings
方法,其內部會呼叫getPropertyMappingValue
,繼續追蹤到getNestedQueryMappingValue
方法,在這裡,有幾行關鍵程式碼:
// 如果要求延遲載入,則延遲載入
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行:
@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
中,則會將查詢出來的結果放到快取中。
// 從資料庫中讀取操作
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有關。
@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結構儲存的:
/**
* 一級快取的實現類,部分原始碼
*/
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
方法中,就有快取的使用:
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
是在哪裡建立的呢?通過呼叫的追溯,可以找到它的建立:
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裡面的:
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
的原始碼我們可以知道,預設查詢後新增的快取儲存在待提交物件裡。
public void putObject(Object key, Object object) {
// 暫存 KV 到 entriesToAddOnCommit 中
entriesToAddOnCommit.put(key, object);
}
只有等到commit
的時候才會去刷入快取。
public void commit() {
// 如果 clearOnCommit 為 true ,則清空 delegate 快取
if (clearOnCommit) {
delegate.clear();
}
// 將 entriesToAddOnCommit、entriesMissedInCache 刷入 delegate 中
flushPendingEntries();
// 重置
reset();
}
檢視clear
程式碼,只是做了標記,並沒有真正釋放物件。在查詢時根據標記直接返回空,在commit
才真正釋放物件:
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
會清空這些臨時快取:
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
:返回物件處理器
這四個物件的介面的所有方法都可以用外掛攔截。
外掛的實現程式碼如下:
// 建立 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()
方法的呼叫,繼續檢視這個方法:
/**
* 應用所有外掛
*
* @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的動態代理,攔截方法的實現:
/**
* 建立目標類的代理物件
*
* @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
方法,這個方法實現的是查詢我們自定義攔截器的註解,通過註解確定哪些方法需要被攔截:
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操作的代理,畢竟動態代理還是有效能損耗的。