精盡MyBatis源碼分析 – 插件機制
- 2020 年 11 月 27 日
- 筆記
- mybatis, 源碼解析, 精盡MyBatis源碼分析
該系列文檔是本人在學習 Mybatis 的源碼過程中總結下來的,可能對讀者不太友好,請結合我的源碼注釋(Mybatis源碼分析 GitHub 地址、Mybatis-Spring 源碼分析 GitHub 地址、Spring-Boot-Starter 源碼分析 GitHub 地址)進行閱讀
MyBatis 版本:3.5.2
MyBatis-Spring 版本:2.0.3
MyBatis-Spring-Boot-Starter 版本:2.1.4
插件機制
開源框架一般都會提供插件或其他形式的擴展點,供開發者自行擴展,增加框架的靈活性
當然,MyBatis 也提供了插件機制,基於它開發者可以進行擴展,對 MyBatis 的功能進行增強,例如實現分頁、SQL分析、監控等功能,本文會對 MyBatis 插件機制的原理以及如何實現一個自定義的插件來進行講述
我們在編寫插件時,除了需要讓插件類實現 org.apache.ibatis.plugin.Interceptor
接口,還需要通過註解標註該插件的攔截點,也就是插件需要增強的方法,MyBatis 只提供下面這些類中定義的方法能夠被增強:
-
Executor:執行器
-
ParameterHandler:參數處理器
-
ResultSetHandler:結果集處理器
-
StatementHandler:Statement 處理器
植入插件邏輯
在《MyBatis的SQL執行過程》一系列文檔中,有講到在創建Executor、ParameterHandler、ResultSetHandler和StatementHandler對象時,會調用InterceptorChain
的pluginAll
方法,遍歷所有的插件,調用Interceptor
插件的plugin
方法植入相應的插件邏輯,所以在 MyBatis 中只有上面的四個對象中的方法可以被增強
代碼如下:
// Configuration.java
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
// <1> 獲得執行器類型
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
// <2> 創建對應實現的 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);
}
// <3> 如果開啟緩存,創建 CachingExecutor 對象,進行包裝
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// <4> 應用插件
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
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;
}
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;
}
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement,
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement,
parameterObject, rowBounds, resultHandler, boundSql);
// 將 Configuration 全局配置中的所有插件應用在 StatementHandler 上面
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
分頁插件示例
我們先來看一個簡單的插件示例,代碼如下:
@Intercepts({
@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)
})
public class ExamplePlugin implements Interceptor {
// Executor的查詢方法:
// public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler)
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
RowBounds rowBounds = (RowBounds) args[2];
if (rowBounds == RowBounds.DEFAULT) { // 無需分頁
return invocation.proceed();
}
/*
* 將query方法的 RowBounds 入參設置為空對象
* 也就是關閉 MyBatis 內部實現的分頁(邏輯分頁,在拿到查詢結果後再進行分頁的,而不是物理分頁)
*/
args[2] = RowBounds.DEFAULT;
MappedStatement mappedStatement = (MappedStatement) args[0];
BoundSql boundSql = mappedStatement.getBoundSql(args[1]);
// 獲取 SQL 語句,拼接 limit 語句
String sql = boundSql.getSql();
String limit = String.format("LIMIT %d,%d", rowBounds.getOffset(), rowBounds.getLimit());
sql = sql + " " + limit;
// 創建一個 StaticSqlSource 對象
SqlSource sqlSource = new StaticSqlSource(mappedStatement.getConfiguration(), sql, boundSql.getParameterMappings());
// 通過反射獲取並設置 MappedStatement 的 sqlSource 字段
Field field = MappedStatement.class.getDeclaredField("sqlSource");
field.setAccessible(true);
field.set(mappedStatement, sqlSource);
// 執行被攔截方法
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
// default impl
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// default nop
}
}
在上面的分頁插件中,@Intercepts
和@Signature
兩個註解指定了增強的方法是Executor.query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler)
,也就是我們使用到的 Executor 執行數據庫查詢操作的方法
在實現的 intercept
方法中,通過 RowBounds
參數獲取分頁信息,並生成相應的 SQL(拼接了 limit) ,並使用該 SQL 作為參數重新創建一個 StaticSqlSource
對象,最後通過反射替換 MappedStatement
對象中的 sqlSource
字段,這樣就實現了一個簡單的分頁插件
上面只是一個簡單的示例,實際場景中慎用
Interceptor
org.apache.ibatis.plugin.Interceptor
:攔截器接口,代碼如下:
public interface Interceptor {
/**
* 攔截方法
*
* @param invocation 調用信息
* @return 調用結果
* @throws Throwable 若發生異常
*/
Object intercept(Invocation invocation) throws Throwable;
/**
* 應用插件。如應用成功,則會創建目標對象的代理對象
*
* @param target 目標對象
* @return 應用的結果對象,可以是代理對象,也可以是 target 對象,也可以是任意對象。具體的,看代碼實現
*/
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
/**
* 設置攔截器屬性
*
* @param properties 屬性
*/
default void setProperties(Properties properties) {
// NOP
}
}
- intercept方法:攔截方法,插件的增強邏輯
- plugin方法:應用插件,往目標對象中植入相應的插件邏輯,如果應用成功則返回一個代理對象(JDK動態代理),否則返回原始對象,默認調用
Plugin
的wrap
方法 - setProperties方法:設置攔截器屬性
Invocation
org.apache.ibatis.plugin.Invocation
:被攔截的對象信息,代碼如下:
public class Invocation {
/**
* 目標對象
*/
private final Object target;
/**
* 方法
*/
private final Method method;
/**
* 參數
*/
private final Object[] args;
public Invocation(Object target, Method method, Object[] args) {
this.target = target;
this.method = method;
this.args = args;
}
// 省略 getter setter 方法
}
Plugin
org.apache.ibatis.plugin.Plugin
:實現InvocationHandler接口,用於對攔截的對象進行,一方面提供創建動態代理對象的方法,另一方面實現對指定類的指定方法的攔截處理,MyBatis插件機制的核心類
構造方法
public class Plugin implements InvocationHandler {
/**
* 目標對象
*/
private final Object target;
/**
* 攔截器
*/
private final Interceptor interceptor;
/**
* 攔截的方法映射
*
* KEY:類
* VALUE:方法集合
*/
private final Map<Class<?>, Set<Method>> signatureMap;
private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
this.target = target;
this.interceptor = interceptor;
this.signatureMap = signatureMap;
}
}
wrap方法
wrap(Object target, Interceptor interceptor)
方法,創建目標類的代理對象,方法如下:
public static Object wrap(Object target, Interceptor interceptor) {
// <1> 獲得攔截器中需要攔截的類的方法集合
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
// <2> 獲得目標對象的 Class 對象
Class<?> type = target.getClass();
// <3> 獲得目標對象所有需要被攔截的 Class 對象(父類或者接口)
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
// <4> 若存在需要被攔截的,則為目標對象的創建一個動態代理對象(JDK 動態代理),代理類為 Plugin 對象
if (interfaces.length > 0) {
// 因為 Plugin 實現了 InvocationHandler 接口,所以可以作為 JDK 動態代理的調用處理器
return Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap));
}
// <5> 如果沒有,則返回原始的目標對象
return target;
}
- 調用
getSignatureMap
方法,獲得攔截器中需要攔截的類的方法集合,有就是通過@Intercepts
和@Signature
兩個註解指定的增強的方法 - 獲得目標對象的 Class 對象(父類或者接口)
- 獲得目標對象所有需要被攔截的 Class 對象
- 如果需要被攔截,則為目標對象的創建一個動態代理對象(JDK 動態代理),代理類為
Plugin
對象,並返回該動態代理對象 - 否則返回原始的目標對象
getSignatureMap方法
getSignatureMap(Interceptor interceptor)
方法,獲取插件需要增強的方法,方法如下:
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
// 獲取 @Intercepts 註解
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());
}
// 獲取 @Intercepts 註解中的 @Signature 註解
Signature[] sigs = interceptsAnnotation.value();
Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
for (Signature sig : sigs) {
// 為 @Signature 註解中定義類名創建一個方法數組
Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
try {
// 獲取 @Signature 註解中定義的方法對象
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;
}
- 通過該插件上面的
@Intercepts
和@Signature
註解,獲取到所有需要被攔截的對象中的需要增強的方法
getAllInterfaces方法
getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap)
方法,判斷目標對象是否需要被插件應用,方法如下:
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
// 接口的集合
Set<Class<?>> interfaces = new HashSet<>();
// 循環遞歸 type 類,機器父類
while (type != null) {
// 遍歷接口集合,若在 signatureMap 中,則添加到 interfaces 中
for (Class<?> c : type.getInterfaces()) {
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
// 獲得父類
type = type.getSuperclass();
}
// 創建接口的數組
return interfaces.toArray(new Class<?>[interfaces.size()]);
}
- 入參
signatureMap
就是getSignatureMap
方法返回的該插件需要增強的方法 - 返回存在於
signatureMap
集合中所有目標對象的父類或者接口
invoke方法
invoke(Object proxy, Method method, Object[] args)
方法,動態代理對象的攔截方法,方法如下:
@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);
}
}
- 獲得目標方法所在的類需要被攔截的方法
- 如果被攔截的方法包含當前方法,則將當前方法封裝成
Invocation
對象,調用Interceptor
插件的intercept
方法,執行插件邏輯 - 否則執行原有方法
這樣一來,當你調用了目標對象的對應方法時,則會進入該插件的intercept
方法,執行插件邏輯,擴展功能
InterceptorChain
org.apache.ibatis.plugin.InterceptorChain
:攔截器鏈,用於將所有的攔截器按順序將插件邏輯植入目標對象,代碼如下:
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<>();
public Object pluginAll(Object target) {
// 遍歷攔截器集合
for (Interceptor interceptor : interceptors) {
// 調用攔截器的 plugin 方法植入相應的插件邏輯
target = interceptor.plugin(target);
}
return target;
}
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
配置MyBatis插件都會保存在interceptors
集合中,可以回顧到《初始化(一)之加載mybatis-config.xml》的XMLConfigBuilder小節的pluginElement
方法,會將解析到的依次全部添加到Configuration
的InterceptorChain
對象中,代碼如下:
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
// 遍歷 <plugins /> 標籤
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
// <1> 創建 Interceptor 對象,並設置屬性
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
interceptorInstance.setProperties(properties);
// <2> 添加到 configuration 中
configuration.addInterceptor(interceptorInstance);
}
}
}
總結
本文分析了 MyBatis 中插件機制,總體來說比較簡單的,想要實現一個插件,需要實現 Interceptor
接口,並通過@Intercepts
和@Signature
兩個註解指定該插件的攔截點(支持對Executor、ParameterHandler、ResultSetHandler 和 StatementHandler 四個對象中的方法進行增強),在實現的intercept
方法中進行邏輯處理
在 MyBatis 初始化的時候,會掃描插件,將其添加到InterceptorChain
中
然後 MyBatis 在 SQL 執行過程中,創建上面四個對象的時候,會將創建的對象交由InterceptorChain
去處理,遍歷所有的插件,通過插件的plugin
方法為其創建一個動態代理對象並返回,代理類是Plugin
對象
在Plugin
對象中的invoke
方法中,將請求交由插件的intercept
方法去處理
雖然 MyBatis 的插件機制比較簡單,但是想要實現一個完善且高效的插件卻比較複雜,可以參考PageHelper分頁插件
到這裡,相信大家對 MyBatis 的插件機制有了一定的了解,感謝大家的閱讀!!!😄😄😄
參考文章:芋道源碼《精盡 MyBatis 源碼分析》