先看一下Page实体类定义,几个重要的属性值。
public class Page<E> extends ArrayList<E> {
private static final long serialVersionUID = 1L;
/**
* 不进行count查询
*/
private static final int NO_SQL_COUNT = -1;
/**
* 进行count查询
*/
private static final int SQL_COUNT = 0;
/**
* 页码,从1开始
*/
private int pageNum;
/**
* 页面大小
*/
private int pageSize;
/**
* 起始行
*/
private int startRow;
/**
* 末行
*/
private int endRow;
/**
* 总数
*/
private long total;
/**
* 总页数
*/
private int pages;
/**
* 分页合理化
*/
private Boolean reasonable;
/**
* 当设置为true的时候,如果pagesize设置为0(或RowBounds的limit=0),就不执行分页,返回全部结果
*/
private Boolean pageSizeZero;
}
复制代码
Intercepts是mybatis的注解,@Signature是拦截器要拦截的方法签名,
如下:拦截Executor类的query方法,query方法的参数类型有MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class
@Intercepts(@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
复制代码
拦截器的实现是实现mybatis的Interceptor接口
public class PageHelper implements Interceptor {
//sql工具类
private SqlUtil sqlUtil;
//属性参数信息
private Properties properties;
//自动获取dialect
private Boolean autoDialect;
}
复制代码
PageHelper的分页方法,很直观,不赘述。
/**
* 开始分页
*
* @param pageNum 页码
* @param pageSize 每页显示数量
*/
public static Page startPage(int pageNum, int pageSize) {
return startPage(pageNum, pageSize, true);
}
/**
* 开始分页
*
* @param pageNum 页码
* @param pageSize 每页显示数量
* @param count 是否进行count查询
*/
public static Page startPage(int pageNum, int pageSize, boolean count) {
return startPage(pageNum, pageSize, count, null);
}
/**
* 开始分页
*
* @param pageNum 页码
* @param pageSize 每页显示数量
* @param orderBy 排序
*/
public static Page startPage(int pageNum, int pageSize, String orderBy) {
orderBy(orderBy);
return startPage(pageNum, pageSize);
}
/**
* 开始分页
*
* @param pageNum 页码
* @param pageSize 每页显示数量
* @param count 是否进行count查询
* @param reasonable 分页合理化,null时用默认配置
*/
public static Page startPage(int pageNum, int pageSize, boolean count, Boolean reasonable) {
return startPage(pageNum, pageSize, count, reasonable, null);
}
/**
* 开始分页
*
* @param pageNum 页码
* @param pageSize 每页显示数量
* @param count 是否进行count查询
* @param reasonable 分页合理化,null时用默认配置
* @param pageSizeZero true且pageSize=0时返回全部结果,false时分页,null时用默认配置
*/
public static Page startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
Page page = new Page(pageNum, pageSize, count);
page.setReasonable(reasonable);
page.setPageSizeZero(pageSizeZero);
SqlUtil.setLocalPage(page);
return page;
}
/**
* 开始分页
*
* @param params
*/
public static Page startPage(Object params) {
Page page = SqlUtil.getPageFromObject(params);
SqlUtil.setLocalPage(page);
return page;
}
/**
* 排序
*
* @param orderBy
*/
public static void orderBy(String orderBy) {
OrderByHelper.orderBy(orderBy);
}
复制代码
重写mybatis的Interceptor接口的方法,Invocation是JDK动态代理中的InnvocationHandler对象,其传播拦截器链的方法是invocation.proceed();
/**
* Mybatis拦截器方法
*
* @param invocation 拦截器入参
* @return 返回执行结果
* @throws Throwable 抛出异常
*/
public Object intercept(Invocation invocation) throws Throwable {
if (autoDialect) {
initSqlUtil(invocation);
}
return sqlUtil.processPage(invocation);
}
复制代码
其中涉及sqlUtil,把invocation.proceed()包装了一层。先来看sqlUtil的初始化。
如下代码中,一个重要的类MetaObject是mybatis提供的一个用于方便、优雅访问对象属性的对象,通过它可以简化代码、不需要try/catch各种reflect异常,同时它支持对JavaBean、Collection、Map三种类型对象的操作。
此处,通过MetaObject拿到MappedStatement对象引用的DataSource属性,对象引用链为:MappedStatement->Configuration->Environment->Datasource
拿到datasource的url之后封装在SqlUtil中,初始化过程结束。
/**
* 初始化sqlUtil
*
* @param invocation
*/
public synchronized void initSqlUtil(Invocation invocation) {
if (sqlUtil == null) {
String url = null;
try {
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
MetaObject msObject = SystemMetaObject.forObject(ms);
DataSource dataSource = (DataSource) msObject.getValue("configuration.environment.dataSource");
url = dataSource.getConnection().getMetaData().getURL();
} catch (SQLException e) {
throw new RuntimeException("分页插件初始化异常:" + e.getMessage());
}
if (url == null || url.length() == 0) {
throw new RuntimeException("无法自动获取jdbcUrl,请在分页插件中配置dialect参数!");
}
String dialect = Dialect.fromJdbcUrl(url);
if (dialect == null) {
throw new RuntimeException("无法自动获取数据库类型,请通过dialect参数指定!");
}
sqlUtil = new SqlUtil(dialect);
sqlUtil.setProperties(properties);
properties = null;
autoDialect = false;
}
}
复制代码
先看一眼sqlUtil类的几个成员变量后面_processPage方法会用到,其中Page对象和COUNT都保存在ThreadLocal中,避免并发问题。
private static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
private static final ThreadLocal<Boolean> COUNT = new ThreadLocal<Boolean>() {
@Override
protected Boolean initialValue() {
return null;
}
};
private static final Map<String, MappedStatement> msCountMap = new ConcurrentHashMap<String, MappedStatement>();
复制代码
再回过头去看sqlUtil.processPage(invocation)方法调用,最终调用的是com.github.pagehelper.SqlUtil._processPage方法。源码太长了,且源码都有中文注释比较直观,这里只是梳理一下代码逻辑,伪代码如下:
private Object _processPage(Invocation invocation) throws Throwable {
final Object[] args = invocation.getArgs();
RowBounds rowBounds = (RowBounds) args[2];
//分支1:如果没有分页对象LocalPage,RowBounds也采取默认值,不执行分页
if (SqlUtil.getLocalPage() == null && rowBounds == RowBounds.DEFAULT) {
//有排序要求,则执行排序拦截器
if (OrderByHelper.getOrderBy() != null) {
OrderByHelper.processIntercept(invocation);
}
//不需要排序则直接执行原sql
return invocation.proceed();
//分支2:需要分页
} else {
//获取原始的ms
MappedStatement ms = (MappedStatement) args[0];
//判断并处理为PageSqlSource
if (!isPageSqlSource(ms)) {
processMappedStatement(ms, parser);
}
//忽略RowBounds-否则会进行Mybatis自带的内存分页
args[2] = RowBounds.DEFAULT;
//分页信息
Page page = getPage(rowBounds);
//分支2.1 判断pageSizeZero,也不需要执行分页
//pageSizeZero的判断
if ((page.getPageSizeZero() != null && page.getPageSizeZero()) && page.getPageSize() == 0) {
Object result = invocation.proceed();
return page;
}
//分支2.2
//简单的通过total的值来判断是否进行count查询
if (page.isCount()) {
COUNT.set(Boolean.TRUE);
//替换MS
args[0] = msCountMap.get(ms.getId());
//查询总数
Object result = invocation.proceed();
//还原ms
args[0] = ms;
//设置总数
page.setTotal((Integer) ((List) result).get(0));
if (page.getTotal() == 0) {
return page;
}
}
//分支2.3
//pageSize>0的时候执行分页查询,pageSize<=0的时候不执行相当于可能只返回了一个count
if (page.getPageSize() > 0 &&
((rowBounds == RowBounds.DEFAULT && page.getPageNum() > 0)
|| rowBounds != RowBounds.DEFAULT)) {
//替换SQL为分页SQL
BoundSql boundSql = ms.getBoundSql(args[1]);
args[1] = parser.setPageParameter(ms, args[1], boundSql, page);
COUNT.set(Boolean.FALSE);
//执行分页查询
Object result = invocation.proceed();
//得到处理结果
page.addAll((List) result);
}
//返回结果
return page;
}
}
复制代码
以上代码总结一下,分支1是不执行分页,ThreadLocal里根本没有分页相关变量;分支2是ThreadLocal有Page对象,分支2又包括三种情况,2.1是设置了PageSizeZero则不进行分页,2.2是需要在原SQL上额外查询表信息总条数,替换了mappedStatement得到count值设置到Page对象里,再invocation.proceed()执行原SQL;2.3是替换SQL为分页SQL返回分页查询结果。不管分不分页,最后都是返回Page对象。
PageHelper最后两个方法也是重写Interceptor的接口方法。
plugin()方法是mybatis提供的生成代理对象的方法。细节参看Plugin.wrap(target, this)。
/**
* 只拦截Executor
*
* @param target
* @return
*/
public Object plugin(Object target) {
if (target instanceof Executor) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
复制代码
setProperties()是从mybatis-config.xml读取属性值。如下xml示例:可以读取到name属性的value值。
<plugins>
<plugin interceptor="com.example.springbootdemo.mybatis.interceptor.MybatisInterceptor">
<property name="name" value="test"></property>
</plugin>
</plugins>
复制代码
主要是设置数据库类型,如下xml:
<!-- 4.0.0以后版本可以不设置该参数 -->
<property name="dialect" value="mysql" />
复制代码
/**
* 设置属性值
*
* @param p 属性值
*/
public void setProperties(Properties p) {
//MyBatis3.2.0版本校验
try {
Class.forName("org.apache.ibatis.scripting.xmltags.SqlNode");//SqlNode是3.2.0之后新增的类
} catch (ClassNotFoundException e) {
throw new RuntimeException("您使用的MyBatis版本太低,MyBatis分页插件PageHelper支持MyBatis3.2.0及以上版本!");
}
//数据库方言
String dialect = p.getProperty("dialect");
if (dialect == null || dialect.length() == 0) {
autoDialect = true;
this.properties = p;
} else {
autoDialect = false;
sqlUtil = new SqlUtil(dialect);
sqlUtil.setProperties(p);
}
}
}
复制代码