当前位置:   article > 正文

分页插件PageHelper工作原理_pagehelper底层原理

pagehelper底层原理

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

1. 原理概述

PageHelper是MyBatis的一个插件,内部实现了一个PageInterceptor拦截器。Mybatis会加载这个拦截器到拦截器链中。在我们使用过程中先使用PageHelper.startPage这样的语句在当前线程上下文中设置一个ThreadLocal变量,再利用PageInterceptor这个分页拦截器拦截,从ThreadLocal中拿到分页的信息,如果有分页信息拼装分页SQL(limit语句等)进行分页查询,最后再把ThreadLocal中的东西清除掉。

2. 使用注意事项

PageHelper使用了ThreadLocal保存分页参数,分页参数和线程是绑定的。因此我们需要保证PageHelper 的startPage调用后紧跟 MyBatis 查询方法,这就是安全的。因为 PageHelper 在 finally 代码段中自动清除了 ThreadLocal 存储的对象。

如果代码在进入Executor前发生异常,就会导致线程不可用,这属于人为Bug(例如接口方法和XML中的不匹配,导致找不到MapppedStatement时)。这种情况由于线程不可用,也不会导致 ThreadLocal参数被错误使用。

但是如果写出以下代码,就是不安全的用法:

  1. PageHelper.startPage(1, 10);
  2. List<Country> list;
  3. if(param1 != null){
  4. list = countryMapper.selectIf(param1);
  5. } else {
  6. list = new ArrayList<Country>();
  7. }

这种情况下由于param1存在null的情况,就会导致PageHelper产生了一个分页参数,但是没有被消费,也没有被清理。这个参数就会一直保存在ThreadLocal中。可能导致其它不该分页的方法去消费了这个参数,莫名其妙的做了分页。

此外如果考虑到发生异常,可以加一个finally块,手动清理参数。

3. 使用过程源码分析

3.1 PageHelper.startPage

我们先来看PageHelper.startPage这个静态方法

  1. /**
  2. * 开始分页
  3. *
  4. * @param pageNum 页码
  5. * @param pageSize 每页显示数量
  6. * @param count 是否进行count查询
  7. * @param reasonable 分页合理化,null时用默认配置
  8. * @param pageSizeZero true且pageSize=0时返回全部结果,false时分页,null时用默认配置
  9. */
  10. public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
  11. Page<E> page = new Page<E>(pageNum, pageSize, count);
  12. page.setReasonable(reasonable);
  13. page.setPageSizeZero(pageSizeZero);
  14. //当已经执行过orderBy的时候
  15. Page<E> oldPage = getLocalPage();
  16. if (oldPage != null && oldPage.isOrderByOnly()) {
  17. page.setOrderBy(oldPage.getOrderBy());
  18. }
  19. setLocalPage(page);
  20. return page;
  21. }

上面的代码有个比较关键的地方setLocalPage(page),方法是这样的:

  1. /**
  2. * 设置 Page 参数
  3. *
  4. * @param page
  5. */
  6. protected static void setLocalPage(Page page) {
  7. LOCAL_PAGE.set(page);
  8. }

再来看LOCAL_PAGE的定义

protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();

终于明白了,是基于ThreadLocal,但是还没完,我们只看到了set的地方,却没有看到remove的地方,com.github.pagehelper.page.PageMethod类里有个clearPage方法:

  1. /**
  2. * 移除本地变量
  3. */
  4. public static void clearPage() {
  5. LOCAL_PAGE.remove();
  6. }

清除本地线程变量的就是这个clearPage方法,PageHelper在PageInterceptor的finally代码段中调用clearPage方法清除了ThreadLocal存储的对象。

3.2 PageInterceptor

我们再来看一下PageInterceptor拦截器是如何使用的。

3.2.1 加载过程

首先我们看拦截器是如何被加载的,查看SqlSessionFactoryBuilder类的build方法

在这里插入图片描述

进入pluginElement方法

  1. private void pluginElement(XNode parent) throws Exception {
  2. if (parent != null) {
  3. for (XNode child : parent.getChildren()) {
  4. // 获取到内容:com.github.pagehelper.PageHelper
  5. String interceptor = child.getStringAttribute("interceptor");
  6. // 获取配置的属性信息
  7. Properties properties = child.getChildrenAsProperties();
  8. // 创建的拦截器实例
  9. Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
  10. // 将属性和拦截器绑定
  11. interceptorInstance.setProperties(properties);
  12. // 这个方法需要进入查看
  13. configuration.addInterceptor(interceptorInstance);
  14. }
  15. }
  16. }

addInterceptor就是加载拦截器的方法

  1. public void addInterceptor(Interceptor interceptor) {
  2. // 将拦截器添加到了 拦截器链中 而拦截器链本质上就是一个List有序集合
  3. interceptorChain.addInterceptor(interceptor);
  4. }

在这里插入图片描述

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

3.2.2 PageHelper定义的拦截信息

我们来看看PageHepler的源代码片段

  1. @SuppressWarnings("rawtypes")
  2. @Intercepts(
  3. @Signature(
  4. type = Executor.class,
  5. method = "query",
  6. args = {MappedStatement.class
  7. , Object.class
  8. , RowBounds.class
  9. , ResultHandler.class
  10. }))
  11. public class PageHelper implements Interceptor {
  12. //sql工具类
  13. private SqlUtil sqlUtil;
  14. //属性参数信息
  15. private Properties properties;
  16. //配置对象方式
  17. private SqlUtilConfig sqlUtilConfig;
  18. //自动获取dialect,如果没有setProperties或setSqlUtilConfig,也可以正常进行
  19. private boolean autoDialect = true;
  20. //运行时自动获取dialect
  21. private boolean autoRuntimeDialect;
  22. //多数据源时,获取jdbcurl后是否关闭数据源
  23. private boolean closeConn = true;

这里可以看到通过注解,定义的是拦截 Executor对象中的query(MappedStatement ms,Object o,RowBounds ob ResultHandler rh)这个方法。

3.2.3 Executor中发生的事情

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

  1. public SqlSession openSession() {
  2. return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
  3. }

在这里插入图片描述

再来看newExecutor中做了什么处理

在这里插入图片描述

这里循环调用了所有拦截器

在这里插入图片描述

增强Executor实例

在这里插入图片描述

在这里插入图片描述

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

  1. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  2. try {
  3. Set<Method> methods = signatureMap.get(method.getDeclaringClass());
  4. // 如果是定义的拦截的方法 就执行intercept方法
  5. if (methods != null && methods.contains(method)) {
  6. // 进入查看 该方法增强
  7. return interceptor.intercept(new Invocation(target, method, args));
  8. }
  9. // 不是需要拦截的方法 直接执行
  10. return method.invoke(target, args);
  11. } catch (Exception e) {
  12. throw ExceptionUtil.unwrapThrowable(e);
  13. }
  14. }

3.2.4 selectList方法

当我们调用例如session.selectList("com.bobo.UserMapper.query")方法时,最终是交由Executor类的方法来处理

在这里插入图片描述

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

  1. /**
  2. * Mybatis拦截器方法
  3. *
  4. * @param invocation 拦截器入参
  5. * @return 返回执行结果
  6. * @throws Throwable 抛出异常
  7. */
  8. public Object intercept(Invocation invocation) throws Throwable {
  9. if (autoRuntimeDialect) {
  10. SqlUtil sqlUtil = getSqlUtil(invocation);
  11. return sqlUtil.processPage(invocation);
  12. } else {
  13. if (autoDialect) {
  14. initSqlUtil(invocation);
  15. }
  16. return sqlUtil.processPage(invocation);
  17. }
  18. }

进入sqlUtil.processPage(invocation);方法,这里会从ThreadLocal中获取分页信息

  1. /**
  2. * Mybatis拦截器方法
  3. *
  4. * @param invocation 拦截器入参
  5. * @return 返回执行结果
  6. * @throws Throwable 抛出异常
  7. */
  8. private Object _processPage(Invocation invocation) throws Throwable {
  9. final Object[] args = invocation.getArgs();
  10. Page page = null;
  11. //支持方法参数时,会先尝试获取Page
  12. if (supportMethodsArguments) {
  13. // 从线程本地变量中获取Page信息,就是我们刚刚设置的
  14. page = getPage(args);
  15. }
  16. //分页信息
  17. RowBounds rowBounds = (RowBounds) args[2];
  18. //支持方法参数时,如果page == null就说明没有分页条件,不需要分页查询
  19. if ((supportMethodsArguments && page == null)
  20. //当不支持分页参数时,判断LocalPage和RowBounds判断是否需要分页
  21. || (!supportMethodsArguments && SqlUtil.getLocalPage() == null && rowBounds == RowBounds.DEFAULT)) {
  22. return invocation.proceed();
  23. } else {
  24. //不支持分页参数时,page==null,这里需要获取
  25. if (!supportMethodsArguments && page == null) {
  26. page = getPage(args);
  27. }
  28. // 进入查看
  29. return doProcessPage(invocation, page, args);
  30. }
  31. }
  1. /**
  2. * Mybatis拦截器方法
  3. *
  4. * @param invocation 拦截器入参
  5. * @return 返回执行结果
  6. * @throws Throwable 抛出异常
  7. */
  8. private Page doProcessPage(Invocation invocation, Page page, Object[] args) throws Throwable {
  9. //保存RowBounds状态
  10. RowBounds rowBounds = (RowBounds) args[2];
  11. //获取原始的ms
  12. MappedStatement ms = (MappedStatement) args[0];
  13. //判断并处理为PageSqlSource
  14. if (!isPageSqlSource(ms)) {
  15. processMappedStatement(ms);
  16. }
  17. //设置当前的parser,后面每次使用前都会set,ThreadLocal的值不会产生不良影响
  18. ((PageSqlSource)ms.getSqlSource()).setParser(parser);
  19. try {
  20. //忽略RowBounds-否则会进行Mybatis自带的内存分页
  21. args[2] = RowBounds.DEFAULT;
  22. //如果只进行排序 或 pageSizeZero的判断
  23. if (isQueryOnly(page)) {
  24. return doQueryOnly(page, invocation);
  25. }
  26. //简单的通过total的值来判断是否进行count查询
  27. if (page.isCount()) {
  28. page.setCountSignal(Boolean.TRUE);
  29. //替换MS
  30. args[0] = msCountMap.get(ms.getId());
  31. //查询总数
  32. Object result = invocation.proceed();
  33. //还原ms
  34. args[0] = ms;
  35. //设置总数
  36. page.setTotal((Integer) ((List) result).get(0));
  37. if (page.getTotal() == 0) {
  38. return page;
  39. }
  40. } else {
  41. page.setTotal(-1l);
  42. }
  43. //pageSize>0的时候执行分页查询,pageSize<=0的时候不执行相当于可能只返回了一个count
  44. if (page.getPageSize() > 0 &&
  45. ((rowBounds == RowBounds.DEFAULT && page.getPageNum() > 0)
  46. || rowBounds != RowBounds.DEFAULT)) {
  47. //将参数中的MappedStatement替换为新的qs
  48. page.setCountSignal(null);
  49. // 重点是查看该方法
  50. BoundSql boundSql = ms.getBoundSql(args[1]);
  51. args[1] = parser.setPageParameter(ms, args[1], boundSql, page);
  52. page.setCountSignal(Boolean.FALSE);
  53. //执行分页查询
  54. Object result = invocation.proceed();
  55. //得到处理结果
  56. page.addAll((List) result);
  57. }
  58. } finally {
  59. ((PageSqlSource)ms.getSqlSource()).removeParser();
  60. }
  61. //返回结果
  62. return page;
  63. }

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

  1. @Override
  2. protected BoundSql getPageBoundSql(Object parameterObject) {
  3. String tempSql = sql;
  4. String orderBy = PageHelper.getOrderBy();
  5. if (orderBy != null) {
  6. tempSql = OrderByParser.converToOrderBySql(sql, orderBy);
  7. }
  8. tempSql = localParser.get().getPageSql(tempSql);
  9. return new BoundSql(configuration, tempSql, localParser.get().getPageParameterMapping(configuration, original.getBoundSql(parameterObject)), parameterObject);
  10. }

在这里插入图片描述

MySQL数据库分页实现

在这里插入图片描述

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/88016
推荐阅读
相关标签
  

闽ICP备14008679号