1. 程式人生 > 其它 >帶你瞭解 MyBatis 外掛設計演化過程

帶你瞭解 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 個方案:

  1. 修改原始碼:
    修改 execute 方法,在執行 SQL 前後加入日誌列印方法。違背開源-封閉原則且維護繁瑣(如擴充套件的是第三方jar,需修改原始碼再打包)。且作為通用元件開發也不合適此方案。

  2. 使用繼承:
    繼承父類,重寫方法,屬於縱向方法增強。只對一個類產生作用,如要對一批類進行方法增強,需要建立多個子類,擴充套件性不好。

  3. 動態代理:
    動態生成代理物件,代替目標物件執行操作,無需修改原始碼,易擴充套件和維護。

二、動態代理

接下來我們使用動態代理方案,建立 TargetProxyHandler

實現類和 TargetProxyFactory 工廠類。

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 實現類,將 TargetProxyHandlerinvoke 方法替換成攔截器的呼叫方法。

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 外掛實際的執行程式碼要複雜很多,但思想和原理是大致相同的。