該系列文件是本人在學習 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 原始碼分析》