Mybatis插件實現原理
本文如有任何紕漏、錯誤,請不吝指出,謝謝!
首先,我並沒有使用過 Mybatis的插件,但是這個和我寫這篇文章並不衝突,估計能真正使用到插件的人也比較少,寫這篇文章的目的主要是看源碼時稍微看到了下它的插件實現,發現還挺繞的,於是就特意琢磨了下,然後留了一個記錄。
mybatis
中的插件,也就是攔截器interceptor
,也挺有意思的。
它的簡單使用,就直接拿文檔中的示例來簡單說下
使用
使用方式很簡單
// 使用這個註解,表明這是一個攔截器
@Intercepts(
// 方法簽名
{@Signature(
// 被攔截方法所在的類
type= Executor.class,
// 被攔截方法名
method = "update",
// 被攔截方法參數列表類型
args = {MappedStatement.class ,Object.class})}
)
public class ExamplePlugin implements Interceptor {
// 攔截時具體行爲
@Override
public Object intercept(Invocation invocation) throws Throwable {
// implement pre-processing if needed
Object returnObject = invocation.proceed();
// implement post-processing if needed
return returnObject;
}
}
然後如果是xml配置的話
<plugins>
<plugin interceptor="org.mybatis.example.ExamplePlugin">
<property name="someProperty" value="100"/>
</plugin>
</plugins>
如果是 SpringBoot
的話,應該是配合自動配置使用,將上面的類使用@Component
註解,交由Spring
容器管理,然後註冊到mybatis
的InterceptorChain
mybaits
目前支持攔截的類和方法,有下面這些
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- StatementHandler (prepare, parameterize, batch, update, query)
- ParameterHandler (getParameterObject, setParameters)
- ResultsetHandle (handleResultSets, handleOutputParameters)
實現原理
有可能從上面的使用猜出來它的攔截器的實現原理,那就是動態代理的方式,只不過 mybatis
這裏並不是很直接的來使用代理,繞了個彎,於是給人感覺特別暈,也說不好這個實現是不是有些問題
就先從Executor
說起吧
我們從SqlSessionFactory
獲取一個SqlSession
時,會創建一個新的Executor
實例,這個實際的創建動作在這裏
org.apache.ibatis.session.Configuration#newExecutor
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
// 根據設置的執行器類型,創建相應類型的執行器
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
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);
}
// 如果開啓了緩存,使用緩存,這裏緩存執行器有點類似靜態代理了
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// 將原始執行器對象,包裝下,生成一個新的執行器,代理後的對象
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
InterceptorChain
是一個管家,有點類似於FilterChain
,但是注意,其實差別非常的大
public class InterceptorChain {
// 所有攔截器列表
private final List<Interceptor> interceptors = new ArrayList<>();
// 這裏可能會是層層代理對象,一套又一套的,具體取決於攔截器的個數和被攔截的
// 方法所在的類
// 配置的攔截器數和每一次代理對象生成次數並不相同,會小於等於攔截器的個數
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
// 返回最後一次創建的代理對象
return target;
}
// 註冊攔截器的
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
}
Interceptor
是一個接口,也就是mybatis直接暴露給用戶使用的需要用戶實現的攔截器接口
public interface Interceptor {
// 實現類填充自己的邏輯,參數爲Invocation,
Object intercept(Invocation invocation) throws Throwable;
// 默認方法,創建代理對象
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
// 實現類去做些事情
default void setProperties(Properties properties) {
// NOP
}
}
着重看下一這個
// 首先這是一個Jdk動態代理的InvocationHandler實現類
public class Plugin implements InvocationHandler {
private final Object target;
private final Interceptor interceptor;
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;
}
// 靜態方法,用來直接創建代理對象
public static Object wrap(Object target, Interceptor interceptor) {
// 獲取當前攔截器需要被攔截的所有的方法
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
// 獲取被代理對象的Class
Class<?> type = target.getClass();
// 把被代理對象所有能在簽名Map中找到的直接實現的接口和祖先接口,查找出來
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
// 找到了就創建代理對象
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
// 創建一個InvocationHandler實例
new Plugin(target, interceptor, signatureMap));
}
// 沒找到 就返回
// 這個對象不一定說一個未經代理過的對象,也可能是代理過的
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)) {
// 如果當前被調用的方法是需要被攔截的,那麼就執行我們自定義的攔截邏輯
// interceptor是我們自定義的攔截器,在我們自定義的攔截器裏,需要獲取到
// 原委託對象,被調用的方法,以及參數,這裏做了很好的封裝,將用戶的使用和
// 具體的實現,做了一個完全的分離,用戶感知不到任何具體的實現
// Invocation#proceed 就做了一件事 method.invoke(target,args);
return interceptor.intercept(new Invocation(target, method, args));
}
// 如果當前被調用的方法沒有被攔截,那麼直接調用原方法
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
// 獲取攔截器指定的被攔截方法的方法簽名
// key是被攔截方法的返回值類型
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());
}
// 獲取配置的被攔截方法的簽名信息
Signature[] sigs = interceptsAnnotation.value();
Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
// 統統放到Map
for (Signature sig : sigs) {
Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
try {
// 將定義構建成實際的Method對象
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;
}
// 獲取被代理對象所有的接口信息
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
Set<Class<?>> interfaces = new HashSet<>();
while (type != null) {
for (Class<?> c : type.getInterfaces()) {
if (signatureMap.containsKey(c)) {
// 如果被代理類接口和返回值類型一致,接口加進來
interfaces.add(c);
}
}
// 找到被代理類的父類,然後繼續查找接口信息
type = type.getSuperclass();
}
return interfaces.toArray(new Class<?>[interfaces.size()]);
}
}
到這裏就說完了mybatis
插件的實現原理
整體實現其實也很簡單,我們想給那個類的哪個方法攔截一下,把實現攔截器,然後配置下要攔截的信息,之後mybatis
給我們生成一個動態代理對象,就可以了。
下面我們以具體案例來更加深刻的認識理解其實現原理
加強理解
我們使用具體的案例,來走一遍
Case 1
單攔截器單攔截方法
@Intercepts(
{@Signature(
type= Executor.class,
method = "update",
args = {MappedStatement.class ,Object.class})}
)
public class ExamplePlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("before execute update");
Object returnObject = invocation.proceed();
System.out.println("after execute update");
return returnObject;
}
}
假設目前我們就設置了這麼一個攔截器
那麼就會生成一個CachingExecutor
的代理對象,我們假設代理對象的全限定名是com.sun.proxy.$Proxy0
,並且這個對象對象是implements Executor
的
這時調用代理對象的update
方法調用棧如下
Case 2
單攔截器多攔截方法
假設有個需求需要給StatementHandler
的update
增加點一樣的攔截邏輯
@Intercepts(
{@Signature(
type= Executor.class,
method = "update",
args = {MappedStatement.class ,Object.class})},
{@Signature(
type= StatementHandler.class,
method = "update",
args = {Statement.class})}
)
public class ExamplePlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("before execute update");
Object returnObject = invocation.proceed();
System.out.println("after execute update");
return returnObject;
}
}
同樣是獲取執行器的邏輯,這個時候我們得到的代理後的對象不會是同時實現了Executor
和StatementHandler
接口的,只能實現Executor
接口,因爲這個
// 這個時候type爲 CachingExecutor
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
Set<Class<?>> interfaces = new HashSet<>();
while (type != null) {
// type的接口Executor顯然是在Map中的
for (Class<?> c : type.getInterfaces()) {
// 僅允許對接口爲Executor的實例做代理,因爲代理後的類一定得是一個Executor的實現類
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
type = type.getSuperclass();
}
// 最終也就只能找到Executor這麼一個接口了
return interfaces.toArray(new Class<?>[interfaces.size()]);
}
這種情況下就和上面一樣了
區別在於,如果我們需要創建一個StatementHandler
的實例時,代理對象就會變成StatementHandler
的實現類實例了
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
// 被攔截器增強後的代理對象StatementHandler
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
Case 3
多攔截器單攔截方法
如果有多個攔截器呢?
@Intercepts(
{@Signature(
type= Executor.class,
method = "update",
args = {MappedStatement.class ,Object.class})}
)
public class ExamplePluginA implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("before execute update");
Object returnObject = invocation.proceed();
System.out.println("after execute update");
return returnObject;
}
}
@Intercepts(
{@Signature(
type= Executor.class,
method = "flushStatements",
args = {})}
)
public class ExamplePluginB implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("before execute flushStatements");
Object returnObject = invocation.proceed();
System.out.println("after execute flushStatements");
return returnObject;
}
}
上面有兩個攔截器,會出現代理對象成爲委託對象的情況,也就是對一個已經是代理對象的對象,再去爲它生成代理對象。
假設一開始生成了一個代理類類名爲$Proxy0
,在InterceptorChain#pluginAll
方法中,會將這個代理類作爲委託類繼續生成一個新的代理類,第一個代理類會執行ExamplePluginA
攔截邏輯,第二個代理類會執行ExamplePluginB
的攔截邏輯。
從pluginAll
方法中可以看到,位於攔截器鏈末尾的,也可以說是最後被創建的那個代理類,是最先被調用的,因爲實際返回給客戶端使用的,就是它。
假設第二個代理類類名爲$Proxy1
,那麼看一下這種情況下方法的調用棧是啥樣的
總結
這裏主要說一下mybatis
爲什麼這樣實現?
-
爲什麼不採用過濾器鏈的實現方式?
過濾器鏈的方式,在這裏並不怎麼適用,雖然也許可以,個人觀點覺得,過濾器鏈更多的是用於
過濾
,它是對調用請求進行條件判斷,是否滿足某種條件進而是否具備某個繼續執行的權限。它決定的是,調用是否可以繼續進行下去。而攔截器,更多的是攔截調用請求,對攔截後的請求做一些額外的附加邏輯,代理其實就是這種方式,它是一種對原有邏輯的增強,不會改變最初調用的目的地,無論做了多少層的攔截處理,最終都會到達它的目的地(發生異常除外)
如果用過濾器,需要多個過濾器鏈,並且每個類中都需要持有對其過濾器鏈的引用等等,用我淺薄的知識稍微想了下,並非不可,實而太麻煩了。最優選擇還是動態代理。
-
爲什麼要做封裝?
回想一下我們直接使用Jdk的動態代理,自己動手實現
InvocationHandler
,寫完就沒了,也就是說就做了一次增強。但是這裏情況更加複雜,可能有多個類的多個方法需要被增強,以
Executor
爲例,如果我們每個方法都要增強,並且每個方法的增強邏輯都不一樣,而實際上,最終也只生成一個代理對象,那我們就要在一個InvocationHandler
的invoke
方法中,大量的if-else
判斷(這裏也可以優化if-else
),當前調用的是哪個方法,應該執行什麼樣的邏輯。這樣在實現上是沒問題的,只不過對一個框架來說,易用性也是強有力的吸引力、競爭力。如果這些不優雅的或者用戶實現起來比較麻煩的事,框架都給封裝好,簡單易用,那麼肯定會更加受歡迎,這肯定也會是一個好的框架。