由淺入深分析mybatis經過動態代理實現攔截器(插件)的原理

最近在用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執行過程等等。

相關文章
相關標籤/搜索