百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

面试官:说说MyBatis分页插件(PageHelper)工作原理和配置过程?

mhr18 2025-03-05 18:32 15 浏览 0 评论

数据分页功能是我们软件系统中必备的功能,在持久层使用mybatis的情况下,pageHelper来实现后台分页则是我们常用的一个选择,所以本文专门类介绍下。

PageHelper原理

相关依赖


    org.mybatis
    mybatis
    3.2.8


    com.github.pagehelper
    pagehelper
    1.2.15

1.添加plugin

要使用PageHelper首先在mybatis的全局配置文件中配置。如下:





    
        
        
            
            
            
            
            
            
            
            
            
            
            
            
            
            
            
            
            
            
            
            
            
            
        
    

2.加载过程

我们通过如下几行代码来演示过程

// 获取配置文件
InputStream inputStream = Resources.getResourceAsStream("mybatis/mybatis-config.xml");
// 通过加载配置文件获取SqlSessionFactory对象
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
// 获取SqlSession对象
SqlSession session = factory.openSession();
PageHelper.startPage(1, 5);
session.selectList("com.bobo.UserMapper.query");

加载配置文件我们从这行代码开始

new SqlSessionFactoryBuilder().build(inputStream);
public SqlSessionFactory build(InputStream inputStream) {
   return build(inputStream, null, null);
 }



private void pluginElement(XNode parent) throws Exception {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      // 获取到内容:com.github.pagehelper.PageHelper
      String interceptor = child.getStringAttribute("interceptor");
      // 获取配置的属性信息
      Properties properties = child.getChildrenAsProperties();
      // 创建的拦截器实例
      Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
      // 将属性和拦截器绑定
      interceptorInstance.setProperties(properties);
      // 这个方法需要进入查看
      configuration.addInterceptor(interceptorInstance);
    }
  }
}
public void addInterceptor(Interceptor interceptor) {
      // 将拦截器添加到了 拦截器链中 而拦截器链本质上就是一个List有序集合
    interceptorChain.addInterceptor(interceptor);
  }

小结:通过SqlSessionFactory对象的获取,我们加载了全局配置文件及映射文件同时还将配置的拦截器添加到了拦截器链中。

3.PageHelper定义的拦截信息

我们来看下PageHelper的源代码的头部定义

@SuppressWarnings("rawtypes")
@Intercepts(
    @Signature(
        type = Executor.class, 
        method = "query", 
        args = {MappedStatement.class
                , Object.class
                , RowBounds.class
                , ResultHandler.class
            }))
public class PageHelper implements Interceptor {
    //sql工具类
    private SqlUtil sqlUtil;
    //属性参数信息
    private Properties properties;
    //配置对象方式
    private SqlUtilConfig sqlUtilConfig;
    //自动获取dialect,如果没有setProperties或setSqlUtilConfig,也可以正常进行
    private boolean autoDialect = true;
    //运行时自动获取dialect
    private boolean autoRuntimeDialect;
    //多数据源时,获取jdbcurl后是否关闭数据源
    private boolean closeConn = true;
// 定义的是拦截 Executor对象中的
// query(MappedStatement ms,Object o,RowBounds ob ResultHandler rh)
// 这个方法
type = Executor.class, 
method = "query", 
args = {MappedStatement.class
        , Object.class
        , RowBounds.class
        , ResultHandler.class
    }))

PageHelper中已经定义了该拦截器拦截的方法是什么。

4.Executor

接下来我们需要分析下SqlSession的实例化过程中Executor发生了什么。我们需要从这行代码开始跟踪

SqlSession session = factory.openSession();
public SqlSession openSession() {
  return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}




增强Executor

到此我们明白了,Executor对象其实被我们生存的代理类增强了。invoke的代码为

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
    Set methods = signatureMap.get(method.getDeclaringClass());
    // 如果是定义的拦截的方法 就执行intercept方法
    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);
  }
}
/**
 * Mybatis拦截器方法
 *
 * @param invocation 拦截器入参
 * @return 返回执行结果
 * @throws Throwable 抛出异常
 */
public Object intercept(Invocation invocation) throws Throwable {
    if (autoRuntimeDialect) {
        SqlUtil sqlUtil = getSqlUtil(invocation);
        return sqlUtil.processPage(invocation);
    } else {
        if (autoDialect) {
            initSqlUtil(invocation);
        }
        return sqlUtil.processPage(invocation);
    }
}

该方法中的内容我们后面再分析。Executor的分析我们到此,接下来看下PageHelper实现分页的具体过程。

5.分页过程

接下来我们通过代码跟踪来看下具体的分页流程,我们需要分别从两行代码开始:

5.1 startPage

PageHelper.startPage(1, 5);
/**
 * 开始分页
 *
 * @param params
 */
public static  Page startPage(Object params) {
    Page page = SqlUtil.getPageFromObject(params);
    //当已经执行过orderBy的时候
    Page oldPage = SqlUtil.getLocalPage();
    if (oldPage != null && oldPage.isOrderByOnly()) {
        page.setOrderBy(oldPage.getOrderBy());
    }
    SqlUtil.setLocalPage(page);
    return page;
}
/**
 * 开始分页
 *
 * @param pageNum    页码
 * @param pageSize   每页显示数量
 * @param count      是否进行count查询
 * @param reasonable 分页合理化,null时用默认配置
 */
public static  Page startPage(int pageNum, int pageSize, boolean count, Boolean reasonable) {
    return startPage(pageNum, pageSize, count, reasonable, null);
}
/**
 * 开始分页
 *
 * @param offset 页码
 * @param limit  每页显示数量
 * @param count  是否进行count查询
 */
public static  Page offsetPage(int offset, int limit, boolean count) {
    Page page = new Page(new int[]{offset, limit}, count);
    //当已经执行过orderBy的时候
    Page oldPage = SqlUtil.getLocalPage();
    if (oldPage != null && oldPage.isOrderByOnly()) {
        page.setOrderBy(oldPage.getOrderBy());
    }
    // 这是重点!!!
    SqlUtil.setLocalPage(page);
    return page;
}
private static final ThreadLocal LOCAL_PAGE = new ThreadLocal();
// 将分页信息保存在ThreadLocal中 线程安全!
public static void setLocalPage(Page page) {
    LOCAL_PAGE.set(page);
}

5.2selectList方法

session.selectList("com.bobo.UserMapper.query");
public  List selectList(String statement) {
  return this.selectList(statement, null);
}

public  List selectList(String statement, Object parameter) {
  return this.selectList(statement, parameter, RowBounds.DEFAULT);
}


我们需要回到invoke方法中继续看

/**
 * Mybatis拦截器方法
 *
 * @param invocation 拦截器入参
 * @return 返回执行结果
 * @throws Throwable 抛出异常
 */
public Object intercept(Invocation invocation) throws Throwable {
    if (autoRuntimeDialect) {
        SqlUtil sqlUtil = getSqlUtil(invocation);
        return sqlUtil.processPage(invocation);
    } else {
        if (autoDialect) {
            initSqlUtil(invocation);
        }
        return sqlUtil.processPage(invocation);
    }
}

进入sqlUtil.processPage(invocation);方法

/**
 * Mybatis拦截器方法
 *
 * @param invocation 拦截器入参
 * @return 返回执行结果
 * @throws Throwable 抛出异常
 */
private Object _processPage(Invocation invocation) throws Throwable {
    final Object[] args = invocation.getArgs();
    Page page = null;
    //支持方法参数时,会先尝试获取Page
    if (supportMethodsArguments) {
        // 从线程本地变量中获取Page信息,就是我们刚刚设置的
        page = getPage(args);
    }
    //分页信息
    RowBounds rowBounds = (RowBounds) args[2];
    //支持方法参数时,如果page == null就说明没有分页条件,不需要分页查询
    if ((supportMethodsArguments && page == null)
            //当不支持分页参数时,判断LocalPage和RowBounds判断是否需要分页
            || (!supportMethodsArguments && SqlUtil.getLocalPage() == null && rowBounds == RowBounds.DEFAULT)) {
        return invocation.proceed();
    } else {
        //不支持分页参数时,page==null,这里需要获取
        if (!supportMethodsArguments && page == null) {
            page = getPage(args);
        }
        // 进入查看
        return doProcessPage(invocation, page, args);
    }
}
/**
  * Mybatis拦截器方法
  *
  * @param invocation 拦截器入参
  * @return 返回执行结果
  * @throws Throwable 抛出异常
  */
 private Page doProcessPage(Invocation invocation, Page page, Object[] args) throws Throwable {
     //保存RowBounds状态
     RowBounds rowBounds = (RowBounds) args[2];
     //获取原始的ms
     MappedStatement ms = (MappedStatement) args[0];
     //判断并处理为PageSqlSource
     if (!isPageSqlSource(ms)) {
         processMappedStatement(ms);
     }
     //设置当前的parser,后面每次使用前都会set,ThreadLocal的值不会产生不良影响
     ((PageSqlSource)ms.getSqlSource()).setParser(parser);
     try {
         //忽略RowBounds-否则会进行Mybatis自带的内存分页
         args[2] = RowBounds.DEFAULT;
         //如果只进行排序 或 pageSizeZero的判断
         if (isQueryOnly(page)) {
             return doQueryOnly(page, invocation);
         }

         //简单的通过total的值来判断是否进行count查询
         if (page.isCount()) {
             page.setCountSignal(Boolean.TRUE);
             //替换MS
             args[0] = msCountMap.get(ms.getId());
             //查询总数
             Object result = invocation.proceed();
             //还原ms
             args[0] = ms;
             //设置总数
             page.setTotal((Integer) ((List) result).get(0));
             if (page.getTotal() == 0) {
                 return page;
             }
         } else {
             page.setTotal(-1l);
         }
         //pageSize>0的时候执行分页查询,pageSize<=0的时候不执行相当于可能只返回了一个count if page.getpagesize> 0 &&
                 ((rowBounds == RowBounds.DEFAULT && page.getPageNum() > 0)
                         || rowBounds != RowBounds.DEFAULT)) {
             //将参数中的MappedStatement替换为新的qs
             page.setCountSignal(null);
             // 重点是查看该方法
             BoundSql boundSql = ms.getBoundSql(args[1]);
             args[1] = parser.setPageParameter(ms, args[1], boundSql, page);
             page.setCountSignal(Boolean.FALSE);
             //执行分页查询
             Object result = invocation.proceed();
             //得到处理结果
             page.addAll((List) result);
         }
     } finally {
         ((PageSqlSource)ms.getSqlSource()).removeParser();
     }

     //返回结果
     return page;
 }

进入 BoundSql boundSql = ms.getBoundSql(args[1])方法跟踪到PageStaticSqlSource类中的

@Override
protected BoundSql getPageBoundSql(Object parameterObject) {
    String tempSql = sql;
    String orderBy = PageHelper.getOrderBy();
    if (orderBy != null) {
        tempSql = OrderByParser.converToOrderBySql(sql, orderBy);
    }
    tempSql = localParser.get().getPageSql(tempSql);
    return new BoundSql(configuration, tempSql, localParser.get().getPageParameterMapping(configuration, original.getBoundSql(parameterObject)), parameterObject);
}

也可以看Oracle的分页实现

至此我们发现PageHelper分页的实现原来是在我们执行SQL语句之前动态的将SQL语句拼接了分页的语句,从而实现了从数据库中分页获取的过程。

关注公众号:java宝典

相关推荐

SpringBoot 各种分页查询方式详解(全网最全)

一、分页查询基础概念与原理1.1什么是分页查询分页查询是指将大量数据分割成多个小块(页)进行展示的技术,它是现代Web应用中必不可少的功能。想象一下你去图书馆找书,如果所有书都堆在一张桌子上,你很难...

《战场兄弟》全事件攻略 一般事件合同事件红装及隐藏职业攻略

《战场兄弟》全事件攻略,一般事件合同事件红装及隐藏职业攻略。《战场兄弟》事件奖励,事件条件。《战场兄弟》是OverhypeStudios制作发行的一款由xcom和桌游为灵感来源,以中世纪、低魔奇幻为...

LoadRunner(loadrunner录制不到脚本)

一、核心组件与工作流程LoadRunner性能测试工具-并发测试-正版软件下载-使用教程-价格-官方代理商的架构围绕三大核心组件构建,形成完整测试闭环:VirtualUserGenerator(...

Redis数据类型介绍(redis 数据类型)

介绍Redis支持五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及Zset(sortedset:有序集合)。1、字符串类型概述1.1、数据类型Redis支持...

RMAN备份监控及优化总结(rman备份原理)

今天主要介绍一下如何对RMAN备份监控及优化,这里就不讲rman备份的一些原理了,仅供参考。一、监控RMAN备份1、确定备份源与备份设备的最大速度从磁盘读的速度和磁带写的带度、备份的速度不可能超出这两...

备份软件调用rman接口备份报错RMAN-06820 ORA-17629 ORA-17627

一、报错描述:备份归档报错无法连接主库进行归档,监听问题12541RMAN-06820:WARNING:failedtoarchivecurrentlogatprimarydatab...

增量备份修复物理备库gap(增量备份恢复数据库步骤)

适用场景:主备不同步,主库归档日志已删除且无备份.解决方案:主库增量备份修复dg备库中的gap.具体步骤:1、停止同步>alterdatabaserecovermanagedstand...

一分钟看懂,如何白嫖sql工具(白嫖数据库)

如何白嫖sql工具?1分钟看懂。今天分享一个免费的sql工具,毕竟现在比较火的NavicatDbeaverDatagrip都需要付费才能使用完整功能。幸亏今天有了这款SQLynx,它不仅支持国内外...

「开源资讯」数据管理与可视化分析平台,DataGear 1.6.1 发布

前言数据齿轮(DataGear)是一款数据库管理系统,使用Java语言开发,采用浏览器/服务器架构,以数据管理为核心功能,支持多种数据库。它的数据模型并不是原始的数据库表,而是融合了数据库表及表间关系...

您还在手工打造增删改查代码么,该神器带你脱离苦海

作为Java开发程序,日常开发中,都会使用Spring框架,完成日常的功能开发;在相关业务系统中,难免存在各种增删改查的接口需求开发。通常来说,实现增删改查有如下几个方式:纯手工打造,编写各种Cont...

Linux基础知识(linux基础知识点及答案)

系统目录结构/bin:命令和应用程序。/boot:这里存放的是启动Linux时使用的一些核心文件,包括一些连接文件以及镜像文件。/dev:dev是Device(设备)的缩写,该目录...

PL/SQL 杂谈(二)(pl/sql developer使用)

承接(一)部分。我们从结构和功能这两个方面展示PL/SQL的关键要素。可以看看PL/SQL的优雅的代码。写出一个好的代码,就和文科生写出一篇优秀的作文一样,那么赏心悦目。1、与SQL的集成PL/S...

电商ERP系统哪个好用?(电商erp哪个好一点)

电商ERP系统哪个好用?做电商的,谁还没被ERP折腾过?有老板说:“我们早就上了ERP,订单、库存、财务全搞定,系统用得飞起。”也有运营吐槽:“系统是上了,可库存老不准,订单漏单错单天天有,财务对账还...

汽车检测线系统实例,看集中控制与PLC分布控制

PLC可编程控制器,上个世纪70年代初,为取代早期继电器控制线路,开始采取存储指令方式,完成顺序控制而设计的。开始仅有逻辑运算、计时、计数等简单功能。随着微处理的发展,PLC可编程能力日益提高,已经能...

苹果五件套成公司年会奖品主角,几大小技巧教你玩转苹果新品

钱江晚报·小时新闻记者张云山随着春节的临近,各家大公司的年会又将陆续上演。上周,各大游戏公司的年会大奖,苹果五件套又成了标配。在上海的游戏公司中,莉莉丝奖品列表拉得相当长,从特等奖到九等奖还包含了特...

取消回复欢迎 发表评论: