在進行軟件開發過程當中總會遇到一些公用代碼須要被提取出來,這個時候代理是一個好的解決方案,Mybatis也藉助JDK代理實現了一套攔截器機制,能夠在被攔截的方法先後加入通用邏輯,並經過@Intercepts和@Signature註解指定須要被代理的接口和方法。java
場景:須要在插入或修改數據庫時動態加入修改時間和修改人,並記錄下執行數據庫操做花費時間。sql
@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")); } }
<plugins> <plugin interceptor="org.apache.ibatis.plugin.MyTestInterceptor"> <property name="name" value="abc" /> </plugin> </plugins>
看日誌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
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。數組
@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是個數組,說明能夠配置多個攔截規則工具
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。測試
Mybatis在解析配置文件的時候會把自定義的攔截器加到InterceptorChain中,InterceptorChain中有個interceptors集合, 用InterceptorChain能夠對接口進行連接攔截,它的pluginAll實際上就是遍歷interceptors集合調用plugin。
mybatis內部在建立幾個關鍵對象的時候進行interceptorChain.pluginAll(obj),這些對象的上層接口分別是Executor, ParameterHandler, ResultSetHandler, StatementHandler,這4個接口涵蓋了了從獲取參數到構造sql,再到執行sql解析結果的過程,因此能夠在這個過程的任何一個地方進行攔截,只須要配置好攔截的接口及方法參數便可。