数据分页功能是咱们软件系统中必备的功能,在持久层使用mybatis的状况下,pageHelper来实现后台分页则是咱们经常使用的一个选择,因此本文专门类介绍下。java
相关依赖mysql
<dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.2.8</version> </dependency> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>1.2.15</version> </dependency>
要使用PageHelper首先在mybatis的全局配置文件中配置。以下:git
<?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> <plugins> <!-- com.github.pagehelper为PageHelper类所在包名 --> <plugin interceptor="com.github.pagehelper.PageHelper"> <property name="dialect" value="mysql" /> <!-- 该参数默认为false --> <!-- 设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用 --> <!-- 和startPage中的pageNum效果同样 --> <property name="offsetAsPageNum" value="true" /> <!-- 该参数默认为false --> <!-- 设置为true时,使用RowBounds分页会进行count查询 --> <property name="rowBoundsWithCount" value="true" /> <!-- 设置为true时,若是pageSize=0或者RowBounds.limit = 0就会查询出所有的结果 --> <!-- (至关于没有执行分页查询,可是返回结果仍然是Page类型) --> <property name="pageSizeZero" value="true" /> <!-- 3.3.0版本可用 - 分页参数合理化,默认false禁用 --> <!-- 启用合理化时,若是pageNum<1会查询第一页,若是pageNum>pages会查询最后一页 --> <!-- 禁用合理化时,若是pageNum<1或pageNum>pages会返回空数据 --> <property name="reasonable" value="false" /> <!-- 3.5.0版本可用 - 为了支持startPage(Object params)方法 --> <!-- 增长了一个`params`参数来配置参数映射,用于从Map或ServletRequest中取值 --> <!-- 能够配置pageNum,pageSize,count,pageSizeZero,reasonable,不配置映射的用默认值 --> <!-- 不理解该含义的前提下,不要随便复制该配置 --> <property name="params" value="pageNum=start;pageSize=limit;" /> <!-- always老是返回PageInfo类型,check检查返回类型是否为PageInfo,none返回Page --> <property name="returnPageInfo" value="check" /> </plugin> </plugins> </configuration>
咱们经过以下几行代码来演示过程github
// 获取配置文件 InputStream inputStream = Resources.getResourceAsStream("mybatis/mybatis-config.xml"); // 经过加载配置文件获取SqlSessionFactory对象 SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream); // 获取SqlSession对象 SqlSession session = factory.openSession(); PageHelper.startPage(1, 5); session.selectList("com.bobo.UserMapper.query");
加载配置文件咱们从这行代码开始sql
new SqlSessionFactoryBuilder().build(inputStream);
public SqlSessionFactory build(InputStream inputStream) { return build(inputStream, null, null); }
private void pluginElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { // 获取到内容:com.github.pagehelper.PageHelper String interceptor = child.getStringAttribute("interceptor"); // 获取配置的属性信息 Properties properties = child.getChildrenAsProperties(); // 建立的拦截器实例 Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance(); // 将属性和拦截器绑定 interceptorInstance.setProperties(properties); // 这个方法须要进入查看 configuration.addInterceptor(interceptorInstance); } } }
public void addInterceptor(Interceptor interceptor) { // 将拦截器添加到了 拦截器链中 而拦截器链本质上就是一个List有序集合 interceptorChain.addInterceptor(interceptor); }
小结:经过SqlSessionFactory对象的获取,咱们加载了全局配置文件及映射文件同时还==将配置的拦截器添加到了拦截器链中==。数据库
咱们来看下PageHelper的源代码的头部定义安全
@SuppressWarnings("rawtypes") @Intercepts( @Signature( type = Executor.class, method = "query", args = {MappedStatement.class , Object.class , RowBounds.class , ResultHandler.class })) public class PageHelper implements Interceptor { //sql工具类 private SqlUtil sqlUtil; //属性参数信息 private Properties properties; //配置对象方式 private SqlUtilConfig sqlUtilConfig; //自动获取dialect,若是没有setProperties或setSqlUtilConfig,也能够正常进行 private boolean autoDialect = true; //运行时自动获取dialect private boolean autoRuntimeDialect; //多数据源时,获取jdbcurl后是否关闭数据源 private boolean closeConn = true;
// 定义的是拦截 Executor对象中的 // query(MappedStatement ms,Object o,RowBounds ob ResultHandler rh) // 这个方法 type = Executor.class, method = "query", args = {MappedStatement.class , Object.class , RowBounds.class , ResultHandler.class }))
PageHelper中已经定义了该拦截器拦截的方法是什么。session
接下来咱们须要分析下SqlSession的实例化过程当中Executor发生了什么。咱们须要从这行代码开始跟踪mybatis
SqlSession session = factory.openSession();
public SqlSession openSession() { return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false); }
加强Executor
app
到此咱们明白了,Executor对象其实被咱们生存的代理类加强了。invoke的代码为
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { Set<Method> methods = signatureMap.get(method.getDeclaringClass()); // 若是是定义的拦截的方法 就执行intercept方法 if (methods != null && methods.contains(method)) { // 进入查看 该方法加强 return interceptor.intercept(new Invocation(target, method, args)); } // 不是须要拦截的方法 直接执行 return method.invoke(target, args); } catch (Exception e) { throw ExceptionUtil.unwrapThrowable(e); } }
/** * Mybatis拦截器方法 * * @param invocation 拦截器入参 * @return 返回执行结果 * @throws Throwable 抛出异常 */ public Object intercept(Invocation invocation) throws Throwable { if (autoRuntimeDialect) { SqlUtil sqlUtil = getSqlUtil(invocation); return sqlUtil.processPage(invocation); } else { if (autoDialect) { initSqlUtil(invocation); } return sqlUtil.processPage(invocation); } }
该方法中的内容咱们后面再分析。Executor的分析咱们到此,接下来看下PageHelper实现分页的具体过程。
接下来咱们经过代码跟踪来看下具体的分页流程,咱们须要分别从两行代码开始:
PageHelper.startPage(1, 5);
/** * 开始分页 * * @param params */ public static <E> Page<E> startPage(Object params) { Page<E> page = SqlUtil.getPageFromObject(params); //当已经执行过orderBy的时候 Page<E> oldPage = SqlUtil.getLocalPage(); if (oldPage != null && oldPage.isOrderByOnly()) { page.setOrderBy(oldPage.getOrderBy()); } SqlUtil.setLocalPage(page); return page; }
/** * 开始分页 * * @param pageNum 页码 * @param pageSize 每页显示数量 * @param count 是否进行count查询 * @param reasonable 分页合理化,null时用默认配置 */ public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable) { return startPage(pageNum, pageSize, count, reasonable, null); }
/** * 开始分页 * * @param offset 页码 * @param limit 每页显示数量 * @param count 是否进行count查询 */ public static <E> Page<E> offsetPage(int offset, int limit, boolean count) { Page<E> page = new Page<E>(new int[]{offset, limit}, count); //当已经执行过orderBy的时候 Page<E> oldPage = SqlUtil.getLocalPage(); if (oldPage != null && oldPage.isOrderByOnly()) { page.setOrderBy(oldPage.getOrderBy()); } // 这是重点!!! SqlUtil.setLocalPage(page); return page; }
private static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>(); // 将分页信息保存在ThreadLocal中 线程安全! public static void setLocalPage(Page page) { LOCAL_PAGE.set(page); }
session.selectList("com.bobo.UserMapper.query");
public <E> List<E> selectList(String statement) { return this.selectList(statement, null); } public <E> List<E> selectList(String statement, Object parameter) { return this.selectList(statement, parameter, RowBounds.DEFAULT); }
咱们须要回到invoke方法中继续看
/** * Mybatis拦截器方法 * * @param invocation 拦截器入参 * @return 返回执行结果 * @throws Throwable 抛出异常 */ public Object intercept(Invocation invocation) throws Throwable { if (autoRuntimeDialect) { SqlUtil sqlUtil = getSqlUtil(invocation); return sqlUtil.processPage(invocation); } else { if (autoDialect) { initSqlUtil(invocation); } return sqlUtil.processPage(invocation); } }
进入sqlUtil.processPage(invocation);方法
/** * Mybatis拦截器方法 * * @param invocation 拦截器入参 * @return 返回执行结果 * @throws Throwable 抛出异常 */ private Object _processPage(Invocation invocation) throws Throwable { final Object[] args = invocation.getArgs(); Page page = null; //支持方法参数时,会先尝试获取Page if (supportMethodsArguments) { // 从线程本地变量中获取Page信息,就是咱们刚刚设置的 page = getPage(args); } //分页信息 RowBounds rowBounds = (RowBounds) args[2]; //支持方法参数时,若是page == null就说明没有分页条件,不须要分页查询 if ((supportMethodsArguments && page == null) //当不支持分页参数时,判断LocalPage和RowBounds判断是否须要分页 || (!supportMethodsArguments && SqlUtil.getLocalPage() == null && rowBounds == RowBounds.DEFAULT)) { return invocation.proceed(); } else { //不支持分页参数时,page==null,这里须要获取 if (!supportMethodsArguments && page == null) { page = getPage(args); } // 进入查看 return doProcessPage(invocation, page, args); } }
/** * Mybatis拦截器方法 * * @param invocation 拦截器入参 * @return 返回执行结果 * @throws Throwable 抛出异常 */ private Page doProcessPage(Invocation invocation, Page page, Object[] args) throws Throwable { //保存RowBounds状态 RowBounds rowBounds = (RowBounds) args[2]; //获取原始的ms MappedStatement ms = (MappedStatement) args[0]; //判断并处理为PageSqlSource if (!isPageSqlSource(ms)) { processMappedStatement(ms); } //设置当前的parser,后面每次使用前都会set,ThreadLocal的值不会产生不良影响 ((PageSqlSource)ms.getSqlSource()).setParser(parser); try { //忽略RowBounds-不然会进行Mybatis自带的内存分页 args[2] = RowBounds.DEFAULT; //若是只进行排序 或 pageSizeZero的判断 if (isQueryOnly(page)) { return doQueryOnly(page, invocation); } //简单的经过total的值来判断是否进行count查询 if (page.isCount()) { page.setCountSignal(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; } } else { page.setTotal(-1l); } //pageSize>0的时候执行分页查询,pageSize<=0的时候不执行至关于可能只返回了一个count if (page.getPageSize() > 0 && ((rowBounds == RowBounds.DEFAULT && page.getPageNum() > 0) || rowBounds != RowBounds.DEFAULT)) { //将参数中的MappedStatement替换为新的qs page.setCountSignal(null); // 重点是查看该方法 BoundSql boundSql = ms.getBoundSql(args[1]); args[1] = parser.setPageParameter(ms, args[1], boundSql, page); page.setCountSignal(Boolean.FALSE); //执行分页查询 Object result = invocation.proceed(); //获得处理结果 page.addAll((List) result); } } finally { ((PageSqlSource)ms.getSqlSource()).removeParser(); } //返回结果 return page; }
进入 BoundSql boundSql = ms.getBoundSql(args[1])方法跟踪到PageStaticSqlSource类中的
@Override protected BoundSql getPageBoundSql(Object parameterObject) { String tempSql = sql; String orderBy = PageHelper.getOrderBy(); if (orderBy != null) { tempSql = OrderByParser.converToOrderBySql(sql, orderBy); } tempSql = localParser.get().getPageSql(tempSql); return new BoundSql(configuration, tempSql, localParser.get().getPageParameterMapping(configuration, original.getBoundSql(parameterObject)), parameterObject); }
也能够看Oracle的分页实现
至此咱们发现PageHelper分页的实现原来是在咱们执行SQL语句以前动态的将SQL语句拼接了分页的语句,从而实现了从数据库中分页获取的过程。