`
234390216
  • 浏览: 10232310 次
  • 性别: Icon_minigender_1
  • 来自: 深圳
博客专栏
A5ee55b9-a463-3d09-9c78-0c0cf33198cd
Oracle基础
浏览量:462597
Ad26f909-6440-35a9-b4e9-9aea825bd38e
springMVC介绍
浏览量:1775469
Ce363057-ae4d-3ee1-bb46-e7b51a722a4b
Mybatis简介
浏览量:1398309
Bdeb91ad-cf8a-3fe9-942a-3710073b4000
Spring整合JMS
浏览量:394996
5cbbde67-7cd5-313c-95c2-4185389601e7
Ehcache简介
浏览量:679961
Cc1c0708-ccc2-3d20-ba47-d40e04440682
Cas简介
浏览量:530847
51592fc3-854c-34f4-9eff-cb82d993ab3a
Spring Securi...
浏览量:1183873
23e1c30e-ef8c-3702-aa3c-e83277ffca91
Spring基础知识
浏览量:467807
4af1c81c-eb9d-365f-b759-07685a32156e
Spring Aop介绍
浏览量:151371
2f926891-9e7a-3ce2-a074-3acb2aaf2584
JAXB简介
浏览量:68129
社区版块
存档分类
最新评论

Mybatis拦截器介绍及分页插件

阅读更多

Mybatis拦截器介绍及分页插件

1.1    目录

1.1 目录

1.2 前言

1.3 Interceptor接口

1.4 注册拦截器

1.5 Mybatis可拦截的方法

1.6 利用拦截器进行分页

1.2     前言

       拦截器的一个作用就是我们可以拦截某些方法的调用,我们可以选择在这些被拦截的方法执行前后加上某些逻辑,也可以在执行这些被拦截的方法时执行自己的逻辑而不再执行被拦截的方法。Mybatis拦截器设计的一个初衷就是为了供用户在某些时候可以实现自己的逻辑而不必去动Mybatis固有的逻辑。打个比方,对于Executor,Mybatis中有几种实现:BatchExecutor、ReuseExecutor、SimpleExecutor和CachingExecutor。这个时候如果你觉得这几种实现对于Executor接口的query方法都不能满足你的要求,那怎么办呢?是要去改源码吗?当然不。我们可以建立一个Mybatis拦截器用于拦截Executor接口的query方法,在拦截之后实现自己的query方法逻辑,之后可以选择是否继续执行原来的query方法。

1.3     Interceptor接口

       对于拦截器Mybatis为我们提供了一个Interceptor接口,通过实现该接口就可以定义我们自己的拦截器。我们先来看一下这个接口的定义:

package org.apache.ibatis.plugin;
 
import java.util.Properties;
 
public interface Interceptor {
 
  Object intercept(Invocation invocation) throws Throwable;
 
  Object plugin(Object target);
 
  void setProperties(Properties properties);
 
}

 

       我们可以看到在该接口中一共定义有三个方法,intercept、plugin和setProperties。plugin方法是拦截器用于封装目标对象的,通过该方法我们可以返回目标对象本身,也可以返回一个它的代理。当返回的是代理的时候我们可以对其中的方法进行拦截来调用intercept方法,当然也可以调用其他方法,这点将在后文讲解。setProperties方法是用于在Mybatis配置文件中指定一些属性的。

       定义自己的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;
 
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) {
    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;
  }
 
  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);
    }
  }
 
  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;
  }
 
  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()]);
  }
 
}

 

       我们先看一下Plugin的wrap方法,它根据当前的Interceptor上面的注解定义哪些接口需要拦截,然后判断当前目标对象是否有实现对应需要拦截的接口,如果没有则返回目标对象本身,如果有则返回一个代理对象。而这个代理对象的InvocationHandler正是一个Plugin。所以当目标对象在执行接口方法时,如果是通过代理对象执行的,则会调用对应InvocationHandler的invoke方法,也就是Plugin的invoke方法。所以接着我们来看一下该invoke方法的内容。这里invoke方法的逻辑是:如果当前执行的方法是定义好的需要拦截的方法,则把目标对象、要执行的方法以及方法参数封装成一个Invocation对象,再把封装好的Invocation作为参数传递给当前拦截器的intercept方法。如果不需要拦截,则直接调用当前的方法。Invocation中定义了定义了一个proceed方法,其逻辑就是调用当前方法,所以如果在intercept中需要继续调用当前方法的话可以调用invocation的procced方法。

       这就是Mybatis中实现Interceptor拦截的一个思想,如果用户觉得这个思想有问题或者不能完全满足你的要求的话可以通过实现自己的Plugin来决定什么时候需要代理什么时候需要拦截。以下讲解的内容都是基于Mybatis的默认实现即通过Plugin来管理Interceptor来讲解的。

       对于实现自己的Interceptor而言有两个很重要的注解,一个是@Intercepts,其值是一个@Signature数组。@Intercepts用于表明当前的对象是一个Interceptor,而@Signature则表明要拦截的接口、方法以及对应的参数类型。来看一个自定义的简单Interceptor:

package com.tiantian.mybatis.interceptor;
 
import java.sql.Connection;
import java.util.Properties;
 
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
 
@Intercepts( {
       @Signature(method = "query", type = Executor.class, args = {
              MappedStatement.class, Object.class, RowBounds.class,
              ResultHandler.class }),
       @Signature(method = "prepare", type = StatementHandler.class, args = { Connection.class }) })
public class MyInterceptor implements Interceptor {
 
    public Object intercept(Invocation invocation) throws Throwable {
       Object result = invocation.proceed();
       System.out.println("Invocation.proceed()");
       return result;
    }
 
    public Object plugin(Object target) {
       return Plugin.wrap(target, this);
    }
 
    public void setProperties(Properties properties) {
       String prop1 = properties.getProperty("prop1");
       String prop2 = properties.getProperty("prop2");
       System.out.println(prop1 + "------" + prop2);
    }
 
}

 

       首先看setProperties方法,这个方法在Configuration初始化当前的Interceptor时就会执行,这里只是简单的取两个属性进行打印。

       其次看plugin方法中我们是用的Plugin的逻辑来实现Mybatis的逻辑的。

       接着看MyInterceptor类上我们用@Intercepts标记了这是一个Interceptor,然后在@Intercepts中定义了两个@Signature,即两个拦截点。第一个@Signature我们定义了该Interceptor将拦截Executor接口中参数类型为MappedStatement、Object、RowBounds和ResultHandler的query方法;第二个@Signature我们定义了该Interceptor将拦截StatementHandler中参数类型为Connection的prepare方法。

       最后再来看一下intercept方法,这里我们只是简单的打印了一句话,然后调用invocation的proceed方法,使当前方法正常的调用。

       对于这个拦截器,Mybatis在注册该拦截器的时候就会利用定义好的n个property作为参数调用该拦截器的setProperties方法。之后在新建可拦截对象的时候会调用该拦截器的plugin方法来决定是返回目标对象本身还是代理对象。对于这个拦截器而言,当Mybatis是要Executor或StatementHandler对象的时候就会返回一个代理对象,其他都是原目标对象本身。然后当Executor代理对象在执行参数类型为MappedStatement、Object、RowBounds和ResultHandler的query方法或StatementHandler代理对象在执行参数类型为Connection的prepare方法时就会触发当前的拦截器的intercept方法进行拦截,而执行这两个接口对象的其他方法时都只是做一个简单的代理。

1.4    注册拦截器

       注册拦截器是通过在Mybatis配置文件中plugins元素下的plugin元素来进行的。一个plugin对应着一个拦截器,在plugin元素下面我们可以指定若干个property子元素。Mybatis在注册定义的拦截器时会先把对应拦截器下面的所有property通过Interceptor的setProperties方法注入给对应的拦截器。所以,我们可以这样来注册我们在前面定义的MyInterceptor:

 

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <properties resource="config/jdbc.properties"></properties>
    <typeAliases>
       <package name="com.tiantian.mybatis.model"/>
    </typeAliases>
    <plugins>
       <plugin interceptor="com.tiantian.mybatis.interceptor.MyInterceptor">
           <property name="prop1" value="prop1"/>
           <property name="prop2" value="prop2"/>
       </plugin>
    </plugins>
    <environments default="development">
       <environment id="development">
           <transactionManager type="JDBC" />
           <dataSource type="POOLED">
              <property name="driver" value="${jdbc.driver}" />
              <property name="url" value="${jdbc.url}" />
              <property name="username" value="${jdbc.username}" />
              <property name="password" value="${jdbc.password}" />
           </dataSource>
       </environment>
    </environments>
    <mappers>
       <mapper resource="com/tiantian/mybatis/mapper/UserMapper.xml"/>
    </mappers>
</configuration>

 

1.5    Mybatis可拦截的方法

       Mybatis拦截器只能拦截四种类型的接口:Executor、StatementHandler、ParameterHandler和ResultSetHandler。这是在Mybatis的Configuration中写死了的,如果要支持拦截其他接口就需要我们重写Mybatis的Configuration。Mybatis可以对这四个接口中所有的方法进行拦截。

1.6     利用拦截器进行分页

       下面将介绍一个Mybatis拦截器的实际应用。Mybatis拦截器常常会被用来进行分页处理。我们知道要利用JDBC对数据库进行操作就必须要有一个对应的Statement对象,Mybatis在执行Sql语句前也会产生一个包含Sql语句的Statement对象,而且对应的Sql语句是在Statement之前产生的,所以我们就可以在它成Statement之前对用来生成Statement的Sql语句下手。在Mybatis中Statement语句是通过RoutingStatementHandler对象的prepare方法生成的。所以利用拦截器实现Mybatis分页的一个思路就是拦截StatementHandler接口的prepare方法,然后在拦截器方法中把Sql语句改成对应的分页查询Sql语句,之后再调用StatementHandler对象的prepare方法,即调用invocation.proceed()。更改Sql语句这个看起来很简单,而事实上来说的话就没那么直观,因为包括sql等其他属性在内的多个属性都没有对应的方法可以直接取到,它们对外部都是封闭的,是对象的私有属性,所以这里就需要引入反射机制来获取或者更改对象的私有属性的值了。对于分页而言,在拦截器里面我们常常还需要做的一个操作就是统计满足当前条件的记录一共有多少,这是通过获取到了原始的Sql语句后,把它改为对应的统计语句再利用Mybatis封装好的参数和设置参数的功能把Sql语句中的参数进行替换,之后再执行查询记录数的Sql语句进行总记录数的统计。先来看一个我们对分页操作封装的一个实体类Page:

import java.util.HashMap;
import java.util.List;
import java.util.Map;
 
/**
 * 对分页的基本数据进行一个简单的封装
 */
public class Page<T> {
 
    private int pageNo = 1;//页码,默认是第一页
    private int pageSize = 15;//每页显示的记录数,默认是15
    private int totalRecord;//总记录数
    private int totalPage;//总页数
    private List<T> results;//对应的当前页记录
    private Map<String, Object> params = new HashMap<String, Object>();//其他的参数我们把它分装成一个Map对象
 
    public int getPageNo() {
       return pageNo;
    }
 
    public void setPageNo(int pageNo) {
       this.pageNo = pageNo;
    }
 
    public int getPageSize() {
       return pageSize;
    }
 
    public void setPageSize(int pageSize) {
       this.pageSize = pageSize;
    }
 
    public int getTotalRecord() {
       return totalRecord;
    }
 
    public void setTotalRecord(int totalRecord) {
       this.totalRecord = totalRecord;
       //在设置总页数的时候计算出对应的总页数,在下面的三目运算中加法拥有更高的优先级,所以最后可以不加括号。
       int totalPage = totalRecord%pageSize==0 ? totalRecord/pageSize : totalRecord/pageSize + 1;
       this.setTotalPage(totalPage);
    }
 
    public int getTotalPage() {
       return totalPage;
    }
 
    public void setTotalPage(int totalPage) {
       this.totalPage = totalPage;
    }
 
    public List<T> getResults() {
       return results;
    }
 
    public void setResults(List<T> results) {
       this.results = results;
    }
   
    public Map<String, Object> getParams() {
       return params;
    }
   
    public void setParams(Map<String, Object> params) {
       this.params = params;
    }
 
    @Override
    public String toString() {
       StringBuilder builder = new StringBuilder();
       builder.append("Page [pageNo=").append(pageNo).append(", pageSize=")
              .append(pageSize).append(", results=").append(results).append(
                     ", totalPage=").append(totalPage).append(
                     ", totalRecord=").append(totalRecord).append("]");
       return builder.toString();
    }
 
}

 

       对于需要进行分页的Mapper映射,我们会给它传一个Page对象作为参数,我们可以看到Page对象里面包括了一些分页的基本信息,这些信息我们可以在拦截器里面用到,然后我们把除分页的基本信息以外的其他参数用一个Map对象进行包装,这样在Mapper映射语句中的其他参数就可以从Map中取值了。接着来看一下我们的PageInterceptor的定义,对于PageInterceptor我就不做过多的说明,代码里面附有很详细的注释信息:

package com.tiantian.mybatis.interceptor;
 
import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Properties;
 
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.RoutingStatementHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;
 
import com.tiantian.mybatis.model.Page;
 
/**
 *
 * 分页拦截器,用于拦截需要进行分页查询的操作,然后对其进行分页处理。
 * 利用拦截器实现Mybatis分页的原理:
 * 要利用JDBC对数据库进行操作就必须要有一个对应的Statement对象,Mybatis在执行Sql语句前就会产生一个包含Sql语句的Statement对象,而且对应的Sql语句
 * 是在Statement之前产生的,所以我们就可以在它生成Statement之前对用来生成Statement的Sql语句下手。在Mybatis中Statement语句是通过RoutingStatementHandler对象的
 * prepare方法生成的。所以利用拦截器实现Mybatis分页的一个思路就是拦截StatementHandler接口的prepare方法,然后在拦截器方法中把Sql语句改成对应的分页查询Sql语句,之后再调用
 * StatementHandler对象的prepare方法,即调用invocation.proceed()。
 * 对于分页而言,在拦截器里面我们还需要做的一个操作就是统计满足当前条件的记录一共有多少,这是通过获取到了原始的Sql语句后,把它改为对应的统计语句再利用Mybatis封装好的参数和设
 * 置参数的功能把Sql语句中的参数进行替换,之后再执行查询记录数的Sql语句进行总记录数的统计。
 *
 */
@Intercepts( {
       @Signature(method = "prepare", type = StatementHandler.class, args = {Connection.class}) })
public class PageInterceptor implements Interceptor {
 
    private String databaseType;//数据库类型,不同的数据库有不同的分页方法
   
    /**
     * 拦截后要执行的方法
     */
    public Object intercept(Invocation invocation) throws Throwable {
       //对于StatementHandler其实只有两个实现类,一个是RoutingStatementHandler,另一个是抽象类BaseStatementHandler,
       //BaseStatementHandler有三个子类,分别是SimpleStatementHandler,PreparedStatementHandler和CallableStatementHandler,
       //SimpleStatementHandler是用于处理Statement的,PreparedStatementHandler是处理PreparedStatement的,而CallableStatementHandler是
       //处理CallableStatement的。Mybatis在进行Sql语句处理的时候都是建立的RoutingStatementHandler,而在RoutingStatementHandler里面拥有一个
       //StatementHandler类型的delegate属性,RoutingStatementHandler会依据Statement的不同建立对应的BaseStatementHandler,即SimpleStatementHandler、
       //PreparedStatementHandler或CallableStatementHandler,在RoutingStatementHandler里面所有StatementHandler接口方法的实现都是调用的delegate对应的方法。
       //我们在PageInterceptor类上已经用@Signature标记了该Interceptor只拦截StatementHandler接口的prepare方法,又因为Mybatis只有在建立RoutingStatementHandler的时候
       //是通过Interceptor的plugin方法进行包裹的,所以我们这里拦截到的目标对象肯定是RoutingStatementHandler对象。
       RoutingStatementHandler handler = (RoutingStatementHandler) invocation.getTarget();
       //通过反射获取到当前RoutingStatementHandler对象的delegate属性
       StatementHandler delegate = (StatementHandler)ReflectUtil.getFieldValue(handler, "delegate");
       //获取到当前StatementHandler的 boundSql,这里不管是调用handler.getBoundSql()还是直接调用delegate.getBoundSql()结果是一样的,因为之前已经说过了
       //RoutingStatementHandler实现的所有StatementHandler接口方法里面都是调用的delegate对应的方法。
       BoundSql boundSql = delegate.getBoundSql();
       //拿到当前绑定Sql的参数对象,就是我们在调用对应的Mapper映射语句时所传入的参数对象
       Object obj = boundSql.getParameterObject();
       //这里我们简单的通过传入的是Page对象就认定它是需要进行分页操作的。
       if (obj instanceof Page<?>) {
           Page<?> page = (Page<?>) obj;
           //通过反射获取delegate父类BaseStatementHandler的mappedStatement属性
           MappedStatement mappedStatement = (MappedStatement)ReflectUtil.getFieldValue(delegate, "mappedStatement");
           //拦截到的prepare方法参数是一个Connection对象
           Connection connection = (Connection)invocation.getArgs()[0];
           //获取当前要执行的Sql语句,也就是我们直接在Mapper映射语句中写的Sql语句
           String sql = boundSql.getSql();
           //给当前的page参数对象设置总记录数
           this.setTotalRecord(page,
                  mappedStatement, connection);
           //获取分页Sql语句
           String pageSql = this.getPageSql(page, sql);
           //利用反射设置当前BoundSql对应的sql属性为我们建立好的分页Sql语句
           ReflectUtil.setFieldValue(boundSql, "sql", pageSql);
       }
       return invocation.proceed();
    }
 
 
    /**
     * 拦截器对应的封装原始对象的方法
     */
    public Object plugin(Object target) {
       return Plugin.wrap(target, this);
    }
 
    /**
     * 设置注册拦截器时设定的属性
     */
    public void setProperties(Properties properties) {
       this.databaseType = properties.getProperty("databaseType");
    }
   
    /**
     * 根据page对象获取对应的分页查询Sql语句,这里只做了两种数据库类型,Mysql和Oracle
     * 其它的数据库都 没有进行分页
     *
     * @param page 分页对象
     * @param sql 原sql语句
     * @return
     */
    private String getPageSql(Page<?> page, String sql) {
       StringBuffer sqlBuffer = new StringBuffer(sql);
       if ("mysql".equalsIgnoreCase(databaseType)) {
           return getMysqlPageSql(page, sqlBuffer);
       } else if ("oracle".equalsIgnoreCase(databaseType)) {
           return getOraclePageSql(page, sqlBuffer);
       }
       return sqlBuffer.toString();
    }
   
    /**
     * 获取Mysql数据库的分页查询语句
     * @param page 分页对象
     * @param sqlBuffer 包含原sql语句的StringBuffer对象
     * @return Mysql数据库分页语句
     */
    private String getMysqlPageSql(Page<?> page, StringBuffer sqlBuffer) {
       //计算第一条记录的位置,Mysql中记录的位置是从0开始的。
       int offset = (page.getPageNo() - 1) * page.getPageSize();
       sqlBuffer.append(" limit ").append(offset).append(",").append(page.getPageSize());
       return sqlBuffer.toString();
    }
   
    /**
     * 获取Oracle数据库的分页查询语句
     * @param page 分页对象
     * @param sqlBuffer 包含原sql语句的StringBuffer对象
     * @return Oracle数据库的分页查询语句
     */
    private String getOraclePageSql(Page<?> page, StringBuffer sqlBuffer) {
       //计算第一条记录的位置,Oracle分页是通过rownum进行的,而rownum是从1开始的
       int offset = (page.getPageNo() - 1) * page.getPageSize() + 1;
       sqlBuffer.insert(0, "select u.*, rownum r from (").append(") u where rownum < ").append(offset + page.getPageSize());
       sqlBuffer.insert(0, "select * from (").append(") where r >= ").append(offset);
       //上面的Sql语句拼接之后大概是这个样子:
       //select * from (select u.*, rownum r from (select * from t_user) u where rownum < 31) where r >= 16
       return sqlBuffer.toString();
    }
   
    /**
     * 给当前的参数对象page设置总记录数
     *
     * @param page Mapper映射语句对应的参数对象
     * @param mappedStatement Mapper映射语句
     * @param connection 当前的数据库连接
     */
    private void setTotalRecord(Page<?> page,
           MappedStatement mappedStatement, Connection connection) {
       //获取对应的BoundSql,这个BoundSql其实跟我们利用StatementHandler获取到的BoundSql是同一个对象。
       //delegate里面的boundSql也是通过mappedStatement.getBoundSql(paramObj)方法获取到的。
       BoundSql boundSql = mappedStatement.getBoundSql(page);
       //获取到我们自己写在Mapper映射语句中对应的Sql语句
       String sql = boundSql.getSql();
       //通过查询Sql语句获取到对应的计算总记录数的sql语句
       String countSql = this.getCountSql(sql);
       //通过BoundSql获取对应的参数映射
       List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
       //利用Configuration、查询记录数的Sql语句countSql、参数映射关系parameterMappings和参数对象page建立查询记录数对应的BoundSql对象。
       BoundSql countBoundSql = new BoundSql(mappedStatement.getConfiguration(), countSql, parameterMappings, page);
       //通过mappedStatement、参数对象page和BoundSql对象countBoundSql建立一个用于设定参数的ParameterHandler对象
       ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, page, countBoundSql);
       //通过connection建立一个countSql对应的PreparedStatement对象。
       PreparedStatement pstmt = null;
       ResultSet rs = null;
       try {
           pstmt = connection.prepareStatement(countSql);
           //通过parameterHandler给PreparedStatement对象设置参数
           parameterHandler.setParameters(pstmt);
           //之后就是执行获取总记录数的Sql语句和获取结果了。
           rs = pstmt.executeQuery();
           if (rs.next()) {
              int totalRecord = rs.getInt(1);
              //给当前的参数page对象设置总记录数
              page.setTotalRecord(totalRecord);
           }
       } catch (SQLException e) {
           e.printStackTrace();
       } finally {
           try {
              if (rs != null)
                  rs.close();
               if (pstmt != null)
                  pstmt.close();
           } catch (SQLException e) {
              e.printStackTrace();
           }
       }
    }
   
    /**
     * 根据原Sql语句获取对应的查询总记录数的Sql语句
     * @param sql
     * @return
     */
    private String getCountSql(String sql) {
       
       return "select count(1) from (" + sql + ")";
    }
   
    /**
     * 利用反射进行操作的一个工具类
     *
     */
    private static class ReflectUtil {
       /**
        * 利用反射获取指定对象的指定属性
        * @param obj 目标对象
        * @param fieldName 目标属性
        * @return 目标属性的值
        */
       public static Object getFieldValue(Object obj, String fieldName) {
           Object result = null;
           Field field = ReflectUtil.getField(obj, fieldName);
           if (field != null) {
              field.setAccessible(true);
              try {
                  result = field.get(obj);
              } catch (IllegalArgumentException e) {
                  // TODO Auto-generated catch block
                  e.printStackTrace();
              } catch (IllegalAccessException e) {
                  // TODO Auto-generated catch block
                  e.printStackTrace();
              }
           }
           return result;
       }
      
       /**
        * 利用反射获取指定对象里面的指定属性
        * @param obj 目标对象
        * @param fieldName 目标属性
        * @return 目标字段
        */
       private static Field getField(Object obj, String fieldName) {
           Field field = null;
          for (Class<?> clazz=obj.getClass(); clazz != Object.class; clazz=clazz.getSuperclass()) {
              try {
                  field = clazz.getDeclaredField(fieldName);
                  break;
              } catch (NoSuchFieldException e) {
                  //这里不用做处理,子类没有该字段可能对应的父类有,都没有就返回null。
              }
           }
           return field;
       }
 
       /**
        * 利用反射设置指定对象的指定属性为指定的值
        * @param obj 目标对象
        * @param fieldName 目标属性
         * @param fieldValue 目标值
        */
       public static void setFieldValue(Object obj, String fieldName,
              String fieldValue) {
           Field field = ReflectUtil.getField(obj, fieldName);
           if (field != null) {
              try {
                  field.setAccessible(true);
                  field.set(obj, fieldValue);
              } catch (IllegalArgumentException e) {
                  // TODO Auto-generated catch block
                  e.printStackTrace();
              } catch (IllegalAccessException e) {
                  // TODO Auto-generated catch block
                  e.printStackTrace();
              }
           }
        }
    }
 
}

 

       接着我们在Mybatis的配置文件里面注册该拦截器:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <properties resource="config/jdbc.properties"></properties>
    <typeAliases>
       <package name="com.tiantian.mybatis.model"/>
    </typeAliases>
    <plugins>
       <plugin interceptor="com.tiantian.mybatis.interceptor.PageInterceptor">
           <property name="databaseType" value="Oracle"/>
       </plugin>
    </plugins>
    <environments default="development">
       <environment id="development">
           <transactionManager type="JDBC" />
           <dataSource type="POOLED">
              <property name="driver" value="${jdbc.driver}" />
              <property name="url" value="${jdbc.url}" />
               <property name="username" value="${jdbc.username}" />
              <property name="password" value="${jdbc.password}" />
           </dataSource>
       </environment>
    </environments>
    <mappers>
       <mapper resource="com/tiantian/mybatis/mapper/UserMapper.xml"/>
    </mappers>

 

       这样我们的拦截器就已经定义并且配置好了,接下来我们就来测试一下。假设在我们的UserMapper.xml中有如下这样一个Mapper映射信息:

    <select id="findPage" resultType="User" parameterType="page">
       select * from t_user
    </select>

 

       那我们就可以这样来测试它:

       SqlSession sqlSession = sqlSessionFactory.openSession();
       try {
           UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
           Page<User> page = new Page<User>();
           page.setPageNo(2);
           List<User> users = userMapper.findPage(page);
           page.setResults(users);
           System.out.println(page);
       } finally {
           sqlSession.close();
       }

 

 

34
3
分享到:
评论
68 楼 liu.anxin 2017-05-25  
基于 StatementHandler 做拦截好像无法使用 cache 了
67 楼 culven 2017-03-31  
知道原因啦。不能继承RowBounds,直接按照楼主所写即可
66 楼 culven 2017-03-31  
楼主帮忙看下:
我在page的params中加入条件查询,总数查出来了,但是具体数据没有查出来,不知怎么回事。

另外,Object obj = boundSql.getParameterObject(); 这行我传入的参数是page,但是得到的obj为空,这是为何呢?

我的page自行修改封装过,但是大同小异的,如下:


import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.ibatis.session.RowBounds;

/**
 * Mybatis分页参数及查询结果封装. 注意所有序号从1开始.
 * @param <T> Page中记录的类型.
 * @author Administrator
 **/
public class Page<T> extends RowBounds {
    // --分页参数 --//
    /**
     * 页编号 : 第几页
     */
    protected int pageNo = 1;
    /**
     * 页大小 : 每页的数量
     */
    protected int pageSize = 10;

    /**
     * 偏移量 : 第一条数据在表中的位置
     */
    protected int offset;

    /**
     * 限定数 : 每页的数量
     */
    protected int limit;
    
    /**
     * 查询参数
     */
    protected Map<String,Object> params = new HashMap<String,Object>();

    // --结果 --//
    /**
     * 查询结果
     */
    protected List<T> result = new ArrayList<T>();

    /**
     * 总条数
     */
    protected int totalCount;

    /**
     * 总页数
     */
    protected int totalPages;

    // --计算 数据库 查询的参数 : LIMIT 3, 3; LIMIT offset, limit; --//
    /**
     * 计算偏移量
     */
    private void calcOffset() {
        this.offset = ((pageNo - 1) * pageSize);
    }

    /**
     * 计算限定数
     */
    private void calcLimit() {
        this.limit = pageSize;
    }

    // -- 构造函数 --//
    public Page() {
        this.calcOffset();
        this.calcLimit();
    }

    public Page(int pageNo, int pageSize) {
        this.pageNo = pageNo;
        this.pageSize = pageSize;
        this.calcOffset();
        this.calcLimit();
    }

    // -- 访问查询参数函数 --//
    /**
     * 获得当前页的页号,序号从1开始,默认为1.
     */
    public int getPageNo() {
        return pageNo;
    }

    /**
     * 获得每页的记录数量,默认为1.
     */
    public int getPageSize() {
        return pageSize;
    }

    /**
     * 根据pageNo和pageSize计算当前页第一条记录在总结果集中的位置,序号从1开始.
     */
    public int getFirst() {
        return ((pageNo - 1) * pageSize) + 1;
    }

    /**
     * 根据pageNo和pageSize计算当前页第一条记录在总结果集中的位置,序号从0开始.
     */
    public int getOffset() {
        return offset;
    }

    public int getLimit() {
        return limit;
    }

    // -- 访问查询结果函数 --//
    /**
     * 取得页内的记录列表.
     */
    public List<T> getResult() {
        return result;
    }

    /**
     * 设置页内的记录列表.
     */
    public void setResult(final List<T> result) {
        this.result = result;
    }

    /**
     * 取得总记录数, 默认值为-1.
     */
    public int getTotalCount() {
        return totalCount;
    }

    /**
     * 设置总记录数.
     */
    public void setTotalCount(final int totalCount) {
        this.totalCount = totalCount;
        this.totalPages = this.getTotalPages();
    }

    /**
     * 根据pageSize与totalCount计算总页数, 默认值为-1.
     */
    public int getTotalPages() {
        if (totalCount < 0) {
            return -1;
        }
        int pages = totalCount / pageSize;
        return totalCount % pageSize > 0 ? ++pages : pages;
    }

    public void setTotalPages(int totalPages) {
        this.totalPages = totalPages;
    }

	public Map<String, Object> getParams() {
		return params;
	}

	public void setParams(Map<String, Object> params) {
		this.params = params;
	}
    
}
65 楼 iammabing 2016-07-01  
成功了,多谢楼主提供的文章,在基于楼主代码的基础上补充一下SqlServer的分页方言
/**
     * 获取SqlServer数据库的分页查询语句
     * [color=yellow]@param[/color] page 分页对象
     * [color=yellow]@param[/color] sqlBuffer 包含原sql语句的StringBuffer对象
     * [color=yellow]@return[/color] SqlServer数据库分页语句
     * 
     */
	private String getSqlServerPageSql(Page<?> page, StringBuffer sqlBuffer) {
		// 计算第一条记录的位置,sqlServer中记录的位置是从0开始的。
		int offset = (page.getPageNo()) * page.getPageSize();

		// 先取where之前的部分,然后lastIndexOf来获取from的位置,防止字段名称中有"from"
		String frontPart = sqlBuffer.substring(0, sqlBuffer.toString().toLowerCase().indexOf("where") == -1 ? sqlBuffer.length() : sqlBuffer.toString().toLowerCase().indexOf("where"));
		int indexOfFrom = frontPart.toLowerCase().lastIndexOf("from");
		String selectFld = sqlBuffer.substring(0, indexOfFrom);

		// 取出 order by 语句
		int lastIndexOfOrderBy = sqlBuffer.toString().toLowerCase().lastIndexOf("order by");
		String orderby = sqlBuffer.substring(lastIndexOfOrderBy, sqlBuffer.length());

		// 取出 from 语句后的内容
		String selectFromTableAndWhere = sqlBuffer.substring(indexOfFrom, lastIndexOfOrderBy);
		
		// 清空sqlBuffer开始重新拼装
		sqlBuffer.delete(0,sqlBuffer.length());
		sqlBuffer.append("select * from (").append(selectFld).append(",ROW_NUMBER() OVER(").append(orderby).append(") as _page_row_num_hb ").append(selectFromTableAndWhere).append(" ) temp ").append(" where  _page_row_num_hb BETWEEN  ").append(offset).append(" and ").append((page.getPageNo()+1)*page.getPageSize());
		return sqlBuffer.toString();
	}

注意,我这里的PageNo是从0开始的。
64 楼 zhangwanli99 2016-05-27  
非常不错,但是有个瑕疵。
sql语句中有<bind/>标签时,mybatis会在BoundSql中添加Map<String, Object> additionalParameters参数。该插件未对此做支持,
加上以下代码可解决此问题:

private void setTotalRecord(Page<?> page, MappedStatement mappedStatement, Connection connection) {   
        BoundSql boundSql = mappedStatement.getBoundSql(page);
        String sql = boundSql.getSql();
        String countSql = getCountSql(sql);
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        //获取原bondSql的additionalParameters
        Map<String, Object> additionalParameters = (Map<String, Object>) ReflectUtil.getFieldValue(boundSql, "additionalParameters");
        BoundSql countBoundSql = new BoundSql(mappedStatement.getConfiguration(), countSql, parameterMappings, page);
        Set<String> keySet = additionalParameters.keySet();
        for (String key : keySet) {
            countBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
        }
63 楼 lutghj1990 2016-04-08  
楼主的方法非常好的,分页中查询数据反应在一秒之内,我看楼下有人说where条件中的参数不能传入,其实是可以得,只不过你写的不对而已


参数传入参考这里:http://openwares.net/database/mybatis_parametertype.html


局部代码参考:
接口:

public List<CarRealTimeStatusT> queryByCarResetListPage(@Param("page") Page page, @Param("map")Map<String, Object> map);

controller:

List<CarRealTimeStatusT> carResetList = iCarResetOperation.queryByCarResetListPage(page,map);

mapper:
<select id="queryByCarResetListPage" parameterType="Map" resultType="CarRealTimeStatusT">
select * from t_carrealtimestatus
<where>

<if test="map.id != null">and id = #{map.id}</if>
</where>
</select>


62 楼 lily_better 2016-03-24  
可以使用,赞
61 楼 java-lxm 2015-07-30  
再有ORDER BY 是不是效率有点低
60 楼 l975764577 2015-03-02  
大神 能不能留个qq  交流下 被分页弄的头痛
59 楼 234390216 2015-01-16  
dearmite 写道
把代码,细看了一下.
原来还有这么一段..
int index = sql.indexOf("from");
return "select count(*) " + sql.substring(index);

根本就不是select count(*) from (select ##,##,## from aa )

不过,反倒有点担心这个替换会不会碰到复杂SQL 出错了.
....
因为网上多数还都在使用子查询这种...


对,这里确实会有这种问题。多谢提醒。
58 楼 dearmite 2015-01-13  
把代码,细看了一下.
原来还有这么一段..
int index = sql.indexOf("from");
return "select count(*) " + sql.substring(index);

根本就不是select count(*) from (select ##,##,## from aa )

不过,反倒有点担心这个替换会不会碰到复杂SQL 出错了.
....
因为网上多数还都在使用子查询这种...
57 楼 dearmite 2015-01-13  
我就是上来泡个冒,看看我还存在不?

我觉得开发方便就是第一位.至少是少写了很多的sql..

如果真的有一个两个大表 需要效率,你不传PAGE,
你就按以前不分页的mybatis 处理不就行了么??

总不至少有人说,加了一个plugin 里面多了一个判断,然后效率就低下了许多吧?

再说现在的分页的插件 感觉好多文章 ,大致的思路都是这样.

你也可以改造一下.

在page 里加入一个needCount 这样,传这个参数false 的时候,就不去数据库查count 总数.
让用户自己查.

当然,最最直接就是,按原始的mybatis 自己查了
56 楼 qkjava 2014-11-17  
很好。可以满足快速开发的需求,至于说效率,我想大多数项目还是以开发的效率优先而不是所谓的执行速度。现在的软硬件环境已经能满足很多应用的速度需求了。 
55 楼 234390216 2014-10-27  
di1984HIT 写道
个人觉得这样确实有好处。但是就是有可能查询效率太低下了。

实践出真知,绝大部分应该都是可以满足的。
54 楼 di1984HIT 2014-10-25  
个人觉得这样确实有好处。但是就是有可能查询效率太低下了。
53 楼 mingkongeye 2014-09-29  
跑通啦,多谢 楼主!

说要去掉where....这些,是没有在参数前加 params,比如楼主的: name like #{params.name}  ,这是正解。
52 楼 234390216 2014-08-07  
liruikqn 写道
<select id="findPage" resultMap="UserResult" parameterType="page"> 
    select * from t_user  
    <where> 
        <if test="params.name != '' and params.name != null"> 
            name like #{params.name} 
        </if> 
    </where> 
</select> 
楼主,我就是像你这样写会报错。去掉where条件只有select * from t_user就不报错了,跟44楼的说的一样。什么情况?

报什么错误了?贴出来看看。
51 楼 liruikqn 2014-08-06  
<select id="findPage" resultMap="UserResult" parameterType="page"> 
    select * from t_user  
    <where> 
        <if test="params.name != '' and params.name != null"> 
            name like #{params.name} 
        </if> 
    </where> 
</select> 
楼主,我就是像你这样写会报错。去掉where条件只有select * from t_user就不报错了,跟44楼的说的一样。什么情况?
50 楼 liruikqn 2014-08-06  
楼主,mapper.xml可不可以像下面这样有where条件?比如:
<select id="findPage" resultType="User" parameterType="page"> 
   select * from t_user where 字段 = #{xxx}
</select> 
我这样写了,插件拦截后为什么报错:无效的索引,去掉where后不报错了,求解答
49 楼 sunny8675 2014-07-02  
如何添加SqlServer的分页语句呢?

相关推荐

    Mybatis拦截器介绍及分页插件示例

    ### Mybatis拦截器介绍及分页插件 #### 1.2 前言 在Mybatis框架中,拦截器(Interceptor)是一个重要的组件,它允许开发者在不修改框架核心逻辑的情况下,添加自定义的行为。例如,可以在SQL执行前后添加日志记录...

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

    ### Mybatis拦截器介绍及分页插件 #### 1.1 前言与概述 在探讨Mybatis拦截器及其分页插件之前,我们首先需要了解什么是拦截器以及为何要在Mybatis中使用它。 **拦截器**是一种设计模式,它允许在执行某个操作前或...

    MyBatis拦截器及分页插件

    MyBatis拦截器及分页插件 MyBatis拦截器是MyBatis框架中一个强大的功能,它允许用户在执行某些方法之前或之后执行某些逻辑。拦截器的使用可以极大地扩展MyBatis的功能,使其更加灵活和强大。 拦截器的设计初衷是...

    mybatis使用拦截器实现分页操作

    MyBatis拦截器类似于Java的AOP(面向切面编程)中的拦截器,它可以在特定的方法调用前后插入自定义的行为。在MyBatis中,我们可以通过实现`org.apache.ibatis.plugin.Interceptor`接口并重写`intercept`方法来创建一...

    MyBatis拦截器分页与动态修改SQL及其参数值

    在"MyBatis拦截器分页与动态修改SQL及其参数值"的主题中,我们可以深入理解以下几个关键知识点: 1. **MyBatis拦截器**:MyBatis提供了一种插件机制,即拦截器(Interceptor),它基于Java的动态代理,可以在SQL...

    springboot+mybatis拦截器实现自动分页

    MyBatis本身并不直接支持分页,但可以通过拦截器或者自定义插件的方式来实现。这里我们将使用拦截器。 1. **创建拦截器**: 首先,我们需要创建一个实现了`Interceptor`接口的类。在`intercept`方法中,我们可以获取...

    mybatis 分页拦截器及拦截器配置

    MyBatis 分页拦截器是实现数据库查询分页效果的一种高效解决方案。在传统的SQL查询中,我们通常需要手动编写...在实际项目中,根据业务需求选择合适的分页插件,并正确配置拦截器,能够有效提升开发效率和代码质量。

    Mybatis分页拦截器

    总的来说,Mybatis分页拦截器是提升项目效率的一个重要工具,它通过插件化的方式,使开发者能方便地为Mybatis添加自定义的分页功能,同时保持代码的整洁和可维护性。在不同版本的Mybatis中,正确理解和使用拦截器,...

    mybatis分页拦截器(自动封装版)剖析.pdf

    在分页拦截器内部,一般会利用MyBatis提供的插件机制,通过实现`Interceptor`接口并重写`intercept`方法来插入分页逻辑。在`intercept`方法中,拦截器会检查被拦截的SQL语句,并根据PageBean中的信息动态拼接分页...

    spring+springMVC+mybatis拦截器分页 源码

    综上所述,"spring+springMVC+mybatis拦截器分页"项目结合了三大框架的优势,通过SpringMVC的拦截器实现业务逻辑的扩展,利用MyBatis的分页插件处理后台数据,再由EasyUI提供友好的用户界面。这样的组合为高效且可控...

    mybatis拦截器分页

    - MyBatis 还提供了 PageHelper 插件,它也实现了分页功能,但与自定义拦截器相比,可能在灵活性和定制性上稍有不足。 - 使用拦截器可以与现有的分页库(如 MyBatis-Plus)无缝集成,或者完全独立实现,从而更好地...

    mybatis代码生成器和分页插件.zip

    在这个“mybatis代码生成器和分页插件.zip”压缩包中,包含了两个重要的工具,即MyBatis的代码生成器和分页插件,它们极大地提高了开发效率并优化了数据库操作。 1. MyBatis 代码生成器(MyBatis Generator, MBG) ...

    mybatis分页插件的使用

    - **PageHelper**: 分页插件的核心类,实现了Mybatis拦截器接口,负责处理分页逻辑。 - **PageInfo**: 包装类,提供了丰富的分页属性信息,如总记录数、当前页码等。 - **SqlParser**: 可选组件,用于生成高效的...

    mybatis分页拦截器

    MyBatis拦截器是基于AOP(面向切面编程)的一种机制,它允许我们在特定的执行点(如SQL语句的执行、参数的设置等)插入自定义的行为。分页拦截器就是在执行SQL之前或之后,对SQL语句进行修改,添加必要的分页条件,...

    mybatis拦截器与分页插件实例教程

    所以下面这篇文章主要给大家介绍了关于mybatis拦截器与分页插件的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用mybatis具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧

    Mybatis拦截器实现分页

    【Mybatis拦截器实现分页】 在Mybatis框架中,拦截器是一种强大的工具,它允许我们在执行SQL之前或之后进行额外的操作,如日志记录、权限检查等。本篇文章将探讨如何使用Mybatis拦截器来实现高效且方便的分页功能。 ...

    Mybatis PageHelper分页插件是一个应用于Mybatis中的分页插件系统.rar

    分页插件PageHelper是通过mybatis的拦截器实现分页功能的,拦截sql查询请求,添加分页语句,最终实现分页查询功能。 一、分页插件PageHelper支持的数据库类型? Oracle,MySql,MariaDB,SQLite等 二、分页插件...

Global site tag (gtag.js) - Google Analytics