由淺入深分析mybatis通過動態代理實現攔截器(外掛)的原理 !很值得一看
最近在用mybatis做專案,需要用到mybatis的攔截器功能,就順便把mybatis的攔截器原始碼大致的看了一遍,為了溫故而知新,在此就按照自己的理解由淺入深的理解一下它的設計。
和大家分享一下,不足和謬誤之處歡迎交流。直接入正題。
首先,先不管mybatis的原始碼是怎麼設計的,先假設一下自己要做一個攔截器應該怎麼做。攔截器的實現都是基於代理的設計模式設計的,簡單的說就是要創造一個目標類的代理類,在代理類中執行目標類的方法並攔截執行攔截器程式碼。
那麼我們就用JDK的動態代理設計一個簡單的攔截器:
將被攔截的目標介面:
Java程式碼
- public interface Target {
- public void execute();
- }
目標介面的一個實現類:
Java程式碼
- public class TargetImpl implements Target {
- public void execute() {
- System.out.println("Execute");
- }
- }
利用JDK的動態代理實現攔截器:
Java程式碼
- 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("Begin");
- return method.invoke(target, args);
- }
- }
客戶端呼叫:
Java程式碼
- public class Client {
- public static void main(String[] args) {
- //沒有被攔截之前
- Target target = new TargetImpl();
- target.execute(); //Execute
- //攔截後
- target = (Target)TargetProxy.bind(target);
- target.execute();
- //Begin
- //Execute
- }
上面的設計有幾個非常明顯的不足,首先說第一個,攔截邏輯被寫死在代理物件中:
Java程式碼
- public Object invoke(Object proxy, Method method,
- Object[] args) throws Throwable {
- //攔截邏輯被寫死在代理物件中,導致客戶端無法靈活的設定自己的攔截邏輯
- System.out.println("Begin");
- return method.invoke(target, args);
- }
我們可以將攔截邏輯封裝到一個類中,客戶端在呼叫TargetProxy的bind()方法的時候將攔截邏輯一起當成引數傳入:
定義一個攔截邏輯封裝的介面Interceptor,這才是真正的攔截器介面。
Java程式碼
- public interface Interceptor {
- public void intercept();
- }
那麼我們的代理類就可以改成:
Java程式碼
- 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);
- }
客戶端呼叫程式碼:
Java程式碼
- //客戶端可以定義各種攔截邏輯
- Interceptor interceptor = new Interceptor() {
- public void intercept() {
- System.out.println("Go Go Go!!!");
- }
- };
- target = (Target)TargetProxy.bind(target, interceptor);
- target.execute();
當然,很多時候我們的攔截器中需要判斷當前方法需不需要攔截,或者獲取當前被攔截的方法引數等。我們可以將被攔截的目標方法物件,引數資訊傳給攔截器。
攔截器介面改成:
Java程式碼
- public interface Interceptor {
- public void intercept(Method method, Object[] args);
- }
在代理類執行的時候可以將當前方法和引數傳給攔截,即TargetProxy的invoke方法改為:
Java程式碼
- 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:
Java程式碼
- 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就變成:
Java程式碼
- public interface Interceptor {
- public Object intercept(Invocation invocation)throws Throwable ;
- }
TargetProxy的invoke方法就變成:
Java程式碼
- public Object invoke(Object proxy, Method method,
- Object[] args) throws Throwable {
- return interceptor.intercept(new Invocation(target,
- method, args));
- }
那麼就每一個Interceptor攔截器實現都需要最後執行Invocation的proceed方法並返回。
客戶端呼叫:
Java程式碼
- Interceptor interceptor = new Interceptor() {
- public Object intercept(Invocation invocation) throws Throwable {
- System.out.println("Go Go Go!!!");
- return invocation.proceed();
- }
- };
好了,通過一系列調整,設計已經挺好了,不過上面的攔截器還是有一個很大的不足,
那就是攔截器會攔截目標物件的所有方法,然而這往往是不需要的,我們經常需要攔截器
攔截目標物件的指定方法。
假設目標物件介面有多個方法:
Java程式碼
- public interface Target {
- public void execute1();
- public void execute2();
- }
利用在Interceptor上加註解解決。
首先簡單的定義一個註解:
Java程式碼
- @Retention(RetentionPolicy.RUNTIME)
- @Target(ElementType.TYPE)
- public @interface MethodName {
- public String value();
- }
在攔截器的實現類加上該註解:
Java程式碼
- @MethodName("execute1")
- public class InterceptorImpl implements Interceptor {...}
在TargetProxy中判斷interceptor的註解,看是否實行攔截:
Java程式碼
- 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);
- }
最後客戶端呼叫:
Java程式碼
- Target target = new TargetImpl();
- Interceptor interceptor = new InterceptorImpl();
- target = (Target)TargetProxy.bind(target, interceptor);
- target.execute();
從客戶端呼叫程式碼可以看出,客戶端首先需要建立一個目標物件和攔截器,然後將攔截器和目標物件繫結並獲取代理物件,最後執行代理物件的execute()方法。
根據迪米特法則來講,其實客戶端根本不需要了解TargetProxy類。將繫結邏輯放到攔截器內部,客戶端只需要和攔截器打交道就可以了。
即攔截器介面變為:
Java程式碼
- public interface Interceptor {
- public Object intercept(Invocation invocation) throws Throwable ;
- public Object register(Object target);
攔截器實現:
Java程式碼
- @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);
- }
- }
客戶端呼叫:
Java程式碼
- 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檔案中:
Xml程式碼
- <!-- 自定義處理Map返回結果的攔截器 -->
- <plugins>
- <plugin interceptor="com.gs.cvoud.dao.interceptor.MapInterceptor" />
- </plugins>
通過讀取配置檔案中的Interceptor,通過反射構造其例項,將所有的Interceptor儲存到InterceptorChain中。
Java程式碼
- 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中寫死的,例如(其他三個類似):
Java程式碼
- 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執行過程等等