源码分析

插件机制

首先我们看下MyBatis拦截器的接口定义:

public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

  Object plugin(Object target);

  void setProperties(Properties properties);

}

比较简单,只有3个方法。 MyBatis默认没有一个拦截器接口的实现类,开发者们可以实现符合自己需求的拦截器。下面的MyBatis官网的一个拦截器实例:

@Intercepts({@Signature(type= Executor.class, method = "update", args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
public Object intercept(Invocation invocation) throws Throwable {
return invocation.proceed();
}
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
public void setProperties(Properties properties) {
}
}

全局xml配置:

<plugins>
<plugin interceptor="org.format.mybatis.cache.interceptor.ExamplePlugin"></plugin>
</plugins>

这个拦截器拦截Executor接口的update方法(其实也就是SqlSession的新增,删除,修改操作),所有执行executor的update方法都会被该拦截器拦截到。

首先从源头->配置文件开始分析:

XMLConfigBuilder解析MyBatis全局配置文件的pluginElement私有方法:

private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
interceptorInstance.setProperties(properties);
configuration.addInterceptor(interceptorInstance);
}
}
}

具体的解析代码其实比较简单,就不贴了,主要就是通过反射实例化plugin节点中的interceptor属性表示的类。然后调用全局配置类Configuration的addInterceptor方法。

public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}

这个interceptorChain是Configuration的内部属性,类型为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);
} }

现在我们理解了拦截器配置的解析以及拦截器的归属,现在我们回过头看下为何拦截器会拦截这些方法(Executor,ParameterHandler,ResultSetHandler,StatementHandler的部分方法):

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
} public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) {
ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
return resultSetHandler;
} public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
} public Executor newExecutor(Transaction transaction, ExecutorType executorType, boolean autoCommit) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor, autoCommit);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}

以上4个方法都是Configuration的方法。这些方法在MyBatis的一个操作(新增,删除,修改,查询)中都会被执行到,执行的先后顺序是Executor,ParameterHandler,ResultSetHandler,StatementHandler(其中ParameterHandler和ResultSetHandler的创建是在创建StatementHandler[3个可用的实现类CallableStatementHandler,PreparedStatementHandler,SimpleStatementHandler]的时候,其构造函数调用的[这3个实现类的构造函数其实都调用了父类BaseStatementHandler的构造函数])。

这4个方法实例化了对应的对象之后,都会调用interceptorChain的pluginAll方法,InterceptorChain的pluginAll刚才已经介绍过了,就是遍历所有的拦截器,然后调用各个拦截器的plugin方法。注意:拦截器的plugin方法的返回值会直接被赋值给原先的对象。

由于可以拦截StatementHandler,这个接口主要处理sql语法的构建,因此比如分页的功能,可以用拦截器实现,只需要在拦截器的plugin方法中处理StatementHandler接口实现类中的sql即可,可使用反射实现。

MyBatis还提供了@Intercepts和 @Signature关于拦截器的注解。官网的例子就是使用了这2个注解,还包括了Plugin类的使用:

@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}

代理链的生成

Mybatis支持对Executor、StatementHandler、ParameterHandler和ResultSetHandler进行拦截,也就是说会对这4种对象进行代理。通过查看Configuration类的源代码我们可以看到,每次都对目标对象进行代理链的生成。

下面以Executor为例。Mybatis在创建Executor对象时会执行下面一行代码:

executor =(Executor) interceptorChain.pluginAll(executor);

InterceptorChain里保存了所有的拦截器,它在mybatis初始化的时候创建。上面这句代码的含义是调用拦截器链里的每个拦截器依次对executor进行plugin(插入?)代码如下:

 /**
* 每一个拦截器对目标类都进行一次代理
* @param target
* @return 层层代理后的对象
*/
public Object pluginAll(Object target) {
for(Interceptor interceptor : interceptors) {
target= interceptor.plugin(target);
}
return target;
}

下面以一个简单的例子来看看这个plugin方法里到底发生了什么:

@Intercepts({@Signature(type = Executor.class, method ="update", args = {MappedStatement.class, Object.class})})
public class ExamplePlugin 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) {
}
}

每一个拦截器都必须实现上面的三个方法,其中:

  • Object intercept(Invocation invocation)是实现拦截逻辑的地方,内部要通过invocation.proceed()显式地推进责任链前进,也就是调用下一个拦截器拦截目标方法。
  • Object plugin(Object target) 就是用当前这个拦截器生成对目标target的代理,实际是通过Plugin.wrap(target,this)来完成的,把目标target和拦截器this传给了包装函数。
  • setProperties(Properties properties)用于设置额外的参数,参数配置在拦截器的Properties节点里。

注解里描述的是指定拦截方法的签名 [type,method,args] (即对哪种对象的哪种方法进行拦截),它在拦截前用于决断。

定义自己的Interceptor最重要的是要实现plugin方法和intercept方法,在plugin方法中我们可以决定是否要进行拦截进而决定要返回一个什么样的目标对象。而intercept方法就是要进行拦截的时候要执行的方法。

对于plugin方法而言,其实Mybatis已经为我们提供了一个实现。Mybatis中有一个叫做Plugin的类,里面有一个静态方法wrap(Object target,Interceptor interceptor),通过该方法可以决定要返回的对象是目标对象还是对应的代理。这里我们先来看一下Plugin的源码:

package org.apache.ibatis.plugin;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set; import org.apache.ibatis.reflection.ExceptionUtil; //这个类是Mybatis拦截器的核心,大家可以看到该类继承了InvocationHandler
//又是JDK动态代理机制
public class Plugin implements InvocationHandler { //目标对象
private Object target;
//拦截器
private Interceptor interceptor;
//记录需要被拦截的类与方法
private Map<Class<?>, Set<Method>> signatureMap; private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
this.target = target;
this.interceptor = interceptor;
this.signatureMap = signatureMap;
} //一个静态方法,对一个目标对象进行包装,生成代理类。
public static Object wrap(Object target, Interceptor interceptor) {
//首先根据interceptor上面定义的注解 获取需要拦截的信息
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
//目标对象的Class
Class<?> type = target.getClass();
//返回需要拦截的接口信息
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
//如果长度为>0 则返回代理类 否则不做处理
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
} //代理对象每次调用的方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//通过method参数定义的类 去signatureMap当中查询需要拦截的方法集合
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);
}
} //根据拦截器接口(Interceptor)实现类上面的注解获取相关信息
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
//获取注解信息
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
//为空则抛出异常
if (interceptsAnnotation == null) { // issue #251
throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
}
//获得Signature注解信息
Signature[] sigs = interceptsAnnotation.value();
Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
//循环注解信息
for (Signature sig : sigs) {
//根据Signature注解定义的type信息去signatureMap当中查询需要拦截方法的集合
Set<Method> methods = signatureMap.get(sig.type());
//第一次肯定为null 就创建一个并放入signatureMap
if (methods == null) {
methods = new HashSet<Method>();
signatureMap.put(sig.type(), methods);
}
try {
//找到sig.type当中定义的方法 并加入到集合
Method method = sig.type().getMethod(sig.method(), sig.args());
methods.add(method);
} catch (NoSuchMethodException e) {
throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
}
}
return signatureMap;
} //根据对象类型与signatureMap获取接口信息
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
Set<Class<?>> interfaces = new HashSet<Class<?>>();
//循环type类型的接口信息 如果该类型存在与signatureMap当中则加入到set当中去
while (type != null) {
for (Class<?> c : type.getInterfaces()) {
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
type = type.getSuperclass();
}
//转换为数组返回
return interfaces.toArray(new Class<?>[interfaces.size()]);
} }

下面是俩个注解类的定义源码:

package org.apache.ibatis.plugin;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
Signature[] value();
}
package org.apache.ibatis.plugin; import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Signature {
Class<?> type(); String method(); Class<?>[] args();
}

Plugin.wrap方法

从前面可以看出,每个拦截器的plugin方法是通过调用Plugin.wrap方法来实现的。代码如下:

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) {
// 生成代理对象, Plugin对象为该代理对象的InvocationHandler (InvocationHandler属于java代理的一个重要概念,不熟悉的请参考相关概念)
return Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target,interceptor,signatureMap));
}
return target;
}

这个Plugin类有三个属性:

private Object target;// 被代理的目标类

private Interceptor interceptor;// 对应的拦截器

private Map<Class<?>, Set<Method>> signatureMap;// 拦截器拦截的方法缓存

getSignatureMap方法

private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
if (interceptsAnnotation == null) { // issue #251
throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
}
Signature[] sigs = interceptsAnnotation.value();
Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
for (Signature sig : sigs) {
Set<Method> methods = signatureMap.get(sig.type());
if (methods == null) {
methods = new HashSet<Method>();
signatureMap.put(sig.type(), methods);
}
try {
Method method = sig.type().getMethod(sig.method(), sig.args());
methods.add(method);
} catch (NoSuchMethodException e) {
throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
}
}
return signatureMap;
}

getSignatureMap方法解释:首先会拿到拦截器这个类的 @Interceptors注解,然后拿到这个注解的属性 @Signature注解集合,然后遍历这个集合,遍历的时候拿出 @Signature注解的type属性(Class类型),然后根据这个type得到带有method属性和args属性的Method。由于 @Interceptors注解的 @Signature属性是一个属性,所以最终会返回一个以type为key,value为Set<Method>的Map。

@Intercepts({@Signature(type= Executor.class, method = "update", args = {MappedStatement.class,Object.class})})

比如这个 @Interceptors注解会返回一个key为Executor,value为集合(这个集合只有一个元素,也就是Method实例,这个Method实例就是Executor接口的update方法,且这个方法带有MappedStatement和Object类型的参数)。这个Method实例是根据 @Signature的method和args属性得到的。如果args参数跟type类型的method方法对应不上,那么将会抛出异常。

getAllInterfaces方法

private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
Set<Class<?>> interfaces = new HashSet<Class<?>>();
while (type != null) {
for (Class<?> c : type.getInterfaces()) {
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
type = type.getSuperclass();
}
return interfaces.toArray(new Class<?>[interfaces.size()]);
}

getAllInterfaces方法解释: 根据目标实例target(这个target就是之前所说的MyBatis拦截器可以拦截的类,Executor,ParameterHandler,ResultSetHandler,StatementHandler)和它的父类们,返回signatureMap中含有target实现的接口数组。

所以Plugin这个类的作用就是根据 @Interceptors注解,得到这个注解的属性 @Signature数组,然后根据每个 @Signature注解的type,method,args属性使用反射找到对应的Method。最终根据调用的target对象实现的接口决定是否返回一个代理对象替代原先的target对象。

我们再次结合(Executor)interceptorChain.pluginAll(executor)这个语句来看,这个语句内部对executor执行了多次plugin,第一次plugin后通过Plugin.wrap方法生成了第一个代理类,姑且就叫executorProxy1,这个代理类的target属性是该executor对象。第二次plugin后通过Plugin.wrap方法生成了第二个代理类,姑且叫executorProxy2,这个代理类的target属性是executorProxy1...这样通过每个代理类的target属性就构成了一个代理链(从最后一个executorProxyN往前查找,通过target属性可以找到最原始的executor类)。

代理链上的拦截

代理链生成后,对原始目标的方法调用都转移到代理者的invoke方法上来了。Plugin作为InvocationHandler的实现类,他的invoke方法是怎么样的呢?

比如MyBatis官网的例子,当Configuration调用newExecutor方法的时候,由于Executor接口的update(MappedStatement ms, Object parameter)方法被拦截器被截获。因此最终返回的是一个代理类Plugin,而不是Executor。这样调用方法的时候,如果是个代理类,那么会执行:

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)) {
// 调用代理类所属拦截器的intercept方法,
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch(Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}

没错,如果找到对应的方法被代理之后,那么会执行Interceptor接口的interceptor方法。

在invoke里,如果方法签名和拦截中的签名一致,就调用拦截器的拦截方法。我们看到传递给拦截器的是一个Invocation对象,这个对象是什么样子的,他的功能又是什么呢?

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;
}
... public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
}
}

可以看到,Invocation类保存了代理对象的目标类,执行的目标类方法以及传递给它的参数。

在每个拦截器的intercept方法内,最后一个语句一定是return invocation.proceed()(不这么做的话拦截器链就断了,你的mybatis基本上就不能正常工作了)。invocation.proceed()只是简单的调用了下target的对应方法,如果target还是个代理,就又回到了上面的Plugin.invoke方法了。这样就形成了拦截器的调用链推进。

public Object intercept(Invocation invocation) throws Throwable {
//完成代理类本身的逻辑
...
//通过invocation.proceed()方法完成调用链的推进
return invocation.proceed();
}

小结

MyBatis拦截器接口提供的3个方法中,plugin方法用于某些处理器(Handler)的构建过程。interceptor方法用于处理代理类的执行。setProperties方法用于拦截器属性的设置。

其实MyBatis官网提供的使用 @Interceptors和 @Signature注解以及Plugin类这样处理拦截器的方法,我们不一定要直接这样使用。我们也可以抛弃这3个类,直接在plugin方法内部根据target实例的类型做相应的操作。

总体来说MyBatis拦截器还是很简单的,拦截器本身不需要太多的知识点,但是学习拦截器需要对MyBatis中的各个接口很熟悉,因为拦截器涉及到了各个接口的知识点。

我们假设在MyBatis配置了一个插件,在运行时会发生什么?

  • 所有可能被拦截的处理类都会生成一个代理
  • 处理类代理在执行对应方法时,判断要不要执行插件中的拦截方法
  • 执行插接中的拦截方法后,推进目标的执行
  • 如果有N个插件,就有N个代理,每个代理都要执行上面的逻辑。这里面的层层代理要多次生成动态代理,是比较影响性能的。虽然能指定插件拦截的位置,但这个是在执行方法时动态判断,初始化的时候就是简单的把插件包装到了所有可以拦截的地方。

因此,在编写插件时需注意以下几个原则

  • 不编写不必要的插件;
  • 实现plugin方法时判断一下目标类型,是本插件要拦截的对象才执行Plugin.wrap方法,否者直接返回目标本省,这样可以减少目标被代理的次数。

分页机制

为什么在StatementHandler拦截

在前面章节介绍了一次sqlsession的完整执行过程,从中可以知道sql的解析是在StatementHandler里完成的,所以为了重写sql需要拦截StatementHandler。

MetaObject简介

在实现里大量使用了MetaObject这个对象,因此有必要先介绍下它。MetaObject是Mybatis提供的一个的工具类,通过它包装一个对象后可以获取或设置该对象的原本不可访问的属性(比如那些私有属性)。它有个三个重要方法经常用到:

  • MetaObject forObject(Object object,ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory) 用于包装对象;
  • Object getValue(String name) 用于获取属性的值(支持OGNL的方法);
  • void setValue(String name, Object value) 用于设置属性的值(支持OGNL的方法);

拦截器签名

@Intercepts({@Signature(type =StatementHandler.class, method = "prepare", args ={Connection.class})})
public class PageInterceptor implements Interceptor {
...
}

从签名里可以看出,要拦截的目标类型是StatementHandler(注意:type只能配置成接口类型),拦截的方法是名称为prepare参数为Connection类型的方法。

intercept实现

public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaStatementHandler = MetaObject.forObject(statementHandler,
DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY);
// 分离代理对象链(由于目标类可能被多个拦截器拦截,从而形成多次代理,通过下面的两次循环
// 可以分离出最原始的的目标类)
while (metaStatementHandler.hasGetter("h")) {
Object object = metaStatementHandler.getValue("h");
metaStatementHandler = MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY,
DEFAULT_OBJECT_WRAPPER_FACTORY);
}
// 分离最后一个代理对象的目标类
while (metaStatementHandler.hasGetter("target")) {
Object object = metaStatementHandler.getValue("target");
metaStatementHandler = MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY,
DEFAULT_OBJECT_WRAPPER_FACTORY);
}
Configuration configuration = (Configuration) metaStatementHandler.
getValue("delegate.configuration");
dialect = configuration.getVariables().getProperty("dialect");
if (null == dialect || "".equals(dialect)) {
logger.warn("Property dialect is not setted,use default 'mysql' ");
dialect = defaultDialect;
}
pageSqlId = configuration.getVariables().getProperty("pageSqlId");
if (null == pageSqlId || "".equals(pageSqlId)) {
logger.warn("Property pageSqlId is not setted,use default '.*Page$' ");
pageSqlId = defaultPageSqlId;
}
MappedStatement mappedStatement = (MappedStatement)
metaStatementHandler.getValue("delegate.mappedStatement");
// 只重写需要分页的sql语句。通过MappedStatement的ID匹配,默认重写以Page结尾的
// MappedStatement的sql
if (mappedStatement.getId().matches(pageSqlId)) {
BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");
Object parameterObject = boundSql.getParameterObject();
if (parameterObject == null) {
throw new NullPointerException("parameterObject is null!");
} else {
// 分页参数作为参数对象parameterObject的一个属性
PageParameter page = (PageParameter) metaStatementHandler
.getValue("delegate.boundSql.parameterObject.page");
String sql = boundSql.getSql();
// 重写sql
String pageSql = buildPageSql(sql, page);
metaStatementHandler.setValue("delegate.boundSql.sql", pageSql);
// 采用物理分页后,就不需要mybatis的内存分页了,所以重置下面的两个参数
metaStatementHandler.setValue("delegate.rowBounds.offset",
RowBounds.NO_ROW_OFFSET);
metaStatementHandler.setValue("delegate.rowBounds.limit", RowBounds.NO_ROW_LIMIT);
Connection connection = (Connection) invocation.getArgs()[0];
// 重设分页参数里的总页数等
setPageParameter(sql, connection, mappedStatement, boundSql, page);
}
}
// 将执行权交给下一个拦截器
return invocation.proceed();
}

StatementHandler的默认实现类是RoutingStatementHandler,因此拦截的实际对象是它。RoutingStatementHandler的主要功能是分发,它根据配置Statement类型创建真正执行数据库操作的StatementHandler,并将其保存到delegate属性里。由于delegate是一个私有属性并且没有提供访问它的方法,因此需要借助MetaObject的帮忙。通过MetaObject的封装后我们可以轻易的获得想要的属性。

在上面的方法里有个两个循环,通过他们可以分离出原始的RoutingStatementHandler(而不是代理对象)。

前面提到,签名里配置的要拦截的目标类型是StatementHandler拦截的方法是名称为prepare参数为Connection类型的方法,而这个方法是每次数据库访问都要执行的。因为我是通过重写sql的方式实现分页,为了不影响其他sql(update或不需要分页的query),我采用了通过ID匹配的方式过滤。默认的过滤方式只对id以Page结尾的进行拦截(注意区分大小写),如下:

<select id="queryUserByPage" parameterType="UserDto" resultType="UserDto">
<![CDATA[
select * from t_user t where t.username = #{username}
]]>
</select>

当然,也可以自定义拦截模式,在mybatis的配置文件里加入以下配置项:

<properties>
<property name="dialect" value="mysql" />
<property name="pageSqlId" value=".*Page$" />
</properties>

其中,属性dialect指示数据库类型,目前只支持mysql和oracle两种数据库。其中,属性pageSqlId指示拦截的规则,以正则方式匹配。

sql重写

sql重写其实在原始的sql语句上加入分页的参数,目前支持mysql和oracle两种数据库的分页。

private String buildPageSql(String sql, PageParameter page) {
if (page != null) {
StringBuilder pageSql = new StringBuilder();
if ("mysql".equals(dialect)) {
pageSql = buildPageSqlForMysql(sql, page);
} else if ("oracle".equals(dialect)) {
pageSql = buildPageSqlForOracle(sql, page);
} else {
return sql;
}
return pageSql.toString();
} else {
return sql;
}
}

mysql的分页实现

public StringBuilder buildPageSqlForMysql(String sql, PageParameter page) {
StringBuilder pageSql = new StringBuilder(100);
String beginrow = String.valueOf((page.getCurrentPage() - 1) * page.getPageSize());
pageSql.append(sql);
pageSql.append(" limit " + beginrow + "," + page.getPageSize());
return pageSql;
}

oracle的分页实现

public StringBuilder buildPageSqlForOracle(String sql, PageParameter page) {
StringBuilder pageSql = new StringBuilder(100);
String beginrow = String.valueOf((page.getCurrentPage() - 1) * page.getPageSize());
String endrow = String.valueOf(page.getCurrentPage() * page.getPageSize());
pageSql.append("select * from ( select temp.*, rownum row_id from ( ");
pageSql.append(sql);
pageSql.append(" ) temp where rownum <= ").append(endrow);
pageSql.append(") where row_id > ").append(beginrow);
return pageSql;
}

分页参数重写

有时候会有这种需求,就是不但要查出指定页的结果,还需要知道总的记录数和页数。我通过重写分页参数的方式提供了一种解决方案:

/**
* 从数据库里查询总的记录数并计算总页数,回写进分页参数<code>PageParameter</code>,这样调用
* 者就可用通过 分页参数<code>PageParameter</code>获得相关信息。
*
* @param sql
* @param connection
* @param mappedStatement
* @param boundSql
* @param page
*/
private void setPageParameter(String sql, Connection connection, MappedStatement mappedStatement,
BoundSql boundSql, PageParameter page) {
// 记录总记录数
String countSql = "select count(0) from (" + sql + ") as total";
PreparedStatement countStmt = null;
ResultSet rs = null;
try {
countStmt = connection.prepareStatement(countSql);
BoundSql countBS = new BoundSql(mappedStatement.getConfiguration(), countSql,
boundSql.getParameterMappings(), boundSql.getParameterObject());
setParameters(countStmt, mappedStatement, countBS, boundSql.getParameterObject());
rs = countStmt.executeQuery();
int totalCount = 0;
if (rs.next()) {
totalCount = rs.getInt(1);
}
page.setTotalCount(totalCount);
int totalPage = totalCount / page.getPageSize() + ((totalCount % page.getPageSize() == 0) ? 0 : 1);
page.setTotalPage(totalPage);
} catch (SQLException e) {
logger.error("Ignore this exception", e);
} finally {
try {
rs.close();
} catch (SQLException e) {
logger.error("Ignore this exception", e);
}
try {
countStmt.close();
} catch (SQLException e) {
logger.error("Ignore this exception", e);
}
}
} /**
* 对SQL参数(?)设值
*
* @param ps
* @param mappedStatement
* @param boundSql
* @param parameterObject
* @throws SQLException
*/
private void setParameters(PreparedStatement ps, MappedStatement mappedStatement, BoundSql boundSql,
Object parameterObject) throws SQLException {
ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, parameterObject, boundSql);
parameterHandler.setParameters(ps);
}

plugin实现

public Object plugin(Object target) {
// 当目标类是StatementHandler类型时,才包装目标类,否者直接返回目标本身,减少目标被代理的
// 次数
if (target instanceof StatementHandler) {
return Plugin.wrap(target, this);
} else {
return target;
}
}

《深入理解Mybatis原理》Mybatis插件机制&分页机制原理的更多相关文章

  1. springboot结合mybatis使用pageHelper插件进行分页查询

    1.pom相关依赖引入 <dependencies> <dependency> <groupId>org.springframework.boot</grou ...

  2. SpringBoot整合mybatis使用pageHelper插件进行分页操作

    SpringBoot整合mybatis分页操作 SpringBoot整合Mybatis进行分页操作,这里需要使用Mybatis的分页插件:pageHelper, 关于pageHelper的介绍,请查看 ...

  3. 10—mybatis 通用mapper插件 pagehelper 分页

    spring boot真的太好用了,大家以后多多使用,今天来说说pagehelper 来做mybatis分页,我用的是spring boot 做的开发,后面会把源码发出来. pagehelper(ht ...

  4. layui结合mybatis的pagehelper插件的分页通用的方法

    总体思路: 1.前台查询的时候将当前页和页大小传到后台 2.后台将当前页,页大小以及数据与数据总数返回前台,前台显示完表格完数据之后显示分页插件. 前台页面: 准备查询条件的表单,与数据表格,分页di ...

  5. mybatis的拦截器及分页机制

    https://blog.csdn.net/ssuperlg/article/details/79847889

  6. Linux的分段和分页机制

    1.分段机制 80386的两种工作模式  80386的工作模式包括实地址模式和虚地址模式(保护模式).Linux主要工作在保护模式下. 分段机制  在保护模式下,80386虚地址空间可达16K个段,每 ...

  7. MyBatis(八):MyBatis插件机制详解

    MyBatis插件插件机制简介 ​ MyBatis插件其实就是为使用者提供的自行拓展拦截器,主要是为了可以更好的满足业务需要. ​ 在MyBatis中提供了四大核心组件对数据库进行处理,分别是Exec ...

  8. Mybatis学习---Mybatis分页插件 - PageHelper

    1. Mybatis分页插件 - PageHelper说明 如果你也在用Mybatis,建议尝试该分页插件,这个一定是最方便使用的分页插件. 该插件目前支持Oracle,Mysql,MariaDB,S ...

  9. 互联网轻量级框架SSM-查缺补漏第八天(MyBatis插件plugin使用及原理)

    简言:今天进行第八天的记录(只是写了八天).有的时候看的多,有的时候看的少,看的少的时候就攒几天一起写了.而今天这个插件我昨天写了一下午,下班没写完就回去了,今天把尾收了,再加上一个过程图方便下面原理 ...

  10. Mybatis拦截器介绍及分页插件

    1.1    目录 1.1 目录 1.2 前言 1.3 Interceptor接口 1.4 注册拦截器 1.5 Mybatis可拦截的方法 1.6 利用拦截器进行分页 1.2     前言 拦截器的一 ...

随机推荐

  1. SQL Prompt 联网限制

    SQLPrompt关闭联网_sqlprompt阻止联网_幻欢子的博客-CSDN博客 方法一:修改hosts文件 1.安装SQLPrompt_xxx.exe成功后,编辑修改hosts文件:C:\Wind ...

  2. C# 注释 各个关键字段 使用说明

    https://www.cnblogs.com/xdot/p/6632313.html#:~:text=%E5%9C%A8C%23%E6%99%BA%E8%83%BD%E6%B3%A8%E9%87%8 ...

  3. C# 用Linq或Lambda查询DataGridView行中的数据是否包含(各种操作)

    http://blog.csdn.net/xht555/article/details/38685845 https://www.cnblogs.com/wuchao/archive/2012/12/ ...

  4. PLC通过lora网关采集温室大棚温湿度数据

    概述: 运用lora网关远程控制大棚内风机,日光灯,温湿度传感器等设备.可以实现远程获取现场环境的空气温湿度.土壤水分温度.二氧化碳浓度.光照强度可以自动控制温室湿帘风机.喷淋滴灌.加温补光等设备,并 ...

  5. 速通提示词工程Prompt Engineering

    提示词工程简介 关注提示词开发和优化,帮助用户将大语言模型用于各场景和研究领域. 利用提示工程来提升大语言模型处理复杂任务场景的能力,如问答和算术推理能力. 通过提示工程设计.研发强大的工程技术,实现 ...

  6. 入门书籍 web前端开发最佳实践

    高效web前端开发  book PageSpeed  出自google公司  是一款  免费的工具  性能分析 Timeline网页交互  和渲染  所要花费的时间 Profile谁  可以查看网页c ...

  7. C/C++语言字符串正则匹配问题

    30分钟入门博客 http://www.awaysoft.com/regular.htm#mission 可能服务器到期了,说实话自己的博客还是部署到CSDN,博客园之类的好. 测试网页 https: ...

  8. SciTech-EECS-Circuits-PCAU(Phase Controlled Array Ultrasound,相控 麦克风/超声波头/雷达/传感器 阵列):精准调制"声场": 实现超声波"悬浮(反重力)" 原理 精准调控 上下两个 或 立体阵列 的 超声波头 组成 "空间超声波场" 使场压抵消物体重力

    SciTech-EECS-Circuits-Ultrasound: 超声波"悬浮(反重力)" 原理 PCAU(Phase Controlled Array Ultrasound,相 ...

  9. SciTech-Wireless-WiFi-WDS(无线路由桥接扩网)两个路由器无线桥接(WDS)要点

    两个路由器无线桥接(WDS)要点 当上网点距离路由器有好几道墙,连接WiFi信号很差时: 可用另外一个路由器无线桥接(大多数路由器的WDS功能)到主路由器(连光猫连接的路由器), 扩大主WiFi路由器 ...

  10. SciTech-Mathmatics-Mathmatical Analysis-Series: 解数列通项的通用模型

    解数列通项的通用模型