2026/2/19 22:57:53
网站建设
项目流程
语言教学网站建设课程总结,上海涛飞专业网站建设,创业做网站 优帮云,东莞市设计公司多年不用PageHelper了#xff0c;最近新入职的公司#xff0c;采用了此工具集成的框架#xff0c;作为一个独立紧急项目开发的基础。项目开发起来#xff0c;还是手到擒来的#xff0c;但是没想到#xff0c;最终测试的时候#xff0c;深深的给我上了一课。我的项目发生…多年不用PageHelper了最近新入职的公司采用了此工具集成的框架作为一个独立紧急项目开发的基础。项目开发起来还是手到擒来的但是没想到最终测试的时候深深的给我上了一课。我的项目发生了哪些奇葩现象?一切的问题都要从我接受的项目开始说起, 在开发这个项目的过程中,发生了各种奇葩的事情, 下面我简单说给你们听听:账号重复注册?你肯定在想这是什么意思? 就是字面意思,已经注册的账号,可以再次注册成功!!!else if (UserConstants.NOT_UNIQUE.equals(userService.checkUserNameUnique(username)) ||匿名用户.equals(username)){ // 注册用户已存在 msg 注册用户 username 失败; }如上所示:checkUserNameUnique(username)用来验证数据库是否存在用户名:select idcheckUserNameUnique parameterTypeString resultTypeint select count(1) from sys_user where user_name #{userName} limit 1 /select正常来说,是不会有问题的,那么原因我们后面讲,接着看下一个问题。查询全部分类的下拉列表只能查出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(); ListCmsBlog 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且pageSize0时返回全部结果false时分页,null时用默认配置 */ public static E PageE startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) { PageE page new PageE(pageNum, pageSize, count); page.setReasonable(reasonable); page.setPageSizeZero(pageSizeZero); // 1、获取本地分页 PageE 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 PageT getLocalPage() { return LOCAL_PAGE.get(); }看看常量LOCAL_PAGE是个什么路数protected static final ThreadLocalPage LOCAL_PAGE new ThreadLocalPage();好家伙是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方法Override public 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方法():Override public 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的实现方式对于热爱源码阅读的同学来说还是有一定的提升的。