一、开发准备1、开发工具IntelliJIDEA2020.2.32。开发环境RedHatOpenJDK8u256ApacheMaven3.6.33。开发依赖SpringBootorg.springframework.bootspring-boot-starter-webMyBatisorg.mybatis.spring。bootmybatis-spring-boot-starter2.1.3PageHelpercom.github.pagehelperpagehelper-spring-boot-starter1.3.0二、技术文档1、基于SpringBootSpringBoot官方文档https://spring.io/projects/spring-bootSpringBoot中文社区https://springboot.io/2。基于MyBatisMyBatis官方文档https://mybatis.org/mybatis-3/zh/index.html3。集成PageHelperPageHelper开源仓库https://github.com/pagehelper/Mybatis-PageHelper3。应用说明1.基本使用在实际项目应用中,PageHelper的使用非常方便快捷。只需要PageInfo+PageHelper两个类就可以完成分页功能了。然而,往往这种最简单的集成使用方式,却被许多实际应用所采用。在现场,还没有得到充分的开发利用。接下来是我们最常见的用法:publicPageInfopage(RequestParamDtoparam){PageHelper.startPage(param.getPageNum(),param.getPageSize());Listlist=mapper.selectManySelective(param);PageInfopageInfo=(PageInfo)列表;returnpageInfo;}从某种程度上说,上面的写法确实符合PageHelper的使用规范:在集合查询之前使用PageHelper.startPage(pageNum,pageSize),中间不能穿插执行其他SQL,但是作为开发者,我们往往只有在追求完美和尽善尽美的道路上才能找到突破和机会。下面是一个合理规范的基本用法:publicPageInfopage(RequestParamDtoparam){returnPageHelper.startPage(param.getPageNum(),param.getPageSize()).doSelectPageInfo(()->list(param))}publicListlist(RequestParamDtoparam){returnmapper.selectManySelective(param);}FAQ1。为什么需要重新声明列表函数?答:往往在很多实际的业务应用场景中,基于大量数据的表格展示需求,进行分页查询。但是很多时候,比如:内部服务的相互调用,提供OpenAPI。即使在一些前后端分离联合调试的业务场景下,也需要非分页的集合查询接口来提供服务。另外,暂时抛开以上因素,我们可以按照上述写法来定义和规范一些东西。例如:分页和集合查询的分离解耦(解耦详见高级用法),分页请求request和response与实际业务参数分离(详见高级用法)等...2.doSelectPageInfo是什么?Answer:doSelectPageInfo是PageHelper.startPage()函数返回的默认Page实例的内置函数。此函数可用于通过Lambda形式的附加函数进行查询,而无需进行冗余的PageInfo和List转换。doSelectPageInfo参数是PageHelper内置的Function(ISelect)接口,达到转换PageInfo的目的。3、这种写法代码量好像挺多的?答:如①所述,在代码量上,没有进一步简化,但在某些业务场景下,当你已经有一个列表函数接口的情况下,是更直观的优化(见进阶使用)优化细节)。2.高级使用,先看代码,再说分析:importcom.github.pagehelper.PageHelper;importcom.github.pagehelper.PageInfo;importjava.util.List;/***@paramgenericrequest*@param通用响应*/publicinterfaceBaseService{/***分页查询**@paramparam请求参数DTO*@return分页集合*/defaultPageInfopage(PageParamparam){返回页面助手。startPage(param).doSelectPageInfo(()->list(param.getParam()));}/***集合查询**@paramparam查询参数*@return查询响应*/Listlist(Paramparam);可以看到BaseService可以作为全局Service公共接口的封装和声明。但是作为一个通用的分页接口,page函数使用接口专用的关键字default直接声明了page函数的方法体。importcom.github.pagehelper.IPage;importlombok.Data;importlombok.experimental.Accessors;@Data//使用lombok省略冗余代码其实应该有正规的Getter/SetterConstructiontoString等@Accessors(chain=true)//这个lombok注解是为了实现Entity伪构建,例如:entity.setX(x).setY(y)publicclassPageParamimplementsIPage{//description="pagenumber",defaultValue=1privateIntegerpageNum=1;//description="页码",defaultValue=20privateIntegerpageSize=20;//description="sort",example="iddesc"privateStringorderBy;//description="parameter"privateTparam;publicPageParamsetOrderBy(StringorderBy){this.orderBy=orderBy;//这里可以优化优化细节看解析returnthis;}}在BaseService中,我们看到一个新的PageParam,它引用PageInfo对分页参数和业务参数进行封装/声明/分离,以及参数类型泛型,支持任意数据类型的业务参数。同时也可以看到PageParam实现了IPage接口,多了一个orderBy属性字段。importcommon.base.BaseService;importdto.req.TemplateReqDto;importdto.resp.TemplateRespDto;publicinterfaceTemplateServiceextendsBaseService{//同样是接口接口,业务Service只需要继承BaseService//并声明请求参数和responses根据实际使用场景result的Entity实体即可}在实际应用中,我们只需要声明我们通用的业务查询请求参数和响应结果即可。importdto.req.TemplateReqDto;importdto.resp.TemplateRespDto;importservice.TemplateService;importpersistence.mapper.TemplateMapper;importlombok.RequiredArgsConstructor;importlombok.extern.slf4j.Slf4j;importorg.springframework.stereotype.Service;importjava.util.List;@Slf4j//根据lombok自动生成logger日志记录实例@Service//SpringBoot中注册ServiceBean的注解@RequiredArgsConstructor//根据类的所有final属性生成基于lombok的构造函数,完成Spring构造注入>list(TemplateReqDtoparam){returnmapper.selectManySelective(param)//实体可以根据实际情况进行转换}}实现类只需要重写list方法体,写出需要的业务逻辑处理和查询方法在实际业务场景中处理Among它们,不需要关心分页功能。@Slf4j//同上@RestController//SpringBoot中注册ControllerBean的注解@RequiredArgsConstructor//同上publicclassTemplateController{publicfinalTemplateService;/***分页查询**@parampageParam分页查询参数*@return分页查询响应*/@PostMapping(path="page")publicPageInfopage(@RequestBodyPageParampageParam){returnservice.page(pageParam);}/***集合查询**@paramlistParam集合查询参数*@return集合查询响应*/@PostMapping(path="list")publicListlist(@RequestBodyParamlistParam){returnservice.list(listParam);}}最后,在编写Controller接口的时候,只需要直接调用service.page,request参数直接用PageParam包裹,将分页参数和业务参数分离,在前后端接口联调时保持这种分离规范,可以大大降低沟通和开发成本。常见问题1。BaseService是一个接口,为什么页面可以声明方法体呢?答:Java8的一个新特性就是在interface接口类中增加了static/default方法,即方法声明后,其子类或实现默认都会有这些方法。可以直接调用。这里之所以声明为Page方法default是因为page函数只关注寻呼参数和寻呼响应,与业务场景分离,方法体也大不相同,所以简单抽象定义,省去繁琐及其实施的冗余过程。2、PageParam的声明有什么意义?实现IPage的目的是什么?答:PageParam是参考PageInfo写的类(不确定PageHelper以后会不会封装这个类,或许可以提个Issue参与开源框架的开发)。写这个类的目的是为了分离分页和业务数据,让开发者专注于业务的实现和开发。它也是分页查询API的规范。无论是请求还是响应,都会提取与分页相关的数据。单独使用。IPage的实现是因为IPage是PageHelper的内置接口。在详细了解它的作用之前,可以作为我们分页参数声明的规范,而IPage中只声明了三个方法,即pageNum/pageSize/orderBy的getter方法,而在源码分析中,我会提到实现这个接口的更深层次的意义。3、PageParam中除了常规的pageNum/pageSize,为什么还需要一个orderBy?答:在常规的分页查询中,只需要pageNum/pageSize就可以完成分页的目的,但是往往伴随着分页查询,还有过滤和排序,而orderBy则侧重于基于SQL的动态参数排序。4.如何使用orderBy?会不会有什么问题?答:orderBy和pageNum/pageSize一样,都是Pagehelper通过MyBatis拦截器注入查询的,所以在前端传参的时候,orderBy参数应该是数据库列的desc/asc形式,多字段排序可以用逗号(,)拼接,例如:columnAdesc,columnB,但另一方面,有两个问题。首先是大部分数据库表字段设计都是蛇形命名,而不是常规开发中的驼峰命名,所以有一层转换,这个转换可以在传递参数的时候赋给前端,也可以赋给后端-接收参数时结束。二是将排序字段裸露给接口,会存在SQL注入排序的风险,所以在实际使用过程中,我们需要通过一些手段来验证和检查orderBy参数是否合法,比如因为使用正则表达式匹配参数值只能包含orderby语法中必要的值,例如字段名、desc或asc,不允许有特殊字符/数据库关键字。5.pageNum/pageSize是否必须给定默认值?答:通过阅读PageHelper的源码我们知道,当Page查询参数为null时,不给它们默认值,也不做额外的处理,导致分页失败,给默认值给防止前后端调试接口时可能出现的各种意外。3.源码分析首先我们看看PageHelper.startPage(param)过程中发生了什么:PageoldPage=getLocalPage();if(oldPage!=null&&oldPage.isOrderByOnly()){page.setOrderBy(oldPage.getOrderBy());}setLocalPage(page);returnpage;}这是PageMethod继承的抽象类(extended)byPageHelper中的一个静态方法。看第一行代码Pagepage=PageObjectUtil.getPageFromObject(params,true)发生了什么:publicstaticPagegetPageFromObject(Objectparams,booleanrequired){if(params==null){thrownewPageException("无法获取分页查询参数!");}elseif(paramsinstanceofIPage){IPagepageParams=(IPage)params;Pagepage=null;if(pageParams.getPageNum()!=null&&pageParams.getPageSize()!=null){page=newPage(pageParams.getPageNum(),pageParams.getPageSize());}if(StringUtil.isNotEmpty(pageParams.getOrderBy())){if(page!=null){page.setOrderBy(pageParams.getOrderBy());}else{page=newPage();page.setOrderBy(pageParams.getOrderBy());page.setOrderByOnly(true);}}returnpage;}else{...//我这里只截取了一些代码片段,以上是比较重要的piece}}可以看到在这个方法中,会先判断params是否为null,然后通过instanceof判断是IPage的子类还是实现类。如果上面两个if/else都不满足,PageHelper会在我省略贴的代码中通过大量的反射代码获取pageNum/pageSize和orderBy。众所周知,虽然反射在Java中被广泛使用,并且作为该语言独有的特性之一,深受开发者的喜爱,但在某种程度上,反射是需要性能成本的,甚至很多主流的框架和技术也是如此。公司正在努力减少反射的使用,以防止框架因性能不佳而被市场淘汰。那么到此为止,我们终于解释清楚了PageParam为什么要实现IPage接口。这里的代码中,可以直接通过接口获取分页参数,而不需要通过反射获取PageHelper需要的参数,有损性能。继续看startPage中的后续代码:(Page)LOCAL_PAGE.get();}......}可以看到PageHelper继承的抽象类PageMethod声明了一个Page线程局部变量,getLocalPage()就是获取线程中的当前Page。而接下来的if(oldPage!=null&&oldPage.isOrderByOnly())就是判断是否有旧页面数据。这里的isOrderByOnly可以通过getPageFromObject()函数得知,只有orderBy参数存在时为真。也就是说,当存在旧分页数据且旧分页数据只有排序参数时,旧分页数据的排序参数包含在新分页数据的排序参数中。然后将新的分页数据页存储在本地线程变量中。在实际的应用场景中,这种情况还是比较少见的,只排序不分页,所以从某个角度,我们只需要理解即可。接下来看doSelectPageInfo(ISelect)发生了什么:publicPageInfodoSelectPageInfo(ISelectselect){select.doSelect();returnthis.toPageInfo();}可以看到这个方法的实现很简单而clear,即通过注册声明ISelect接口来开发自定义的集合查询方法并在内部执行,然后返回PageInfo实体。前面我们提到PageHelper是基于MyBatis拦截器实现分页的目的,那么为什么这里的ISelect.doSelect()返回的是PageInfo实体呢?其实这就是拦截器的妙用,在select.doSelect()执行时,会触发PageHelper自定义的MyBatis查询拦截器,通过解析SQL和SQL参数,根据数据库类型进行分页,比如MySQL的limit,Oracle的Rownum等。同时在我们定义的querySQL之前,PageHelper会重新生成一个selectcount(*)SQL先行执行,已经达到了它定义Page内置分页的目的参数。@Intercepts({@Signature(type=Executor.class,method="query",args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class}),@Signature(type=Executor.class,method="query",args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class,CacheKey.class,BoundSql.class})})publicclassPageInterceptorimplementsInterceptor{privatevolatileDialectdialect;privateStringcountSuffix="_COUNT";protectedCachemsCountMap=null;privateStringdefault_dialect_class="com.github.pagehelper.PageHelper";publicPageInterceptor(){}publicObjectintercept(Invocationinvocation)throwsThrowable{...}}上面是PageHelper内置的自定义MyBatis拦截器,因为代码太多,为了不违背本博文不对的原则,这里不再赘述。如果有需要,我可以另外写一篇博客,对MyBatis拦截器的概念和原理进行讲解和讲解,深入剖析MyBatis的源码。ExtendedPageHelper不仅有pageNum/pageSize/orderBy参数,还有pageSizeZero,合理的参数等,用于更高级的分页查询定义。如果你需要更深入的了解,我可以再写一个进阶的PageHelper。此处本文仅作为普通开发使用的说明。4.总结PageHelper作为GitHub上近10K的开源分页框架,可能没有市场主流框架和技术那么深入和广泛。很大程度上解决了基于MyBatis的分页的诸多技术问题,简化并提高了开发者的效率。这是开发者在发展的道路上应该向往和努力的方向和道路。而作为受益者,我们不应该只是基本地使用它。除了开发,还要注意一些框架的扩展,对框架的底层有一定的了解,对其进行扩展和优化。再把PageHelper的开源仓库放在这里!