Mybatis源码(六)mybatis的插件编写之分页插件

前面我们已经介绍了mbatis的一些源码,这篇博客我们说下mybatis的插件的书写,首先我们要先了解一下mybatis的执行流程,具体如下图:

在这里插入图片描述

想想我们在执行分页查询的时候,应该在什么时候,虽然上面后三个阶段都可以拦截,但是这儿的分页查询,我们在执行SQL语句之间进行拦截,所以这儿就在StatementHandler的阶段拦截。然后我们再想下,执行分页查询的步骤有哪些?具体如下:

第一步 执行一条count语句
第二步 重写sql select * from admin limit start,limit

有了上面的思路,我们开始书写对应的插件,插件默认要实现mybatisInterceptor接口,于是我写出了如下的代码

//args: 你需要mybatis传入什么参数给你 type :你需要拦截的对象  method=要拦截的方法
@Intercepts(@Signature(type = StatementHandler.class,method ="prepare",args = {Connection.class,Integer.class}))
public class PagePlugin implements Interceptor {
  //插件的核心处理方法。
  @Override
  public Object intercept(Invocation invocation) throws Throwable {
  }
  
  @Override
  public Object plugin(Object target) {
    //固定的写法,第二个参数表示要代理的对象
    return Plugin.wrap(target,this);
  }
  
  //要传给这个插件的配置信息
  @Override
  public void setProperties(Properties properties) {
  }
}

上面我们书写了一个简单的实现Interceptor接口的插件类,回到上面的问题,走来第一步要执行一条count的语句。但是现在我们不知道是对那条语句进行执行count语句,所以这儿必须要对指定的语句进行拦截,于是我们要加一个变量叫pageSqlId,用来执行哪些语句进行分页查询。还有就是不同的数据库的分页查询是不一样的。所以我们这儿需要指定一个数据库的类型,用databaseType变量来指定,这两个变量是在配置文件中配置的。于是我们写出下面的代码

//args: 你需要mybatis传入什么参数给你 type :你需要拦截的对象  method=要拦截的方法
@Intercepts(@Signature(type = StatementHandler.class,method ="prepare",args = {Connection.class,Integer.class}))
public class PagePlugin implements Interceptor {
  
  String databaseType = "";
  String pageSqlId = "";
  
  //插件的核心处理方法。
  @Override
  public Object intercept(Invocation invocation) throws Throwable {
  }
  
  @Override
  public Object plugin(Object target) {
    //固定的写法,第二个参数表示要代理的对象
    return Plugin.wrap(target,this);
  }
  
  //要传给这个插件的配置信息
  @Override
  public void setProperties(Properties properties) {
    //从主配置文件中读取对应的配置
    this.databaseType = properties.getProperty("databaseType");
    this.pageSqlId = properties.getProperty("pageSqlId");
  }
}

现在这两个关键的变量已经执行好了。于是我们开始执行count语句。要执行count语句的前提就是必须知道原来的SQL语句。那么我们如何获取呢?我们开始也不清楚,于是我们先写一部分代码,然后debug,通过反射的方式获取对应的SQL语句,因为mybatis没有提供方法直接获取对应的SQL语句,于是我们写出了下面的代码

//args: 你需要mybatis传入什么参数给你 type :你需要拦截的对象  method=要拦截的方法
@Intercepts(@Signature(type = StatementHandler.class,method ="prepare",args = {Connection.class,Integer.class}))
public class PagePlugin implements Interceptor {
  
  String databaseType = "";
  String pageSqlId = "";
  
  //插件的核心处理方法。
  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
  }
  
  @Override
  public Object plugin(Object target) {
    //固定的写法,第二个参数表示要代理的对象
    return Plugin.wrap(target,this);
  }
  
  //要传给这个插件的配置信息
  @Override
  public void setProperties(Properties properties) {
    //从主配置文件中读取对应的配置
    this.databaseType = properties.getProperty("databaseType");
    this.pageSqlId = properties.getProperty("pageSqlId");
  }
}

通过debug的方式运行,运行的结果如下:

在这里插入图片描述

你可以发现要获取对应的对象,是多么的难,不过mybatis提供了对应的反射工具类。可以直接获取到。于是我写出了如下的代码

//args: 你需要mybatis传入什么参数给你 type :你需要拦截的对象  method=要拦截的方法
@Intercepts(@Signature(type = StatementHandler.class,method ="prepare",args = {Connection.class,Integer.class}))
public class PagePlugin implements Interceptor {
  
  String databaseType = "";
  String pageSqlId = "";
  
  //插件的核心处理方法。
  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
    //固定的写法
    MetaObject metaObject = MetaObject.forObject(
      statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY,SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,new 
      DefaultReflectorFactory());
    //通过属性名一个个获取
    String sqlId = (String)metaObject.getValue("delegate.mappedStatement.id");
  }
  
  @Override
  public Object plugin(Object target) {
    //固定的写法,第二个参数表示要代理的对象
    return Plugin.wrap(target,this);
  }
  
  //要传给这个插件的配置信息
  @Override
  public void setProperties(Properties properties) {
    //从主配置文件中读取对应的配置
    this.databaseType = properties.getProperty("databaseType");
    this.pageSqlId = properties.getProperty("pageSqlId");
  }
}

通过上面的代码我们就拿到执行的SQLid,具体如下图:

在这里插入图片描述

于是我们通过正则表达式来筛选正确的SQL语句。来决定要对哪些SQL语句进行分页查询,于是我规定了以ByPage结尾的SQL语句,我给他进行了分页查询,于是我写出了如下的代码

//args: 你需要mybatis传入什么参数给你 type :你需要拦截的对象  method=要拦截的方法
@Intercepts(@Signature(type = StatementHandler.class,method ="prepare",args = {Connection.class,Integer.class}))
public class PagePlugin implements Interceptor {
  
  String databaseType = "";
  String pageSqlId = "";
  
  //插件的核心处理方法。
  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
    //固定的写法
    MetaObject metaObject = MetaObject.forObject(
      statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY,SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,new 
      DefaultReflectorFactory());
    //通过属性名一个个获取
    String sqlId = (String)metaObject.getValue("delegate.mappedStatement.id");
    if (sqlId.matches(pageSqlId)){
      ParameterHandler parameterHandler = statementHandler.getParameterHandler();
      //原来应该执行的sql
      String sql = statementHandler.getBoundSql().getSql();
      //执行一条count语句
      //拿到数据库连接对象
      Connection connection = (Connection) invocation.getArgs()[0];
      String countSql = "select count(0) from ("+sql+") a";
      System.out.println(countSql);
      //渲染参数
      PreparedStatement preparedStatement = connection.prepareStatement(countSql);
      //条件交给mybatis
      parameterHandler.setParameters(preparedStatement);
      ResultSet resultSet = preparedStatement.executeQuery();
      int count =0;
      if (resultSet.next()) {
        count = resultSet.getInt(1);
      }
      resultSet.close();
      preparedStatement.close();
    }
  }
  
  @Override
  public Object plugin(Object target) {
    //固定的写法,第二个参数表示要代理的对象
    return Plugin.wrap(target,this);
  }
  
  //要传给这个插件的配置信息
  @Override
  public void setProperties(Properties properties) {
    //从主配置文件中读取对应的配置
    this.databaseType = properties.getProperty("databaseType");
    this.pageSqlId = properties.getProperty("pageSqlId");
  }
}

经过上面的代码,我们就执行了count的语句,接下来就是进行我们的重写sql select * from admin limit start,limit,这个分页参数必须要传给我们,因为我们分页的插件并不知道你要显示那一页,每页显示多少条数据。于是我们在接口中传入一个参数Map用来接收分页参数,同时查询的时候只需要传入当前页还有每页显示的数据。总页数,还有一些其他的变量需要计算,所以我们这儿写了一个分页的工具类。具体的代码如下:

public interface PageMapper {
  List<Admin> selectByPage(Map<String,Object> page);
}
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="dao.PageMapper">
  <select id="selectByPage" resultType="entity.Admin" parameterType="map">
    select * from admin
  </select>
</mapper>
public class PageUtil {
  private int page;
  private int limit;
  private int count;
  private int start;
  
  public PageUtil() {
  }
  
  public PageUtil(int page, int limit) {
    this.page = page;
    this.limit = limit;
    this.start = (page - 1) * limit;
  }

  public int getPage() {
    return page;
  }

  public void setPage(int page) {
    this.page = page;
  }

  public int getLimit() {
    return limit;
  }

  public void setLimit(int limit) {
    this.limit = limit;
  }

  public int getCount() {
    return count;
  }

  public void setCount(int count) {
    this.count = count;
  }

  public int getStart() {
    return start;
  }

  public void setStart(int start) {
    this.start = start;
  }
}

有了这个工具类。我们就可以写出对应的分页的SQL语句了,于是我写出下面的代码

//args : 你需要mybatis传入什么参数给你 type :你需要拦截的对象  method=要拦截的方法
@Intercepts(@Signature(type = StatementHandler.class,method ="prepare",args = {Connection.class,Integer.class}))
public class PagePlugin implements Interceptor {
  String databaseType = "";
  String pageSqlId = "";
  
  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
    MetaObject metaObject = MetaObject.forObject(
      statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY,SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,new 
      DefaultReflectorFactory());
    String sqlId = (String)metaObject.getValue("delegate.mappedStatement.id");
    //判断一下是否是分页
    //<!--第一步 执行一条count语句-->
    //1.1 拿到连接
    //1.2 预编译SQL语句 拿到绑定的sql语句
    //1.3 执行 count语句   怎么返回你执行count的结果

    //<!--第二步 重写sql select * from admin limit start,limit -->
    //2.1 怎么知道 start 和limit
    //2.2 拼接start 和limit
    //2.3 替换原来绑定sql
    //拿到原来应该执行的sql
    if (sqlId.matches(pageSqlId)){
      ParameterHandler parameterHandler = statementHandler.getParameterHandler();
      //原来应该执行的sql
      String sql = statementHandler.getBoundSql().getSql();
      //执行一条count语句
      //拿到数据库连接对象
      Connection connection = (Connection) invocation.getArgs()[0];
      String countSql = "select count(0) from ("+sql+") a";
      System.out.println(countSql);
      //渲染参数
      PreparedStatement preparedStatement = connection.prepareStatement(countSql);
      //条件交给mybatis
      parameterHandler.setParameters(preparedStatement);
      ResultSet resultSet = preparedStatement.executeQuery();
      int count =0;
      if (resultSet.next()) {
        count = resultSet.getInt(1);
      }
      resultSet.close();
      preparedStatement.close();
      //获得你传进来的参数对象
      Map<String, Object> parameterObject = (Map<String, Object>) parameterHandler.getParameterObject();
      //limit  page
      PageUtil pageUtil = (PageUtil) parameterObject.get("page");
      //limit 1 ,10  十条数据   总共可能有100   count 要的是 后面的100
      pageUtil.setCount(count);
      
      //拼接分页语句(limit) 并且修改mysql本该执行的语句
      String pageSql = getPageSql(sql, pageUtil);
      metaObject.setValue("delegate.boundSql.sql",pageSql);
      System.out.println(pageSql);
    }
    //推进拦截器调用链
    return invocation.proceed();
  }

  public String getPageSql(String sql,PageUtil pageUtil){
    if(databaseType.equals("mysql")){
      return sql+" limit "+pageUtil.getStart()+","+pageUtil.getLimit();
    }else if(databaseType.equals("oracle")){
      //拼接oracle的分语句
    }

    return sql+" limit "+pageUtil.getStart()+","+pageUtil.getLimit();
  }

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

  @Override
  public void setProperties(Properties properties) {
    this.databaseType = properties.getProperty("databaseType");
    this.pageSqlId = properties.getProperty("pageSqlId");
  }
}

至此整个分页的插件就写完了。我们来测试一下,记住我们要在s的主配置中文件,注册这个插件,同时配置对应的配置项。具体的如下:

<plugins>
  <plugin interceptor="plugin.PagePlugin">
    <property name="databaseType" value="mysql"/>
    <property name="pageSqlId" value=".*ByPage$"/>
  </plugin>
</plugins>

于是我们写出如下测试类。具体的代码如下:

public class Main {
  public static void main(String[] args) throws IOException {
    String resource = "mybatis.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    SqlSession sqlSession = sessionFactory.openSession();
    PageUtil pageUtil = new PageUtil(2, 10);
    Map<String, Object> page = new HashMap<>();
    page.put("page", pageUtil);
    PageMapper mapper = sqlSession.getMapper(PageMapper.class);
    mapper.selectByPage(page);
    sqlSession.close();
  }
}

运行结果如下图:

在这里插入图片描述

至此整个分页的插件的书写就结束了。下篇博客会讲下mybatis的日志。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值