《深入理解Mybatis原理》Mybatis插件机制分页机制原理

news/2025/1/8 22:29:02/文章来源:https://www.cnblogs.com/seven97-top/p/18651682

源码分析

插件机制

首先我们看下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);//目标对象的ClassClass<?> 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 #251throw 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 就创建一个并放入signatureMapif (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 #251throw 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;  }  
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/866115.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

浅述实模式下的中断

# 写在前面 本文重在讨论8086处理器的中断的原理与分类,以及一些中断向量表的内容。由于笔者水平有限,随笔中难免有些许纰漏和错误,希望广大读者能指正。 # 中断的分类 我们先来看看中断的分类。大体上来说,中断可以分为*外部硬件中断、内部中断、软中断。* 中断就是*打…

Vue 全局前置路由守卫 全局后置路由守卫

一、位置 src/router/index.jsconst router = new VueRouter({routes:[] })// 这个地方写全局前置路由守卫或 全局后置路由守卫export default router一、全局前置路由守卫 1、时间:初始化时被调用,每次切换前被调用 2、应用场景:控制登录用户的权限 3、语法router.beforeEa…

《docker基础篇:3.Docker常用命令》包括帮助启动类命令、镜像命令、有镜像才能创建容器,这是根本前提(下载一个CentOS或者ubuntu镜像演示)、容器命令、小总结

《docker基础篇:3.Docker常用命令》包括帮助启动类命令、镜像命令、有镜像才能创建容器,这是根本前提(下载一个CentOS或者ubuntu镜像演示)、容器命令、小总结@目录3.Docker常用命令3.1帮助启动类命令3.2镜像命令3.3有镜像才能创建容器,这是根本前提(下载一个CentOS或者ubunt…

基于SA模拟退火优化算法的TSP问题求解matlab仿真,并对比ACO蚁群优化算法

1.程序功能描述 基于SA模拟退火优化算法的TSP问题求解matlab仿真,并对比ACO蚁群优化算法,对比两个算法的仿真时间,收敛曲线,以及路径规划的结果,最短路径长度。 2.测试软件版本以及运行结果展示MATLAB2022A版本运行 3.核心程序while t>=Temp1%温度降温判决tfor j=…

基于双PI结构FOC闭环控制的永磁同步电机控制系统simulink建模与仿真

1.课题概述 基于双PI结构FOC闭环控制的永磁同步电机控制系统simulink建模与仿真。2.系统仿真结果 3.核心程序与模型 版本:MATLAB2022a 4.系统原理简介永磁同步电机(PMSM)基于双PI结构的磁场定向控制(Field-Oriented Control, FOC)闭环控制系统是一种高级电机控制策略,旨…

基于FPGA的直接数字频率合成器verilog实现,包含testbench

1.算法运行效果图预览 (完整程序运行后无水印) 2.算法运行软件版本 vivado2019.23.部分核心程序 (完整版代码包含详细中文注释和操作步骤视频)//sin,cos相位累加器的控制 always @(posedge i_clk)//时钟上边沿触发 beginif(i_rst)//系统复位begino_sin_add<=8d0;o_cos_ad…

G1原理—1.G1回收器的分区机制

大纲 1.G1垃圾回收器的分区(Region大小+G1分区+Region过大过小和计算) 2.Region大小的计算原理(先转字节然后确定2的n次幂再通过1左移n位) 3.新生代分区及自动扩展(新生代动态扩展机制) 4.停顿预测模型(衰减算法)保证预期停顿时间1.G1垃圾回收器的分区(Region大小+G1分区+Regio…

你不知道的 AI 提示词,都在这里!

你不知道的 AI 提示词,都在这里! ​​ 使用场景AI 提示词,即英文 AI Prompt,它犹如人类与大模型之间的“沟通语言”,堪称人机交互的重要通道。其形式既可以是简短的指令,也可以是一个问题,甚至是一个词,其目的就在于清晰地告知 AI 期望得到的结果。从原理上来看,提示词…

学习 - Linux - CentOS安装Tomcat8.5.85

CentOS安装Tomcat8.5.85 1、保证已经安装了jdk运行环境 java -version如果没有,请参考Centos安装jdk 2、从 Apache 官方网站下载 Tomcat 8 cd /opt sudo wget https://archive.apache.org/dist/tomcat/tomcat-8/v8.5.85/bin/apache-tomcat-8.5.85.tar.gz 3、载完成后,解压缩 …

Win32汇编学习笔记07.筛选器异常

Win32汇编学习笔记07.筛选器异常-C/C++基础-断点社区-专业的老牌游戏安全技术交流社区 - BpSend.net 钢琴 od调试老师给的多媒体钢琴运行找到Piano的过程函数里去找到处理WM_KEYDOWN消息的那下个断点,然后按键断下来在这分析上图汇编代码:mov eax,dword ptr [ebp+10] 拿wParm…

Axure RP 10 for Mac v10.0.0.3924原型设计软件 中文

Axure RP 10 for Mac v10.0.0.3924原型设计软件 中文 Axure RP 10 for Mac,是一个专业的原型设计和线框图工具,广泛应用于用户体验设计(UX)和用户界面设计(UI)领域,为设计师、产品经理、开发人员提供了强大的功能,帮助其在项目早期阶段快速创建互动原型和详细的设计文档…

EndNote 21 for Mac v21.5 文献管理软件安装

EndNote 21 for Mac v21.5 文献管理软件安装 EndNote for mac,是一款文献管理软件,支持国际期刊的参考文献格式有3776种,写作模板几百种,涵盖各个领域的杂志。EndNote mac不仅仅局限于投稿论文的写作,对于研究生毕业论文的写作也会起到很好的助手作用。