Mybatis攔截器機制深刻

在進行軟件開發過程當中總會遇到一些公用代碼須要被提取出來,這個時候代理是一個好的解決方案,Mybatis也藉助JDK代理實現了一套攔截器機制,能夠在被攔截的方法先後加入通用邏輯,並經過@Intercepts和@Signature註解指定須要被代理的接口和方法。java

1、實例

場景:須要在插入或修改數據庫時動態加入修改時間和修改人,並記錄下執行數據庫操做花費時間。sql

1. 實現自定義攔截器

@Intercepts({@Signature(type=Executor.class, method="update", args={MappedStatement.class, Object.class})})
public class MyTestInterceptor implements Interceptor {

	public Object intercept(Invocation invocation) throws Throwable {
		Object arg = invocation.getArgs()[1];
		if(arg instanceof BaseBean) {
			BaseBean bean = (BaseBean) arg;
			bean.setUpdatetime(System.currentTimeMillis());
			bean.setUpdator("login user");
		}
		
		long t1 = System.currentTimeMillis();
		//執行後面業務邏輯
		Object obj = invocation.proceed();
		
		long t2 = System.currentTimeMillis();
		System.out.println("cost time:" + (t2-t1) + "ms");
		return obj;
	}

	public Object plugin(Object target) {
		return Plugin.wrap(target, this);
	}

	public void setProperties(Properties properties) {
		System.out.println("這是配置文件傳過來的參數: " + properties.get("name"));
	}
}

2. 加入配置文件

<plugins>
	<plugin interceptor="org.apache.ibatis.plugin.MyTestInterceptor">
		<property name="name" value="abc" />
	</plugin>
</plugins>

3. 觸發插入操做

看日誌updatetime和updator參數是跟intercept中傳入的同樣數據庫

這是配置文件傳過來的參數: abc
DEBUG [main] - ==>  Preparing: insert into Author (id,username,password,email,bio,updatetime,updator) values (?,?,?,?,?,?,?) 
DEBUG [main] - ==> Parameters: 500(Integer), 張三(String), ******(String), 張三@222.com(String), Something...(String), 1478778990865(Long), login user(String)
DEBUG [main] - <==    Updates: 1
cost time:773ms

2、源碼分析

1. 攔截器接口Interceptor

Interceptor接口是暴露給用戶的,一般在自定義Interceptor的plugin方法中實現代理,(其實給Interceptor子類傳入被代理對象後理論上能夠對該對象作任何事,包括修改數據,替換對象,甚至從新實現一套代理機制等),Mybaits已經實現了代理機制,而且提供了@Intercepts和@Signature使用規則,Mybaits的代理實現封裝在Plugin工具類中,只須要調用Plugin的wrap方法便可,看下面測試代碼:apache

public class PluginTest {

	@Test
	public void mapPluginShouldInterceptGet() {
		Map map = new HashMap();
		map = (Map) new AlwaysMapPlugin().plugin(map);
		assertEquals("Always", map.get("Anything"));
	}

	@Intercepts({ @Signature(type = Map.class, method = "get", args = { Object.class }) })
	public static class AlwaysMapPlugin implements Interceptor {
		public Object intercept(Invocation invocation) throws Throwable {
			return "Always";
		}

		public Object plugin(Object target) {
			return Plugin.wrap(target, this);
		}

		public void setProperties(Properties properties) {
		}
	}
}

對Map接口的get方法進行了攔截,map.get("Anything")的時候會被攔截進入到AlwaysMapPlugin的intercept方法中,在這裏被截斷返回Always。數組

2. 註解規則

@Intercepts和@Signature註解,這兩個註解定義了該自定義攔截器攔截哪一個接口的哪一個方法。mybatis

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Signature {
	Class<?> type();
	String method();
	Class<?>[] args();
}



@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
	Signature[] value();
}

Signature中有三個屬性,分別是目標接口的Class,須要攔截的方法,攔截方法的參數app

Intercepts中Signature是個數組,說明能夠配置多個攔截規則工具

3. 代理工具類Plugin

wrap方法中先獲取了Interceptor子類上面的@Intercepts和@Signature註解,根據註解能夠取到須要被代理的接口,再把這些接口跟代理目標類的接口取交集,並把這些交集接口用JDK實現代理對象並返回,JDK實現代理機制須要一個執行處理類InvocationHandler,Plugin自己也實現了JDK的InvocationHandler類,在構造JDK代理對象的時候傳入Plugin對象,源碼分析

public class Plugin implements InvocationHandler {

	private Object target;
	private Interceptor interceptor;
	private 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) {
		// interceptor上定義的須要被代理的接口及方法
		Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
		// 被代理對象的class
		Class<?> type = target.getClass();
		//上面二者的交集就是須要被代理的接口
		Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
		if (interfaces.length > 0) {
			//使用jdk實現動態代理
			return Proxy.newProxyInstance(type.getClassLoader(), interfaces,
					new Plugin(target, interceptor, signatureMap));
		}
		return target;
	}

	/**
	 * @Intercepts( {  
       @Signature(type = Executor.class, method = "query", args = {  
              				MappedStatement.class, Object.class, RowBounds.class,  
              				ResultHandler.class }),  
       @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class }) }
       
	 * @param interceptor
	 * @return
	 */
	private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
		Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
		if (interceptsAnnotation == null) { // issue #251
			throw new PluginException(
					"No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
		}
		Signature[] sigs = interceptsAnnotation.value();
		Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
		for (Signature sig : sigs) {
			Set<Method> methods = signatureMap.get(sig.type());
			if (methods == null) {
				methods = new HashSet<Method>();
				signatureMap.put(sig.type(), methods);
			}
			try {
				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<Class<?>>();
		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()]);
	}
	
	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);
		}
	}
}

調用代理對象方法是會進入到Plugin的invoke方法,invoke方法爲動態代理執行方法,執行invoke方法時會判斷是不是攔截的方法,若是是則執行自定義interceptor的intercept方法,並把攔截的目標、方法、參數封裝到Invocation中傳過去, 這就進入到了自定義攔截器的控制範圍,能夠在待執行的目標方法先後添加邏輯,如上面例子中的MyTestInterceptor。測試

4. 代理鏈InterceptorChain

Mybatis在解析配置文件的時候會把自定義的攔截器加到InterceptorChain中,InterceptorChain中有個interceptors集合, 用InterceptorChain能夠對接口進行連接攔截,它的pluginAll實際上就是遍歷interceptors集合調用plugin。

5. mybatis使用攔截機制

mybatis內部在建立幾個關鍵對象的時候進行interceptorChain.pluginAll(obj),這些對象的上層接口分別是Executor, ParameterHandler, ResultSetHandler, StatementHandler,這4個接口涵蓋了了從獲取參數到構造sql,再到執行sql解析結果的過程,因此能夠在這個過程的任何一個地方進行攔截,只須要配置好攔截的接口及方法參數便可。

相關文章
相關標籤/搜索