多年不必PageHelper了,最近新入职的公司,采纳了此工具集成的框架,作为一个独立紧急我的项目开发的根底。我的项目开发起来,还是手到擒来的,然而没想到,最终测试的时候,深深的给我上了一课。
我的我的项目产生了哪些奇葩景象?
所有的问题都要从我承受的我的项目开始说起, 在开发这个我的项目的过程中,产生了各种奇葩的事件, 上面我简略说给你们听听:
账号反复注册?
你必定在想这是什么意思? 就是字面意思,曾经注册的账号,能够再次注册胜利!!!
else if (UserConstants.NOT_UNIQUE.equals(userService.checkUserNameUnique(username))||"匿名用户".equals(username)){ // 注册用户已存在 msg = "注册用户'" + username + "'失败";}
如上所示: checkUserNameUnique(username)
用来验证数据库是否存在用户名:
<select id="checkUserNameUnique" parameterType="String" resultType="int"> select count(1) from sys_user where user_name = #{userName} limit 1</select>
失常来说,是不会有问题的,那么起因咱们前面讲,接着看下一个问题。
举荐一个开源收费的 Spring Boot 实战我的项目:
https://github.com/javastacks/spring-boot-best-practice
查问全副分类的下拉列表只能查出5条数据?
如上所示,明明有十多个后果,怎么只能返回5个?我也没有增加分页参数啊?
置信用过PageHelper的同学曾经晓得问题出在哪里了。
批改用户明码报错?
当管理员在后盾界面重置用户的明码的时候,竟然报错了?
报错信息清晰的通知了我:sql语句异样,update语句不意识 “Limit 5”
到此为止,报错信息曾经通知了我,我的sql被拼接了该死的“limit”分页参数。
小结
下面提到的几个只是冰山一角,在我应用的过程中,还有各种波及到sql的中央,会因为这个分页参数导致的问题,我能够分为两种:
1)间接导致报错的:明确报错起因的
比方insert、update语句等,不反对limit,会间接报错。
2)导致业务逻辑谬误,然而代码没有谬误提醒
如我下面提到的用户能够反复注册,却没有报错,理论在代码当中是有报错的,然而以后办法对异样进行了throw,最终被全局异样捕捉了。
不分页的sql被拼接了limit,导致没有报错,然而数据返回量谬误。
异样不是每次呈现,是有肯定纪律的,然而触发几率较高,起因在前面会逐步脱出。
PageHelper是怎么做到下面的问题的?
PageHelper应用
我这里只解说我的项目基于的框架的应用形式。
代码如下:
@GetMapping("/cms/cmsEssayList")public TableDataInfo cmsEssayList(CmsBlog cmsBlog) { //状态为公布 cmsBlog.setStatus("1"); startPage(); List<CmsBlog> list = cmsBlogService.selectCmsBlogList(cmsBlog); return getDataTable(list);}
应用起来还是很简略的,通过 startPage()
指定分页参数,通过getDataTable(list)
对后果数据封装成分页的格局。
有些同学会问,这也没没传分页参数啊,并且实体类当中也没有,这就是比拟有意思的点,下一小结就来聊聊源码。
startPage()干啥了?
protected void startPage(){ // 通过request去获取前端传递的分页参数,不需控制器要显示接管 PageDomain pageDomain = TableSupport.buildPageRequest(); Integer pageNum = pageDomain.getPageNum(); Integer pageSize = pageDomain.getPageSize(); if (StringUtils.isNotNull(pageNum) && StringUtils.isNotNull(pageSize)) { String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy()); Boolean reasonable = pageDomain.getReasonable(); // 真正应用pageHelper进行分页的地位 PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable); }}
PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable)
的参数别离是:
pageNum
:页数pageSize
:每页数据量orderBy
:排序reasonable
:分页合理化,对于不合理的分页参数主动解决,比方传递pageNum是小于0,会默认设置为1.
持续跟踪,间断点击startpage
构造方法达到如下地位:
/** * 开始分页 * * @param pageNum 页码 * @param pageSize 每页显示数量 * @param count 是否进行count查问 * @param reasonable 分页合理化,null时用默认配置 * @param pageSizeZero true且pageSize=0时返回全副后果,false时候页,null时用默认配置 */public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) { Page<E> page = new Page<E>(pageNum, pageSize, count); page.setReasonable(reasonable); page.setPageSizeZero(pageSizeZero); // 1、获取本地分页 Page<E> oldPage = getLocalPage(); if (oldPage != null && oldPage.isOrderByOnly()) { page.setOrderBy(oldPage.getOrderBy()); } // 2、设置本地分页 setLocalPage(page); return page;}
达到起点地位了,别离是:getLocalPage()
和setLocalPage(page)
,别离来看下:
getLocalPage()
进入办法:
/** * 获取 Page 参数 * * @return */public static <T> Page<T> getLocalPage() { return LOCAL_PAGE.get();}
看看常量LOCAL_PAGE
是个什么路数?
protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
好家伙,是ThreadLocal,学过java根底的都晓得吧,独属于每个线程的本地缓存对象。
当一个申请来的时候,会获取持有以后申请的线程的ThreadLocal,调用LOCAL_PAGE.get()
,查看以后线程是否有未执行的分页配置。
setLocalPage(page)
此办法不言而喻,设置线程的分页配置:
protected static void setLocalPage(Page page) { LOCAL_PAGE.set(page);}
小结
通过后面的剖析,咱们发现,问题仿佛就是这个ThreadLocal导致的。
是否在应用完之后没有进行清理?导致下一次此线程再次解决申请时,还在应用之前的配置?
咱们带着疑难,看看mybatis时如何应用pageHelper的。
mybatis应用pageHelper剖析
咱们须要关注的就是mybatis在何时应用的这个ThreadLocal,也就是何时将分页餐数获取到的。
后面提到过,通过PageHelper的startPage()
办法进行page缓存的设置,当程序执行sql接口mapper的办法时,就会被拦截器PageInterceptor
拦挡到。
PageHelper其实就是mybatis的分页插件,其实现原理就是通过拦截器的形式,pageHelper通PageInterceptor
实现分页成果,咱们只关注intercept办法:
@Overridepublic Object intercept(Invocation invocation) throws Throwable { try { Object[] args = invocation.getArgs(); MappedStatement ms = (MappedStatement) args[0]; Object parameter = args[1]; RowBounds rowBounds = (RowBounds) args[2]; ResultHandler resultHandler = (ResultHandler) args[3]; Executor executor = (Executor) invocation.getTarget(); CacheKey cacheKey; BoundSql boundSql; // 因为逻辑关系,只会进入一次 if (args.length == 4) { //4 个参数时 boundSql = ms.getBoundSql(parameter); cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql); } else { //6 个参数时 cacheKey = (CacheKey) args[4]; boundSql = (BoundSql) args[5]; } checkDialectExists(); //对 boundSql 的拦挡解决 if (dialect instanceof BoundSqlInterceptor.Chain) { boundSql = ((BoundSqlInterceptor.Chain) dialect).doBoundSql(BoundSqlInterceptor.Type.ORIGINAL, boundSql, cacheKey); } List resultList; //调用办法判断是否须要进行分页,如果不须要,间接返回后果 if (!dialect.skip(ms, parameter, rowBounds)) { //判断是否须要进行 count 查问 if (dialect.beforeCount(ms, parameter, rowBounds)) { //查问总数 Long count = count(executor, ms, parameter, rowBounds, null, boundSql); //解决查问总数,返回 true 时持续分页查问,false 时间接返回 if (!dialect.afterCount(count, parameter, rowBounds)) { //当查问总数为 0 时,间接返回空的后果 return dialect.afterPage(new ArrayList(), parameter, rowBounds); } } resultList = ExecutorUtil.pageQuery(dialect, executor, ms, parameter, rowBounds, resultHandler, boundSql, cacheKey); } else { //rowBounds用参数值,不应用分页插件解决时,依然反对默认的内存分页 resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql); } return dialect.afterPage(resultList, parameter, rowBounds); } finally { if(dialect != null){ dialect.afterAll(); } }}
如上所示是intecept的全副代码,咱们上面只关注几个起点地位:
设置分页:dialect.skip(ms, parameter, rowBounds)
此处的skip办法进行设置分页参数,外部调用办法:
Page page = pageParams.getPage(parameterObject, rowBounds);
持续跟踪getPage()
,发现此办法的第一行就获取了ThreadLocal的值:
Page page = PageHelper.getLocalPage();
统计数量:dialect.beforeCount(ms, parameter, rowBounds)
咱们都晓得,分页须要获取记录总数,所以,这个拦截器会在分页前先进行count操作。
如果count为0,则间接返回,不进行分页:
//解决查问总数,返回 true 时持续分页查问,false 时间接返回if (!dialect.afterCount(count, parameter, rowBounds)) { //当查问总数为 0 时,间接返回空的后果 return dialect.afterPage(new ArrayList(), parameter, rowBounds);}
afterPage其实是对分页后果的封装办法,即便不分页,也会执行,只不过返回空列表。
分页:ExecutorUtil.pageQuery
在解决完count办法后,就是真正的进行分页了:
resultList = ExecutorUtil.pageQuery(dialect, executor, ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
此办法在执行分页之前,会判断是否执行分页,根据就是后面咱们通过ThreadLocal的获取的page。
当然,不分页的查问,以及新增和更新不会走到这个办法当中。
非分页:executor.query
而是会走到上面的这个分支:
resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
咱们能够思考一下,如果ThreadLoad在应用后没有被革除,当执行非分页的办法时,那么就会将Limit拼接到sql前面。
为什么不分也得也会拼接?咱们回头看下后面提到的dialect.skip(ms, parameter, rowBounds)
:
如上所示,只有page被获取到了,那么这个sql,就会走后面提到的ExecutorUtil.pageQuery
分页逻辑,最终导致呈现不可意料的状况。
其实PageHelper对于分页后的ThreaLocal是有革除解决的。
革除TheadLocal
在intercept办法的最初,会在sql办法执行实现后,清理page缓存:
finally { if(dialect != null){ dialect.afterAll(); }}
看看这个afterAll()
办法:
@Overridepublic void afterAll() { //这个办法即便不分页也会被执行,所以要判断 null AbstractHelperDialect delegate = autoDialect.getDelegate(); if (delegate != null) { delegate.afterAll(); autoDialect.clearDelegate(); } clearPage();}
只关注 clearPage()
:
/** * 移除本地变量 */public static void clearPage() { LOCAL_PAGE.remove();}
小结
到此为止,对于PageHelper的应用形式就解说完了。
整体看下来,仿佛不会存在什么问题,然而咱们能够思考集中极其状况:
如果应用了startPage()
,然而没有执行对应的sql,那么就表明,以后线程ThreadLocal被设置了分页参数,可是没有被应用,当下一个应用此线程的申请来时,就会呈现问题。
如果程序在执行sql前,产生异样了,就没方法执行finally当中的clearPage()
办法,也会造成线程的ThreadLocal被净化。
所以,官网给咱们的倡议,在应用PageHelper进行分页时,执行sql的代码要紧跟startPage()
办法。
除此之外,咱们能够手动调用clearPage()
办法,在存在问题的办法之前。
须要留神:不要分页的办法前手动调用clearPage,将会导致你的分页呈现问题。
还有人问为什么不是每次申请都出错?
这个其实取决于咱们启动服务所应用的容器,比方tomcat,在其外部解决申请是通过线程池的形式。甚至当初的很多容器是基于netty的,都是通过线程池,复用线程来减少服务的并发量。
假如线程1持有没有被革除的page参数,一直调用同一个办法,前面两个申请应用的是线程2和线程3没有问题,再一个申请轮到线程1了,此时就会呈现问题了。
总结
对于PageHelper的介绍就这么多,真的是折磨我好几天,要不是我的项目紧急,来不及替换,我肯定不会应用这个组件。
莫名其妙的就会有个办法呈现问题,一通排查,发现都是这个PageHelper导致的。尽管我曾经全局搜寻应用的中央,保障startPage()
后紧跟sql命令,然而依然有嫌犯叛逃,只能在有问题的办法应用clearPage()
来打补丁。
尽管PageHelper给我带来一些困扰,消耗了肯定的工夫,然而定位问题的过程中,也学习了mybatis和pagehepler的实现形式,对于酷爱源码浏览的同学来说还是有肯定的晋升的。
原文:juejin.cn/post/7125356642366914596
更多文章举荐:
1.Spring Boot 3.x 教程,太全了!
2.2,000+ 道 Java面试题及答案整顿(2024最新版)
3.收费获取 IDEA 激活码的 7 种形式(2024最新版)
感觉不错,别忘了顺手点赞+转发哦!