简介
利用 MyBatis Plugin 插件技术实现分页功能。
分页插件实现思路如下:
- 业务代码在 ThreadLocal 中保存分页信息;
- MyBatis Interceptor 拦截查询请求,获取分页信息,实现分页操作,封装分页列表数据返回;
测试类:com.yjw.demo.PageTest
插件开发过程
确定需要拦截的签名
MyBatis 插件可以拦截四大对象中的任意一个,从 Plugin 源码中可以看到它需要注册签名才能够运行插件,签名需要确定一些要素。
确定需要拦截的对象
- Executor 是执行 SQL 的全过程,包括组装参数,组装结果集返回和执行 SQL 过程,都可以拦截。
- StatementHandler 是执行 SQL 的过程,我们可以重写执行 SQL 的过程。
- ParameterHandler 是拦截执行 SQL 的参数组装,我们可以重写组装参数规则。
- ResultSetHandler 用于拦截执行结果的组装,我们可以重写组装结果的规则。
拦截方法和参数
当确定了需要拦截什么对象,接下来就要确定需要拦截什么方法和方法的参数。比如分页插件需要拦截 Executor 的 query 方法,我们先看看 Executor 接口的定义,代码清单如下:
public interface Executor {ResultHandler NO_RESULT_HANDLER = null;int update(MappedStatement ms, Object parameter) throws SQLException;<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;<E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;List<BatchResult> flushStatements() throws SQLException;void commit(boolean required) throws SQLException;void rollback(boolean required) throws SQLException;CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);boolean isCached(MappedStatement ms, CacheKey key);void clearLocalCache();void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);Transaction getTransaction();void close(boolean forceRollback);boolean isClosed();void setExecutorWrapper(Executor executor);}
以上的任何方法都可以拦截,从接口定义而言,query 方法有两个,我们可以按照代码清单来定义签名。
@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})})
其中,@Intercepts 说明它是一个拦截器。@Signature 是注册拦截器签名的地方,type 是四大对象中的一个,method 是需要拦截的方法,args 是方法的参数。
插件接口定义
在 MyBatis 中开发插件,需要实现 Interceptor 接口,接口的定义如下:
public interface Interceptor {Object intercept(Invocation invocation) throws Throwable;Object plugin(Object target);void setProperties(Properties properties);}
- intercept 方法:它将直接覆盖你所拦截对象原有的方法,因此它是插件的核心方法。通过 invocation 参数可以反射调度原来对象的方法。
- plugin 方法:target 是被拦截对象,它的作用是给被拦截对象生成一个代理对象,并返回它。为了方便 MyBatis 使用 org.apache.ibatis.plugin.Plugin 中的 wrap 静态方法提供生成代理对象。
- setProperties 方法:允许在 plugin 元素中配置所需参数,方法在插件初始化的时候就被调用了一次,然后把插件对象存入到配置中,以便后面再取出。
实现类
根据分页插件的实现思路,定义了三个类。
Page 类
Page 类继承了 ArrayList 类,用来封装分页信息和列表数据。
/*** 分页返回对象** @author yinjianwei* @date 2018/11/05*/public class Page<E> extends ArrayList<E> {private static final long serialVersionUID = 1L;/*** 页码,从1开始*/private int pageNum;/*** 页面大小*/private int pageSize;/*** 起始行*/private int startRow;/*** 末行*/private int endRow;/*** 总数*/private long total;/*** 总页数*/private int pages;public int getPageNum() {return pageNum;}public void setPageNum(int pageNum) {this.pageNum = pageNum;}public int getPageSize() {return pageSize;}public void setPageSize(int pageSize) {this.pageSize = pageSize;}public int getStartRow() {return startRow;}public void setStartRow(int startRow) {this.startRow = startRow;}public int getEndRow() {return endRow;}public void setEndRow(int endRow) {this.endRow = endRow;}public long getTotal() {return total;}public void setTotal(long total) {this.total = total;this.pages = (int)(total / pageSize + (total % pageSize == 0 ? 0 : 1));if (pageNum > pages) {pageNum = pages;}this.startRow = this.pageNum > 0 ? (this.pageNum - 1) * this.pageSize : 0;this.endRow = this.startRow + this.pageSize * (this.pageNum > 0 ? 1 : 0);}public int getPages() {return pages;}public void setPages(int pages) {this.pages = pages;}/*** 返回当前对象** @return*/public List<E> getResult() {return this;}}
PageHelper 类
PageHelper 类是分页的帮助类,主要利用 ThreadLocal 线程变量存储分页信息。代码清单如下:
/*** 分页帮助类** @author yinjianwei* @date 2018/11/05*/@SuppressWarnings("rawtypes")public class PageHelper {private static final ThreadLocal<Page> PAGE_THREADLOCAT = new ThreadLocal<Page>();/*** 设置线程局部变量分页信息** @param page*/public static void setPageThreadLocal(Page page) {PAGE_THREADLOCAT.set(page);}/*** 获取线程局部变量分页信息** @return*/public static Page getPageThreadLocal() {return PAGE_THREADLOCAT.get();}/*** 清空线程局部变量分页信息*/public static void pageThreadLocalClear() {PAGE_THREADLOCAT.remove();}/*** 设置分页参数** @param pageNum* @param pageSize*/public static void startPage(Integer pageNum, Integer pageSize) {Page page = new Page();page.setPageNum(pageNum);page.setPageSize(pageSize);setPageThreadLocal(page);}}
PageInterceptor 类
PageInterceptor 类实现了 Interceptor 接口,是分页插件的核心类。代码清单如下:
/*** 分页拦截器** @author yinjianwei* @date 2018/11/05*/@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})})public class PageInterceptor implements Interceptor {private Field additionalParametersField;@SuppressWarnings({"rawtypes", "unchecked"})@Overridepublic Object intercept(Invocation invocation) throws Throwable {Executor executor = (Executor)invocation.getTarget();Object[] args = invocation.getArgs();MappedStatement ms = (MappedStatement)args[0];Object parameter = args[1];RowBounds rowBounds = (RowBounds)args[2];ResultHandler resultHandler = (ResultHandler)args[3];CacheKey cacheKey;BoundSql boundSql;// 4个参数if (args.length == 4) {boundSql = ms.getBoundSql(parameter);cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);}// 6个参数else {cacheKey = (CacheKey)args[4];boundSql = (BoundSql)args[5];}// 判断是否需要分页Page page = PageHelper.getPageThreadLocal();// 不执行分页if (page.getPageNum() <= 0) {return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);}// count查询MappedStatement countMs = newCountMappedStatement(ms);String sql = boundSql.getSql();String countSql = "select count(1) from (" + sql + ") _count";BoundSql countBoundSql =new BoundSql(ms.getConfiguration(), countSql, boundSql.getParameterMappings(), parameter);Map<String, Object> additionalParameters = (Map<String, Object>)additionalParametersField.get(boundSql);for (Entry<String, Object> additionalParameter : additionalParameters.entrySet()) {countBoundSql.setAdditionalParameter(additionalParameter.getKey(), additionalParameter.getValue());}CacheKey countCacheKey = executor.createCacheKey(countMs, parameter, rowBounds, countBoundSql);Object countResult =executor.query(countMs, parameter, RowBounds.DEFAULT, resultHandler, countCacheKey, countBoundSql);Long count = (Long)((List)countResult).get(0);page.setTotal(count);// 分页查询String pageSql = sql + " limit " + page.getStartRow() + "," + page.getPageSize();BoundSql pageBoundSql =new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);for (Entry<String, Object> additionalParameter : additionalParameters.entrySet()) {pageBoundSql.setAdditionalParameter(additionalParameter.getKey(), additionalParameter.getValue());}CacheKey pageCacheKey = executor.createCacheKey(ms, parameter, rowBounds, pageBoundSql);List listResult = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageCacheKey, pageBoundSql);page.addAll(listResult);// 清空线程局部变量分页信息PageHelper.pageThreadLocalClear();return page;}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {try {additionalParametersField = BoundSql.class.getDeclaredField("additionalParameters");additionalParametersField.setAccessible(true);} catch (NoSuchFieldException | SecurityException e) {e.printStackTrace();}}/*** 创建count的MappedStatement** @param ms* @return*/private MappedStatement newCountMappedStatement(MappedStatement ms) {MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId() + "_count",ms.getSqlSource(), ms.getSqlCommandType());builder.resource(ms.getResource());builder.fetchSize(ms.getFetchSize());builder.statementType(ms.getStatementType());builder.keyGenerator(ms.getKeyGenerator());if (ms.getKeyProperties() != null && ms.getKeyProperties().length != 0) {StringBuilder keyProperties = new StringBuilder();for (String keyProperty : ms.getKeyProperties()) {keyProperties.append(keyProperty).append(",");}keyProperties.delete(keyProperties.length() - 1, keyProperties.length());builder.keyProperty(keyProperties.toString());}builder.timeout(ms.getTimeout());builder.parameterMap(ms.getParameterMap());// count查询返回值intList<ResultMap> resultMaps = new ArrayList<ResultMap>();ResultMap resultMap = new ResultMap.Builder(ms.getConfiguration(), ms.getId() + "_count", Long.class,new ArrayList<ResultMapping>(0)).build();resultMaps.add(resultMap);builder.resultMaps(resultMaps);builder.resultSetType(ms.getResultSetType());builder.cache(ms.getCache());builder.flushCacheRequired(ms.isFlushCacheRequired());builder.useCache(ms.isUseCache());return builder.build();}}
配置
MyBatis 配置文件增加 plugin 配置项。
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN""http://mybatis.org/dtd/mybatis-3-config.dtd"><configuration><settings><setting name="lazyLoadingEnabled" value="true"/><setting name="aggressiveLazyLoading" value="false"/></settings><typeHandlers><typeHandler javaType="com.yjw.demo.mybatis.common.constant.Sex"jdbcType="TINYINT"handler="com.yjw.demo.mybatis.common.type.SexEnumTypeHandler"/></typeHandlers><plugins><plugin interceptor="com.yjw.demo.mybatis.common.page.PageInterceptor"></plugin></plugins></configuration>
作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/qh6cfu 来源:殷建卫 - 架构笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
