Mybatis: 插件及分页
Mybatis采用责任链模式,通过动态代理组织多个拦截器(插件),通过这些拦截器可以改变Mybatis的默认行为(诸如SQL重写之类的)。
Mybatis支持对Executor、StatementHandler、ParameterHandler和ResultSetHandler进行拦截。
插件的运行时的逻辑:
- 所有可能被拦截的处理类都会生成一个代理。
- 代理类在执行对应方法时,判断要不要执行插件中的拦截方法。
- 执行插件中的拦截方法后,推进目标的执行。
Executor
前面博客介绍过Executor
是在openSession()
的过程中被创建的。在调用Configuration
的newExecutor()
方法创建Executor
时会进行以下操作。
1 | executor = (Executor) interceptorChain.pluginAll(executor); |
每一个拦截器对目标类都进行一次代理,层层进行。
InterceptorChainexecutor执行了多次plugin,第一次plugin后通过Plugin.wrap方法生成了第一个代理类,姑且就叫executorProxy1,这个代理类的target属性是该executor对象。第二次plugin后通过Plugin.wrap方法生成了第二个代理类,姑且叫executorProxy2,这个代理类的target属性是executorProxy1…这样通过每个代理类的target属性就构成了一个代理链。
123456 | public Object (Object target) { for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target;} |
Interceptor
拦截器Interceptor
接口定义了三个方法:
intercept()
: 内部要通过invocation.proceed()
显式地推进责任链前进,也就是调用下一个拦截器拦截目标方法。plugin()
: 用当前这个拦截器生成对目标target的代理,实际是通过Plugin.wrap(target,this)
来完成的,把目标target和拦截器this传给了包装函数。setProperties()
: 用于设置额外的参数,参数配置在拦截器的Properties节点里。
12345678910111213141516171819202122232425262728293031 | package plugin; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.plugin.*; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; import java.util.Properties;({@Signature( type= Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})}) public class MyPlugin implements Interceptor{ @Override public Object intercept(Invocation invocation) throws Throwable { return invocation.proceed(); } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { }} |
Plugin
生成拦截器代理对象是在Plugin.wrap()
中完成的,Plugin
本身实现了InvocationHandler
(JDK代理实现)。
target
: 被代理的目标类。Interceptor
: 对应的拦截器。signatureMap
: 拦截器拦截的方法缓存。
在Plugin
代理对象的invoke()
完成对目标类的方法调用。如果方法签名和拦截中的签名一致,就调用拦截器的拦截方法intercept()
,传递的是一个Invocation
对象。
123456789101112131415161718192021222324252627282930 | public class Plugin implements InvocationHandler { ... 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大专栏 Mybatis: 插件及分页pan> Proxy.newProxyInstance( type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); } return target; } @Override 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); } } ...} |
Invocation
Invocation
对象保存了代理对象的目标类,执行的目标类方法以及传递给它的参数,真正执行目标类的方法调用是在Invocation
中的proceed()
方法。
所以代理对象的invoke()
中调用拦截器的intercept(Invocation invocation)
方法后,在该方法还必须调用invocation.proceed()
方法才能使代理链继续执行下去。
12345678910111213141516 | package org.apache.ibatis.plugin; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class Invocation { private Object target; private Method method; private Object[] args; ... public Object proceed() throws InvocationTargetException, IllegalAccessException { return method.invoke(target, args); }} |
详情参见:Mybatis 插件原理
分页
Mybatis的分页功能是基于内存的分页,也就是查出所有记录再按照偏移量offset和limit取出结果。在大数据量的情况下不适用,下面两个插件都是通过拦截Executor
等重写sql语句实现数据库的物理分页。
逻辑分页(内存分页)
Mybatis 自身通过 RowBounds
来完成内存分页的。
1234 | List<Actor> actorsInMem = mapper.selectAll(new RowBounds(10, 5)); for (Actor actor: actorsInMem){ System.out.println(actor);} |
1 | List<Actor> selectAll(RowBounds rowBounds); |
12345 | <select id="selectAll" resultMap="BaseResultMap"> select <include refid="Base_Column_List"/> from actor</select> |
12345 | Actor{actorId=11, firstName='ZERO', lastName='CAGE', lastUpdate=Wed Feb 15 04:34:33 CST 2006}Actor{actorId=12, firstName='KARL', lastName='BERRY', lastUpdate=Wed Feb 15 04:34:33 CST 2006}Actor{actorId=13, firstName='UMA', lastName='WOOD', lastUpdate=Wed Feb 15 04:34:33 CST 2006}Actor{actorId=14, firstName='VIVIEN', lastName='BERGEN', lastUpdate=Wed Feb 15 04:34:33 CST 2006}Actor{actorId=15, firstName='CUBA', lastName='OLIVIER', lastUpdate=Wed Feb 15 04:34:33 CST 2006} |
物理分页
物理分页就是在SQL查询过程中实现分页,不同的数据库厂商,实现也会不同。MySql通过在sql语句中添加offset和limit实现。
分页插件实现,通过添加拦截器,对Executor
进行拦截,然后重写sql:
详情参见:Mybatis 分页