由浅入深分析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执行过程等等。

相关文章
相关标签/搜索