MyBatis 分页插件设计与实战(完整实操案例)分页查询是业务系统中最常见的需求之一。虽然可以手动在 SQL 后拼接LIMIT或ROWNUM,但这样会侵入业务代码,且需要为每个查询编写重复的分页逻辑。通过 MyBatis 插件机制,我们可以实现一个透明物理分页插件:开发者只需在调用 Mapper 方法前设置分页参数,插件自动拦截 SQL 并改写为对应数据库的分页语句,同时返回总记录数。一、需求分析功能点说明透明分页无需修改原有 Mapper 接口和 XML,只需在 Service 层设置分页参数物理分页使用数据库方言(LIMIT/ROWNUM/OFFSET FETCH)实现真分页自动查询总数执行分页查询时自动生成COUNT(*)SQL 并查询总记录数方言适配支持 MySQL、PostgreSQL、Oracle、SQL Server 等返回结果返回包含分页信息(总条数、当前页、每页大小)和数据的统一对象二、设计思路基于 MyBatis 插件机制,拦截Executor.query方法(因为所有查询最终都会经过它)。核心步骤如下:定义分页参数对象Page:存储当前页码、每页大小、总记录数、排序字段等。定义方言接口Dialect:提供getCountSql(生成计数 SQL)和getPageSql(生成分页 SQL)方法。实现各数据库方言:MySQL、Oracle、PostgreSQL 等。编写分页拦截器PageInterceptor:通过@Intercepts注解拦截Executor.query方法。判断参数中是否包含Page对象,若无则直接放行。若有,则:通过反射获取原始的MappedStatement和BoundSql。生成并执行 COUNT SQL,获取总记录数并设置到Page对象中。生成分页 SQL,替换原有BoundSql中的 SQL,并继续执行查询。将查询结果封装到Page对象中返回。使用ThreadLocal传递分页参数:实现线程安全的分页参数传递,避免侵入方法签名。三、完整源码实现3.1 分页参数对象Page.javapackagecom.example.plugin.page;importjava.io.Serializable;importjava.util.List;publicclassPageTimplementsSerializable{privatestaticfinallongserialVersionUID=1L;// 当前页码privateintpageNum=1;// 每页条数privateintpageSize=10;// 总记录数privatelongtotal=0;// 总页数privateintpages=0;// 查询结果集privateListTlist;// 是否查询总数(默认 true)privatebooleancount=true;// 排序字段(可选,用于简化)privateStringorderBy;publicPage(){}publicPage(intpageNum,intpageSize){this.pageNum=pageNum;this.pageSize=pageSize;}// getter / setter 省略}3.2 方言接口Dialect.javapackagecom.example.plugin.dialect;publicinterfaceDialect{/** * 根据原始 SQL 生成计数 SQL * @param originalSql 原始查询 SQL * @return 计数 SQL */StringgetCountSql(StringoriginalSql);/** * 根据原始 SQL 和分页参数生成分页 SQL * @param originalSql 原始查询 SQL * @param offset 起始行(从0开始) * @param limit 每页条数 * @return 分页 SQL */StringgetPageSql(StringoriginalSql,intoffset,intlimit);}3.3 MySQL 方言实现MySQLDialect.javapackagecom.example.plugin.dialect;publicclassMySQLDialectimplementsDialect{@OverridepublicStringgetCountSql(StringoriginalSql){// 简单的 SELECT COUNT(*) 包装,实际需处理 ORDER BY、DISTINCT 等return"SELECT COUNT(*) FROM ("+originalSql+") AS tmp_count";}@OverridepublicStringgetPageSql(StringoriginalSql,intoffset,intlimit){returnoriginalSql+" LIMIT "+offset+", "+limit;}}3.4 Oracle 方言实现OracleDialect.javapackagecom.example.plugin.dialect;publicclassOracleDialectimplementsDialect{@OverridepublicStringgetCountSql(StringoriginalSql){return"SELECT COUNT(*) FROM ("+originalSql+")";}@OverridepublicStringgetPageSql(StringoriginalSql,intoffset,intlimit){intstartRow=offset+1;intendRow=offset+limit;StringBuildersql=newStringBuilder();sql.append("SELECT * FROM (SELECT TMP_PAGE.*, ROWNUM PAGE_ROW_NUM FROM (");sql.append(originalSql);sql.append(") TMP_PAGE WHERE ROWNUM = ").append