Mybatis利用Intercepter实现物理分页
一、Interceptor介绍
Mybatis 允许用户使用自定义拦截器对SQL语句执行过程中的某一点进行拦截。默认情况,可以拦截的方法如下:
- Executor 中的 update()、query()、flushStatement()、commit()、rollback()、getTransaction()、close()、isClosed()方法。
- ParameterHandler 中的getParameterObject()方法、setParameters()方法。
- ResultSetHandler 中的 handleResultSets()方法、handleOutputParameters()方法。
- StatementHandler 中的 prepare() 方法、parameterize()方法、batch()方法、update()方法、query()方法。
Interceptor接口如下:
public interface Interceptor { // 执行拦截逻辑的方法 Object intercept(Invocation invocation) throws Throwable; // 决定是否触发intercept()方法 Object plugin(Object target); // 根据配置初始化Interceptor对象 void setProperties(Properties properties); }
setProperties()方法可以加载mybatis-config.xml配置文件中配置的属性,例如:
- <plugins>
- <plugin interceptor="cn.sp.interceptor.PageInterceptor">
- <property name="testProp" value="100"></property>
- </plugin>
- </plugins>
用户自定义拦截器的plugin()方法可以使用Mybatis提供的Plugin工具类实现,它实现了InvocationHandler接口,并提供了一个wrap()静态方法用于创建代理对象。
用户自定义的拦截器除了要实现Interceptor接口外,还需要使用 @Intercepts 和 @Signature 注解。
@Intercepts注解中是一个@Signature列表,每个@Signature注解都标识了该插件需要拦截的方法的信息,其中type表示需要拦截的类型,method属性指定具体的方法名,args属性指定了被拦截方法的参数列表。通过这三个属性值就可以表示一个方法签名。
@Documented @Retention(RetentionPolicy.RUNTIME) @Target({}) public @interface Signature { Class<?> type(); String method(); Class<?>[] args(); }
二、实现PageInterceptor
2.1Mybatis的默认分页机制
Mybatis本身可以使用RowBounds方式进行分页,但是在 DefaultResultSetHandler 中它用的是查询所有数据,然后调用ResultSet.absoulte()方法或循环调用ResultSet.next()方法定位到指定的记录行。这种基于内存分页的方式,当表中的数据量比较大时,会查询全表导致性能问题。
还有一个就是写SQL基于limit实现的物理分页,但是这种基于 "limit offset,length" 的方式如果offset的值很大时,也会导致性能很差,有时间再详细说说mysql分页部分。
下面的例子就是通过自定义拦截器实现物理分页。
2.2代码部分
搭建一个整合Mybatis的SpringBoot项目很简单,过程我就省略了。
PersonDao
public interface PersonDao { List<Person> queryPersonsByPage(RowBounds rowBounds); }
PersonMapper.xml
- "1.0" encoding="UTF-8" xml version=
- <mapper namespace="cn.sp.dao.PersonDao" >
- <select id="queryPersonsByPage" resultType="cn.sp.bean.Person">
- select * from person ORDER BY id DESC
- </select>
- </mapper>
PageInterceptor
/** * 利用拦截器实现分页 * Created by 2YSP on 2019/7/7. */ @Intercepts({ @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}) }) @Slf4j public class PageInterceptor implements Interceptor { /** * Executor.query()方法中,MappedStatement对象在参数列表中的索引位置 */ private static int MAPPEDSTATEMENT_INDEX = 0; /** * 用户传入的实参对象在参数列表中的索引位置 */ private static int PARAMTEROBJECT_INDEX = 1; /** * 分页对象在参数列表中的索引位置 */ private static int ROWBOUNDS_INDEX = 2; /** * 执行拦截逻辑的方法 */ @Override public Object intercept(Invocation invocation) throws Throwable { // 参数列表 Object[] args = invocation.getArgs(); final MappedStatement mappedStatement = (MappedStatement) args[MAPPEDSTATEMENT_INDEX]; final Object parameter = args[PARAMTEROBJECT_INDEX]; final RowBounds rowBounds = (RowBounds) args[ROWBOUNDS_INDEX]; // 获取offset,即查询的起始位置 int offset = rowBounds.getOffset(); int limit = rowBounds.getLimit(); // 获取BoundSql对象,其中记录了包含"?"占位符的SQL语句 final BoundSql boundSql = mappedStatement.getBoundSql(parameter); // 获取BoundSql中记录的SQL语句 String sql = boundSql.getSql(); sql = getPagingSql(sql, offset, limit); log.info("==========sql:\n" + sql); // 重置RowBounds对象 args[ROWBOUNDS_INDEX] = new RowBounds(RowBounds.NO_ROW_OFFSET, RowBounds.NO_ROW_LIMIT); // 根据当前语句创建新的MappedStatement args[MAPPEDSTATEMENT_INDEX] = createMappedStatement(mappedStatement, boundSql, sql); // 通过Invocation.proceed()方法调用被拦截的Executor.query()方法 return invocation.proceed(); } private Object createMappedStatement(MappedStatement mappedStatement, BoundSql boundSql, String sql) { // 创建新的BoundSql对象 BoundSql newBoundSql = createBoundSql(mappedStatement, boundSql, sql); Builder builder = new Builder(mappedStatement.getConfiguration(), mappedStatement.getId(), new BoundSqlSqlSource(newBoundSql), mappedStatement.getSqlCommandType()); builder.useCache(mappedStatement.isUseCache()); builder.cache(mappedStatement.getCache()); builder.databaseId(mappedStatement.getDatabaseId()); builder.fetchSize(mappedStatement.getFetchSize()); builder.flushCacheRequired(mappedStatement.isFlushCacheRequired()); builder.keyColumn(delimitedArrayToString(mappedStatement.getKeyColumns())); builder.keyGenerator(mappedStatement.getKeyGenerator()); builder.keyProperty(delimitedArrayToString(mappedStatement.getKeyProperties())); builder.lang(mappedStatement.getLang()); builder.resource(mappedStatement.getResource()); builder.parameterMap(mappedStatement.getParameterMap()); builder.resultMaps(mappedStatement.getResultMaps()); builder.resultOrdered(mappedStatement.isResultOrdered()); builder.resultSets(delimitedArrayToString(mappedStatement.getResultSets())); builder.resultSetType(mappedStatement.getResultSetType()); builder.timeout(mappedStatement.getTimeout()); builder.statementType(mappedStatement.getStatementType()); return builder.build(); } public String delimitedArrayToString(String[] array) { String result = ""; if (array == null || array.length == 0) { return result; } for (int i = 0; i < array.length; i++) { result += array[i]; if (i != array.length - 1) { result += ","; } } return result; } class BoundSqlSqlSource implements SqlSource { private BoundSql boundSql; public BoundSqlSqlSource(BoundSql boundSql) { this.boundSql = boundSql; } @Override public BoundSql getBoundSql(Object parameterObject) { return boundSql; } } private BoundSql createBoundSql(MappedStatement mappedStatement, BoundSql boundSql, String sql) { BoundSql newBoundSql = new BoundSql(mappedStatement.getConfiguration(), sql, boundSql.getParameterMappings(), boundSql.getParameterObject()); return newBoundSql; } /** * 重写sql */ private String getPagingSql(String sql, int offset, int limit) { sql = sql.trim(); boolean hasForUpdate = false; String forUpdatePart = "for update"; if (sql.toLowerCase().endsWith(forUpdatePart)) { // 将当前SQL语句的"for update片段删除" sql = sql.substring(0, sql.length() - forUpdatePart.length()); hasForUpdate = true; } StringBuilder result = new StringBuilder(); result.append(sql); result.append(" limit "); result.append(offset); result.append(","); result.append(limit); if (hasForUpdate) { result.append(" " + forUpdatePart); } return result.toString(); } /** * 决定是否触发intercept()方法 */ @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } /** * 根据配置初始化Interceptor对象 */ @Override public void setProperties(Properties properties) { log.info("properties: " + properties.getProperty("testProp")); } }
这里的思路就是拦截 query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) 方法或query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) 方法,通过RowBounds对象获得所需记录的offset和limit,通过BoundSql获取待执行的sql语句,最后重写SQL语句加入"limit offset,length"实现分页。
三、测试
执行如下测试方法:

控制台显示结果:

输出结果是,id为7的王五和id为6的张三,再对比数据库数据。

最后得出结论成功实现分页查询,GitHub上开源的大名鼎鼎的PageHelper也是利用拦截器插件实现的,有时间要再看下它的源码实现。
本文代码点击这里。
本文作者:烟味i
本文链接:https://www.cnblogs.com/2YSP/p/11166771.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步