MyBatis最终对数据库操作的是内部框架定义的StatementHandler接口(可以参考本人文章http://blog.csdn.net/wq_tft/article/details/40134209),分页插件实现的原理就是对这个StatementHandler进行加工,利用的是java的动态代理机制,也就是说最终这个StatementHandler是个代理对象。
MyBatis的插件都必须实现org.apache.ibatis.plugin.Interceptor接口,该接口的定义如下:
package org.apache.ibatis.plugin;
import java.util.Properties;
public interface Interceptor {
// 实际执行的代理方法
Object intercept(Invocation invocation) throws Throwable;
// 获取代理对象,target是源对象,也就是StatementHandler
Object plugin(Object target);
// 设置属性,可以不用care
void setProperties(Properties properties);
}
那如何获得代理对象呢,也就是实现插件的接口中的plugin方法?MyBatis有个默认的静态方法可以获取:org.apache.ibatis.plugin.Plugin.wrap(Object target, Interceptor interceptor),源代码如下:
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
Plugin这个类实现了InvocationHandler接口,MyBatis实际上就是对StatementHandler的源实现对象进行代理,然后实际上的触发方法是Plugin的invoke(Object proxy, Method method, Object[] args)方法,在Plugin的构造函数中传入了源目标对象(也就是StatementHandler),interceptor(也就是我们实现的插件),signatureMap(根据插件类的注解,判断对StatementHandler的哪些方法进行拦截),这些信息作为Plugin的成员对象保存下来。
Plugin的invoke方法源代码如下:
/**
*proxy:被代理的对象,即StatementHandler
*method:被代理对象触发的方法
*args:被代理对象触发方法的传入参数
**/
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);
}
}
从源代码里面可以看出,MyBatis会先判断被代理对象的方法是否需要拦截,如果不需要拦截,直接触发源对象的方法即可,即
method.invoke(target, args)
否则,则触发插件的intercept方法,这个方法的参数传入了Invocation对象,在这个对象里面保存了被代理的对象,被代理对象的执行方法,以及被代理对象执行方法的传入参数。
然后在插件里面,我们可以根据Invocation这个对象里面的信息,获取原来要执行的对象(StatementHandler,实际上是RoutingStatementHandler,RoutingStatementHandler的delegate成员是实际操作数据库的StatementHandler)的信息,利用java的反射机制,我们可以随意的修改其中的任意信息(需要对源代码有一定理解),比如sql语句等等,进行分页语句的加工,然后执行Invocation.proceed()方法,触发源对象的方法,return源对象的返回值。原理就是是源对象中的信息已经被我们改变了,所以它实际查询出来的数据就是我们想要的分页的数据集,这就达到了插件的目的。