最近在用mybatis作項目,須要用到mybatis的攔截器功能,就順便把mybatis的攔截器源碼大體的看了一遍,爲了溫故而知新,在此就按照本身的理解由淺入深的理解一下它的設計。
和你們分享一下,不足和謬誤之處歡迎交流。直接入正題。
首先,先無論mybatis的源碼是怎麼設計的,先假設一下本身要作一個攔截器應該怎麼作。攔截器的實現都是基於代理的設計模式設計的,簡單的說就是要創造一個目標類的代理類,在代理類中執行目標類的方法並攔截執行攔截器代碼。
那麼咱們就用JDK的動態代理設計一個簡單的攔截器:
將被攔截的目標接口: java
public interface Target { public void execute(); }
目標接口的一個實現類:sql
public class TargetImpl implements Target { public void execute() { System.out.println("Execute"); } }
利用JDK的動態代理實現攔截器:設計模式
package com.tangia.mybatis.proxy; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class TargetProxy implements InvocationHandler { private Object target; private TargetProxy(Object target) { this.target = target; } // 生成一個目標對象的代理對象 public static Object bind(Object target) { return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new TargetProxy(target)); } // 在執行目標對象方法前加上本身的攔截邏輯 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("我攔截了啊"); return method.invoke(target, args); } }
客戶端調用:mybatis
package com.tangia.mybatis.proxy; public class Client { public static void main(String[] args) { // 沒有被攔截以前 Target target = new TargetImpl(); target.execute(); //執行結果爲: Execute System.out.println("===================="); // 攔截後 target = (Target) TargetProxy.bind(target); target.execute(); // 執行結果爲:我攔截了啊 // Execute } }
上面的設計有幾個很是明顯的不足,首先說第一個,攔截邏輯被寫死在代理對象中: app
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //攔截邏輯被寫死在代理對象中,致使客戶端沒法靈活的設置本身的攔截邏輯 System.out.println("Begin"); return method.invoke(target, args); }
咱們能夠將攔截邏輯封裝到一個類中,客戶端在調用TargetProxy的bind()方法的時候將攔截邏輯一塊兒當成參數傳入:
定義一個攔截邏輯封裝的接口Interceptor,這纔是真正的攔截器接口。框架
public interface Interceptor { public void intercept(); }
那麼咱們的代理類就能夠改爲:學習
package com.tangia.mybatis.proxy; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class TargetProxy implements InvocationHandler { private Object target; private Interceptor interceptor; private TargetProxy(Object target, Interceptor interceptor) { this.target = target; this.interceptor = interceptor; } // 將攔截邏輯封裝到攔截器中,有客戶端生成目標類的代理類的時候一塊兒傳入,這樣客戶端就能夠設置不一樣的攔截邏輯。 public static Object bind(Object target, Interceptor interceptor) { return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new TargetProxy(target, interceptor)); } // 在執行目標對象方法前加上本身的攔截邏輯 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 執行客戶端定義的攔截邏輯 interceptor.intercept(); return method.invoke(target, args); } }
客戶端調用代碼:this
package com.tangia.mybatis.proxy; public class Client { public static void main(String[] args) { // 沒有被攔截以前 Target target = new TargetImpl(); target.execute(); // Execute System.out.println("===================="); // 攔截後 Interceptor interceptor = new Interceptor() { public void intercept() { System.out.println("Go Go Go!!!"); } }; target = (Target) TargetProxy.bind(target, interceptor); target.execute(); } }
固然,不少時候咱們的攔截器中須要判斷當前方法需不須要攔截,或者獲取當前被攔截的方法參數等。咱們能夠將被攔截的目標方法對象,參數信息傳給攔截器。
攔截器接口改爲: spa
public interface Interceptor { public void intercept(Method method, Object[] args); }
在代理類執行的時候能夠將當前方法和參數傳給攔截,即TargetProxy的invoke方法改成: 插件
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { interceptor.intercept(method, args); return method.invoke(target, args); }
在Java設計原則中有一個叫作迪米特法則,大概的意思就是一個類對其餘類知道得越少越好。其實就是減小類與類之間的耦合強度。這是從類成員的角度去思考的。
什麼叫越少越好,什麼是最少?最少就是不知道。
因此咱們是否是能夠這麼理解,一個類所要了解的類應該越少越好呢?
固然,這只是從類的角度去詮釋了迪米特法則。
甚至能夠反過來思考,一個類被其餘類瞭解得越少越好。
A類只讓B類瞭解總要強於A類讓B,C,D類都去了解。
舉個例子:
咱們的TargetProxy類中須要瞭解的類有哪些呢?
1. Object target 不須要了解,由於在TargetProxy中,target都被做爲參數傳給了別的類使用,本身不須要了解它。
2. Interceptor interceptor 須要瞭解,須要調用其intercept方法。
3. 一樣,Proxy須要瞭解。
4. Method method 參數須要瞭解,須要調用其invoke方法。
一樣,若是interceptor接口中須要使用intercept方法傳過去Method類,那麼也須要了解它。那麼既然Interceptor都須要使用Method,還不如將Method的執行也放到Interceptor中,再也不讓TargetProxy類對其瞭解。Method的執行須要target對象,因此也須要將target對象給Interceptor。將Method,target和args封裝到一個對象Invocation中,將Invocation傳給Interceptor。
Invocation:
package com.tangia.mybatis.proxy; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public 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; } // 將本身成員變量的操做盡可能放到本身內部,不須要Interceptor得到本身的成員變量再去操做它們, // 除非這樣的操做須要Interceptor的其餘支持。然而這兒不須要。 public Object proceed() throws InvocationTargetException, IllegalAccessException { return method.invoke(target, args); } public Object getTarget() { return target; } public void setTarget(Object target) { this.target = target; } public Method getMethod() { return method; } public void setMethod(Method method) { this.method = method; } public Object[] getArgs() { return args; } public void setArgs(Object[] args) { this.args = args; } }
Interceptor就變成:
public interface Interceptor { public Object intercept(Invocation invocation)throws Throwable ; }
TargetProxy的invoke方法就變成:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { return interceptor.intercept(new Invocation(target,method, args)); }
那麼就每個Interceptor攔截器實現都須要最後執行Invocation的proceed方法並返回。
客戶端調用:
Interceptor interceptor = new Interceptor() { public Object intercept(Invocation invocation) throws Throwable { System.out.println("Go Go Go!!!"); return invocation.proceed(); } };
好了,經過一系列調整,設計已經挺好了,不過上面的攔截器仍是有一個很大的不足,
那就是攔截器會攔截目標對象的全部方法,然而這每每是不須要的,咱們常常須要攔截器
攔截目標對象的指定方法。
假設目標對象接口有多個方法:
public interface Target { public void execute1(); public void execute2(); }
利用在Interceptor上加註解解決。
首先簡單的定義一個註解:
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE) public @interface MethodName { public String value(); }
在攔截器的實現類加上該註解:
@MethodName("execute1") public class InterceptorImpl implements Interceptor {...}
在TargetProxy中判斷interceptor的註解,看是否實行攔截:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { MethodName methodName = this.interceptor.getClass().getAnnotation(MethodName.class); if (ObjectUtils.isNull(methodName)) throw new NullPointerException("xxxx"); //若是註解上的方法名和該方法名同樣,才攔截 String name = methodName.value(); if (name.equals(method.getName())) return interceptor.intercept(new Invocation(target, method, args)); return method.invoke(this.target, args); }
最後客戶端調用:
Target target = new TargetImpl(); Interceptor interceptor = new InterceptorImpl(); target = (Target)TargetProxy.bind(target, interceptor); target.execute();
從客戶端調用代碼能夠看出,客戶端首先須要建立一個目標對象和攔截器,而後將攔截器和目標對象綁定並獲取代理對象,最後執行代理對象的execute()方法。
根據迪米特法則來說,其實客戶端根本不須要了解TargetProxy類。將綁定邏輯放到攔截器內部,客戶端只須要和攔截器打交道就能夠了。
即攔截器接口變爲:
public interface Interceptor { public Object intercept(Invocation invocation) throws Throwable ; public Object register(Object target); }
攔截器實現:
@MethodName("execute1") public class InterceptorImpl implements Interceptor { public Object intercept(Invocation invocation)throws Throwable { System.out.println("Go Go Go!!!"); return invocation.proceed(); } public Object register(Object target) { return TargetProxy.bind(target, this); } }
客戶端調用:
Target target = new TargetImpl(); Interceptor interceptor = new InterceptorImpl(); target = (Target)interceptor.register(target); target.execute1();
OK,上面的一系列過程其實都是mybatis的攔截器代碼結構,我只是學習了以後用最簡單的方法理解一遍罷了。
上面的TargetProxy其實就是mybatis的Plug類。Interceptor和Invocation幾乎同樣。只是mybatis的Interceptor支持的註解
更加複雜。 mybatis最終是經過將自定義的Interceptor配置到xml文件中:
<!-- 自定義處理Map返回結果的攔截器 --> <plugins> <plugin interceptor="com.gs.cvoud.dao.interceptor.MapInterceptor" /> </plugins>
經過讀取配置文件中的Interceptor,經過反射構造其實例,將全部的Interceptor保存到InterceptorChain中。
public class InterceptorChain { private final List<Interceptor> interceptors = new ArrayList<Interceptor>(); public Object pluginAll(Object target) { for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; } public void addInterceptor(Interceptor interceptor) { interceptors.add(interceptor); } public List<Interceptor> getInterceptors() { return Collections.unmodifiableList(interceptors); } }
mybatis的攔截器只能代理指定的四個類:ParameterHandler、ResultSetHandler、StatementHandler以及Executor。
這是在mybatis的Configuration中寫死的,例如(其餘三個相似):
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql); //將配置文件中讀取的全部的Interceptor都註冊到ParameterHandler中,最後經過每一個Interceptor的註解判斷是否須要攔截該ParameterHandler的某個方法。 parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler); return parameterHandler; }
因此咱們能夠自定義mybatis的插件(攔截器)修改mybatis的不少默認行爲,
例如,
經過攔截ResultSetHandler修改接口返回類型;
經過攔截StatementHandler修改mybatis框架的分頁機制;
經過攔截Executor查看mybatis的sql執行過程等等。