帶你瞭解 MyBatis 外掛設計演化過程
原文地址:帶你瞭解 MyBatis 外掛設計演化過程
部落格地址:http://www.extlight.com
之前寫過一篇 《Mybatis 外掛實現動態設定引數》 文章,介紹了 Mybatis 外掛的擴充套件和使用。筆者在空閒時間梳理了一下 MyBatis 外掛的工作原理,在此記錄和分享其外掛功能程式碼的演化過程。
一、原始程式碼
我們簡略 MyBatis 執行 SQL 的步驟,下邊的原始程式碼是依靠 Executor 執行 SQL 語句。
interface Executor { void execute(String sql); } class DefaultExecutor implements Executor { @Override public void execute(String sql) { System.out.println("執行:" + sql); } } public class Demo { public static void main(String[] args) { Executor executor = new DefaultExecutor(); executor.execute("select * from t_user"); } }
假設,我們需要 Executor 在執行 SQL 語句的前後打印出當前時間戳(方法增強),那該如何操作?
針對方法增強的情況,有 3 個方案:
-
修改原始碼:
修改 execute 方法,在執行 SQL 前後加入日誌列印方法。違背開源-封閉原則且維護繁瑣(如擴充套件的是第三方jar,需修改原始碼再打包)。且作為通用元件開發也不合適此方案。 -
使用繼承:
繼承父類,重寫方法,屬於縱向方法增強。只對一個類產生作用,如要對一批類進行方法增強,需要建立多個子類,擴充套件性不好。 -
動態代理:
動態生成代理物件,代替目標物件執行操作,無需修改原始碼,易擴充套件和維護。
二、動態代理
接下來我們使用動態代理方案,建立 TargetProxyHandler
class TargetProxyHandler implements InvocationHandler { private Object target; public TargetProxyHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("執行前:" + System.nanoTime()); Object result = method.invoke(target, args); System.out.println("執行後:" + System.nanoTime()); return result; } } class TargetProxyFactory { public static Object newProxy(Object target) { return Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), new TargetProxyHandler(target)); } } public class Demo { public static void main(String[] args) { Executor target = new DefaultExecutor(); Executor executor = (Executor) TargetProxyFactory.newProxy(target); executor.execute("select * from t_user"); } }
執行結果:
執行前:5344823093500
執行:select * from t_user
執行後:5344823399900
TargetProxyHandler 實現類用於執行被代理物件的目標方法( execute ),TargetProxyFactory 負責建立代理物件。
這樣,我們使用動態代理實現了日誌列印的需求。但又產生新的問題:
現在執行被代理物件的 execute 方法前後都有日誌列印,將來我們還想對其進行方法增強(如去掉日誌或新增事務)。還是得修改 TargetProxyHandler 原始碼,但其作為代理物件的執行方法的通用元件,不應該參雜業務程式碼,那我們應該處理呢?
這個問題的根源在於 invoke 方法上。我們需要把其執行內容抽離出來封裝到單獨的元件中,元件提供方法呼叫即可。
這種解決方案就是我們熟知的攔截器。
三、攔截器
我們需要建立 Interceptor 介面與 LogInterceptor 實現類,將 TargetProxyHandler 的 invoke 方法替換成攔截器的呼叫方法。
class Invocation {
private Object target;
private Method method;
private Object[] args;
public Invocation(Object target, Method method, Object[] args) {
this.target = target;
this.method = method;
this.args = args;
}
public Object process() throws Exception {
return method.invoke(target, args);
}
}
interface Interceptor {
Object intercept(Invocation invocation) throws Exception;
}
class LogInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Exception {
System.out.println("執行前:" + System.nanoTime());
Object result = invocation.process();
System.out.println("執行後:" + System.nanoTime());
return result;
}
}
class TargetProxyHandler implements InvocationHandler {
private Object target;
private Interceptor interceptor;
public TargetProxyHandler(Object target, Interceptor interceptor) {
this.target = target;
this.interceptor = interceptor;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Invocation invocation = new Invocation(target, method, args);
return interceptor.intercept(invocation);
}
}
class TargetProxyFactory {
public static Object newProxy(Object target, Interceptor interceptor) {
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new TargetProxyHandler(target, interceptor));
}
}
public class Demo {
public static void main(String[] args) {
Executor target = new DefaultExecutor();
Interceptor logIntercetor = new LogInterceptor();
Executor executor = (Executor) TargetProxyFactory.newProxy(target, logIntercetor);
executor.execute("select * from t_user");
}
}
執行結果同上。
上述程式碼中,我們將方法增強的程式碼從 TargetProxyHandler.invoke 剝離抽取到 LogInterceptor.intercept 中。TargetProxyHandler 得到釋放,LogInterceptor 作為業務程式碼可由業務決定其實現邏輯。
注意,我們還有一個問題沒解決,正如上述描述的,如果我們還要新增一個事務開啟,提交的功能,程式碼如何實現呢?
在上邊的程式碼中,我們定義了 Interceptor 介面,LogInterceptor 實現該介面用於處理日誌方法增強的業務。依瓢畫葫蘆,我們可以建立 TransactionInterceptor 類實現 Interceptor 介面用於處理事務。
問題出現了,現在有兩個攔截器,而 TargetProxyFactory 工廠類只能接受一個攔截器物件,我們如何同時使用這兩個攔截器呢?
當然是使用攔截器鏈!
四、攔截器鏈
建立 InterceptorChain 類封裝攔截器。
interface Interceptor {
Object intercept(Invocation invocation) throws Exception;
Object plugin(Object target);
}
class LogInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Exception {
System.out.println("執行前:" + System.nanoTime());
Object result = invocation.process();
System.out.println("執行後:" + System.nanoTime());
return result;
}
@Override
public Object plugin(Object target) {
return TargetProxyFactory.newProxy(target, this);
}
}
class TransactionInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Exception {
System.out.println("事務提交前");
Object result = invocation.process();
System.out.println("事務提交後");
return result;
}
@Override
public Object plugin(Object target) {
return TargetProxyFactory.newProxy(target, this);
}
}
class InterceptorChain {
private List<Interceptor> interceptors = new ArrayList<>();
public void addInterceptor(Interceptor interceptor) {
this.interceptors.add(interceptor);
}
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
}
class TargetProxyHandler implements InvocationHandler {
private Object target;
private Interceptor interceptor;
public TargetProxyHandler(Object target, Interceptor interceptor) {
this.target = target;
this.interceptor = interceptor;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Invocation invocation = new Invocation(target, method, args);
return interceptor.intercept(invocation);
}
}
class TargetProxyFactory {
public static Object newProxy(Object target, Interceptor interceptor) {
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new TargetProxyHandler(target, interceptor));
}
}
public class Demo {
public static void main(String[] args) {
Executor target = new DefaultExecutor();
Interceptor logIntercetor = new LogInterceptor();
Interceptor transactionIntercetor = new TransactionInterceptor();
InterceptorChain interceptorChain = new InterceptorChain();
interceptorChain.addInterceptor(logIntercetor);
interceptorChain.addInterceptor(transactionIntercetor);
Executor executor = (Executor) interceptorChain.pluginAll(target);
executor.execute("select * from t_user");
}
}
執行結果:
事務提交前
執行前:5467232073200
執行:select * from t_user
執行後:5467232129200
事務提交後
除了新增了 InterceptorChain,我們還修改 Interceptor 介面,為其定義了 plugin 方法,由攔截器自己維護建立代理物件。
在 InterceptorChain 中定義 pluginAll 方法,用於遍歷建立代理物件(第一次遍歷,被代理物件是 target,創建出代理物件為A;第二次遍歷,被代理物件是A,創建出代理物件是B)。
至此,MyBatis 外掛設計演化過程結束。當然,筆者是指簡單的梳理演變過程,MyBatis 外掛實際的執行程式碼要複雜很多,但思想和原理是大致相同的。