其实我们是希望把获取分页信息的工作给统一起来,简洁代码,减少工作量
我们可以利用 mybatis 的插件功能(拦截器)来处理分页,这里我们尝试着自己去写一个简单的分页插件,后面也有介绍一个优秀的 mybatis 分页插件 PageHelper 的使用方法
在 mybatis xml 配置文件中,我们可以配置插件(plugins),mybatis 允许你在已映射语句执行过程中的某一点进行拦截调用,这样我们可以通过拦截查询方法,添加分页查询条件,包装查询结果
通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可
/*
* 指定方法签名:
* 下面的配置表面 将会拦截在 Executor 实例中所有的 “query” 方法调用,
* 这里的 Executor 是负责执行低层映射语句的内部对象
*/
@Intercepts({@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class PageInterceptor implements Interceptor {
/**
* 拦截目标对象中目标方法的执行
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
//TODO 覆盖目标方法,我们在这里覆盖目标方法
// 执行目标方法,并返回结果(这里没有处理)
return invocation.proceed();
}
/**
* 包装目标对象,即为目标对象创建一个代理对象
*/
@Override
public Object plugin(Object target) {
// 借助 Plugin 的 wrap(Object target,Interceptor interceptor); 包装我们的目标对象
// target: 目标对象, interceptor: 拦截器, this 表示使用当前拦截器
return Plugin.wrap(target, this);
}
/**
* 获取插件注册时,传入的property属性
*/
@Override
public void setProperties(Properties properties) {
// TODO
}
}
同时在 mybatis的配置文件中注册插件
<plugins>
<plugin interceptor="com.brave.interceptor.PageInterceptor">
plugin>
plugins>
上面的只是先创建了一个拦截器,还没有做具体的处理,现在我们先理清楚需要做的事情
查询的时候,我们需要传入分页条件,这里我们创建一个 IPage 接口,为了规范统一
public interface IPage {
/**
* 当前页
*/
Integer getPageNum();
/**
* 每页数量
*/
Integer getPageSize();
/**
* 开始行
*/
Integer getStartRow();
/**
* 排序条件
*/
String getOrderBy();
}
并实现一个简单类 PageConfig.java
public class PageConfig implements IPage {
private Integer pageNum;
private Integer pageSize;
private Integer startRow;
private String orderBy;
public PageConfig() {
this(1,10);
}
public PageConfig(Integer pageNum,Integer pageSize) {
this.pageNum = pageNum;
this.pageSize = pageSize;
this.startRow = (this.pageNum -1) * this.pageSize;
}
public Integer getPageNum() {
return pageNum;
}
public void setPageNum(Integer pageNum) {
this.pageNum = pageNum;
this.startRow = (this.pageNum -1) * this.pageSize;
}
public Integer getPageSize() {
return pageSize;
}
public void setPageSize(Integer pageSize) {
this.pageSize = pageSize;
this.startRow = (this.pageNum -1) * this.pageSize;
}
public String getOrderBy() {
return orderBy;
}
public void setOrderBy(String orderBy) {
this.orderBy = orderBy;
}
public Integer getStartRow() {
return startRow;
}
public void setStartRow(Integer startRow) {
this.startRow = startRow;
}
}
同时我们需要包装一下返回结果集,mybatis 直接查询返回的结果集类型为 ArrayList,我们要在此基础上加上分页信息
Page.java
public class Page<E> extends ArrayList<E> {
private static final long serialVersionUID = 1L;
private int pageNum; // 页码
private int pageSize; // 每页数量
private long total; // 总记录数
private int pages; // 页数
public Page() {
super();
}
public Page(int pageNum, int pageSize) {
this(pageNum, pageSize, 0);
}
public Page(int pageNum, int pageSize, long total) {
this.pageNum = pageNum;
this.pageSize = pageSize;
this.total = total;
this.pages = (int)(this.pageSize == 0 ? 0
: this.total % this.pageSize == 0 ? this.total / this.pageSize : (this.total / this.pageSize + 1));
}
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 Long getTotal() {
return total;
}
public void setTotal(long total) {
this.total = total;
this.pages = (int)(this.pageSize == 0 ? 0
: this.total % this.pageSize == 0 ? this.total / this.pageSize : (this.total / this.pageSize + 1));
}
public int getPages() {
return pages;
}
public void setPages(int pages) {
this.pages = pages;
}
@Override
public String toString() {
return "Page [pageNum=" + pageNum + ", pageSize=" + pageSize + ", total=" + total + ", pages=" + pages + "]";
}
}
我们再顺一下拦截查询方法之后的流程
这里直接附上实现代码
/*
* 指定方法签名: 下面的配置表面 将会拦截在 Executor 实例中的 “query” 方法调用, 这里的 Executor 是负责执行低层映射语句的内部对象
*/
@SuppressWarnings({"rawtypes", "unchecked"})
@Intercepts({ @Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class PageInterceptor implements Interceptor {
private final static String COUNT_SUFFIX = "_COUNT"; // count查询语句Id后缀
/**
* 拦截目标对象中目标方法的执行
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获得目标对象
Executor executor = (Executor)invocation.getTarget();
// 获得目标方法的参数
Object[] args = invocation.getArgs();
// MappedStatement表示的是XML中的一个SQL
MappedStatement mappedStatement = (MappedStatement)args[0];
Object parameter = args[1];
System.out.println(parameter.getClass());
RowBounds rowBounds = (RowBounds)args[2];
ResultHandler resultHandler = (ResultHandler)args[3];
BoundSql boundSql = mappedStatement.getBoundSql(parameter);
CacheKey cacheKey = executor.createCacheKey(mappedStatement, parameter, rowBounds, boundSql);
//获取分页参数
IPage pageConfig = getPageConfig(parameter);
if (pageConfig != null) {
return pageQuery(executor, mappedStatement, parameter, resultHandler, boundSql, cacheKey,
pageConfig);
} else {
// 不用分页,执行目标方法
return invocation.proceed();
}
}
/**
* 分页查询
*
* @param executor
* @param ms
* @param parameter
* @param resultHandler
* @param boundSql
* @param cacheKey
* @param pageConfig
* @return
* @throws SQLException
*/
private Object pageQuery(Executor executor, MappedStatement ms, Object parameter,
ResultHandler resultHandler, BoundSql boundSql, CacheKey cacheKey, IPage pageConfig) throws SQLException {
Page page = new Page(pageConfig.getPageNum(), pageConfig.getPageSize());
// 查询总数
Long count = countQuery(executor, ms, parameter, resultHandler, boundSql);
// 总数大于 0 ,开始分页查询
if (count != null && count > 0) {
page.setTotal(count);
// 生成分页的缓存 key
CacheKey pageKey = cacheKey;
// 获取分页 sql
StringBuilder pageSql = new StringBuilder(boundSql.getSql());
if (pageConfig.getStartRow() == 0) {
pageSql.append(" LIMIT ").append(pageConfig.getPageSize());
} else {
pageSql.append(" LIMIT ").append(pageConfig.getStartRow()).append(",").append(pageConfig.getPageSize());
}
BoundSql pageBoundSql =
new BoundSql(ms.getConfiguration(), pageSql.toString(), boundSql.getParameterMappings(), parameter);
List list = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
page.addAll(list);
}
return page;
}
/**
* count 查询,获取总记录数
*
* @param executor
* @param mappedStatement
* @param parameter
* @param resultHandler
* @param boundSql
* @return
* @throws SQLException
*/
private Long countQuery(Executor executor, MappedStatement mappedStatement, Object parameter,
ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
// 创建新的 MappedStatement 用于count查询
MappedStatement countMs = newCountMappedStatement(mappedStatement);
// 创建 count 查询的缓存 key
CacheKey countKey = executor.createCacheKey(countMs, parameter, RowBounds.DEFAULT, boundSql);
// 创建 countBoundSql
String countSql = "SELECT COUNT(*) FROM (" + boundSql.getSql() + ") T";
BoundSql countBoundSql =
new BoundSql(countMs.getConfiguration(), countSql, boundSql.getParameterMappings(), parameter);
// 执行 count 查询
Object countResult =
executor.query(countMs, parameter, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql);
Long count = (Long)((List)countResult).get(0);
return count;
}
/**
* 新建用于count查询的MappedStatement
*
* @param ms
* @return
*/
private MappedStatement newCountMappedStatement(MappedStatement ms) {
String countMsId = ms.getId() + COUNT_SUFFIX;
MappedStatement.Builder builder =
new MappedStatement.Builder(ms.getConfiguration(), countMsId, ms.getSqlSource(), ms.getSqlCommandType());
builder.resource(ms.getResource());
builder.fetchSize(ms.getFetchSize());
builder.statementType(ms.getStatementType());
builder.keyGenerator(ms.getKeyGenerator());
builder.resultSetType(ms.getResultSetType());
builder.cache(ms.getCache());
builder.flushCacheRequired(ms.isFlushCacheRequired());
builder.useCache(ms.isUseCache());
builder.timeout(ms.getTimeout());
builder.parameterMap(ms.getParameterMap());
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());
}
// 设置返回结果为 Long
List<ResultMap> resultMaps = new ArrayList<ResultMap>();
ResultMap resultMap =
new ResultMap.Builder(ms.getConfiguration(), ms.getId(), Long.class, new ArrayList<ResultMapping>(0))
.build();
resultMaps.add(resultMap);
builder.resultMaps(resultMaps);
return builder.build();
}
/**
* 获取分页信息
*
* @param parameter
* @param rowBounds
* @return
*/
private IPage getPageConfig(Object parameter) {
IPage pageConfig = null;
if (parameter instanceof IPage) {
pageConfig = (IPage)parameter;
}else if(parameter instanceof MapperMethod.ParamMap) {
MapperMethod.ParamMap paramMap = (MapperMethod.ParamMap)parameter;
for(Object value:paramMap.values()) {
if (value instanceof IPage) {
pageConfig = (IPage)value;
break;
}
}
}
return pageConfig;
}
/**
* 包装目标对象,即为目标对象创建一个代理对象
*/
@Override
public Object plugin(Object target) {
// 借助 Plugin 的 wrap(Object target,Interceptor interceptor); 包装我们的目标对象
// target: 目标对象, interceptor: 拦截器, this 表示使用当前拦截器
return Plugin.wrap(target, this);
}
/**
* 获取插件注册时,传入的property属性
*/
@Override
public void setProperties(Properties properties) {
}
}
要使用插件,记得在 mybatis xml 中配置plugin
<plugins>
<plugin interceptor="com.brave.interceptor.PageInterceptor">
plugin>
plugins>
我这个例子适用于使用 mapper 接口,只要接口方法有 IPage 的分页查询参数,就会分页查询
public interface UpmsUserMapper {
List<UpmsUser> selectUser(UpmsUser upmsUser, PageConfig pageConfig);
List<UpmsUser> selectUser(IPage pageConfig);
}
我们创建测试类测试一下结果
@RunWith(SpringJUnit4ClassRunner.class) //使用junit4进行测试
@ContextConfiguration ("/conf/spring/applicationContext*.xml")
public class UpmsUserMapperTest {
@Autowired
private UpmsUserMapper upmsUserMapper;
@Test
public void testSelectUserUpmsUserPageConfig() {
PageConfig pageConfig = new PageConfig(1, 2);
List<UpmsUser> list = upmsUserMapper.selectUser(new UpmsUser(), pageConfig);
System.out.println(list.getClass());
System.out.println(list.toString());
for (UpmsUser upmsUser : list) {
System.out.println(upmsUser.toString());
}
}
}
测试结果如下
class com.brave.page.Page
Page [pageNum=1, pageSize=2, total=3, pages=2]
[userId=10001][loginname=zou][password=123456][locked=null]
[userId=10002][loginname=zou][password=123456][locked=null]
上面的例子,只是简陋的实现一个分页插件,明白大概的流程。下面来介绍分页插件 PageHelper 的使用
PageHelper 的 github 地址 https://github.com/pagehelper/Mybatis-PageHelper
<dependency>
<groupId>com.github.pagehelpergroupId>
<artifactId>pagehelperartifactId>
<version>5.1.8version>
dependency>
由于使用了 sql 解析工具,还会引入 jsqlparser.jar 包,如果不是使用 maven,还需要自己导入 pagehelper 依赖版本一致的 jsqlparser.jar 包
在 mybatis 配置文件中添加拦截器插件配置
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<property name="helperDialect" value="mysql"/>
plugin>
plugins>
如果是 spring 项目,也可以在 spring 配置文件的 SqlSessionFactoryBean 里面添加插件配置
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="mapperLocations" value="classpath*:com/brave/dao/mapper/*Mapper.xml" />
<property name="plugins">
<array>
<bean class="com.github.pagehelper.PageInterceptor">
<property name="properties">
<value>
helperDialect=mysql
value>
property>
bean>
array>
property>
bean>
两个地方的配置选其就好,不要同时生效
下面是具体分页插件参数
参数 | 描述 | 默认值 |
---|---|---|
helperDialect | 分页插件会自动检测当前的数据库链接,自动选择合适的分页方式。该属性可以指定分页插件使用哪种方言。配置值:oracle,mysql,mariadb,sqlite,hsqldb,postgresql,db2,sqlserver,informix,h2,sqlserver2012,derby | – |
offsetAsPageNum | 该参数对使用 RowBounds 作为分页参数时有效。设为 true 时,会将 RowBounds 中的 offset 参数当成 pageNum 使用,可以用页码和页面大小两个参数进行分页 | false |
rowBoundsWithCount | 该参数对使用 RowBounds 作为分页参数时有效。设置为true时,使用 RowBounds 分页会进行 count 查询 | false |
pageSizeZero | 设置为 true 时,如果 pageSize=0 或者 RowBounds.limit = 0 就会查询出全部的结果(相当于没有执行分页查询,但是返回结果仍然是 Page 类型) | false |
reasonable | 分页合理化参数。设置为 true 时,pageNum<=0 时会查询第一页, pageNum>pages(超过总数时),会查询最后一页。默认false 时,直接根据参数进行查询 | false |
params | 为了支持startPage(Object params)方法,增加了该参数来配置参数映射,用于从对象中根据属性名取值,可以配置 pageNum,pageSize,count,pageSizeZero,reasonable,不配置映射的用默认值 pageNum=pageNum;pageSize=pageSize;count=countSql;reasonable=reasonable;pageSizeZero=pageSizeZero | |
supportMethodsArguments | 支持通过 Mapper 接口参数来传递分页参数,分页插件会从查询方法的参数值中,自动根据上面 params 配置的字段中取值,查找到合适的值时就会自动分页。 使用方法可以参考测试代码中的 com.github.pagehelper.test.basic 包下的 ArgumentsMapTest 和 ArgumentsObjTest | false |
autoRuntimeDialect | 设置为 true 时,允许在运行时根据多数据源自动识别对应方言的分页 | false |
closeConn | 当使用运行时动态数据源或没有设置 helperDialect 属性自动获取数据库类型时,会自动获取一个数据库连接,通过该属性来设置是否关闭获取的这个连接,默认true关闭,设置为 false 后,不会关闭获取的连接,这个参数的设置要根据自己选择的数据源来决定 | true |
aggregateFunctions(5.1.5+) | 默认为所有常见数据库的聚合函数,允许手动添加聚合函数(影响行数),所有以聚合函数开头的函数,在进行 count 转换时,会套一层。其他函数和列会被替换为 count(0),其中count列可以自己配置。 |
当 offsetAsPageNum=false 的时候,由于 PageNum 问题,RowBounds查询的时候 reasonable 会强制为 false。使用 PageHelper.startPage 方法不受影响。
这些参数是在默认情况(dialect)下有效,默认情况下会使用 PageHelper 方式进行分页,如果想要实现自己的分页逻辑,可以实现 Dialect(com.github.pagehelper.Dialect) 接口,然后配置该属性为实现类的全限定名称。
这里主要是4种使用方法
使用 RowBounds 参数进行分页,这种方式侵入性最小,用只是使用了这个参数,并没有增加其他任何内容
@Repository
public class UpmsUserDaoImpl extends SuperDao implements UpmsUserDao {
@Override
public List<UpmsUser> selectUser(int offset,int limit) {
List<UpmsUser> list = getSqlSession().selectList("com.brave.dao.UpmsUserDao2.selectUser", null, new RowBounds(offset, limit));
return list;
}
}
测试代码
@Test
public void testSelectUser() {
List<UpmsUser> upmsUsers = upmsUserDao.selectUser(0, 10);
for (UpmsUser upmsUser : upmsUsers) {
System.out.println(upmsUser.toString());
}
}
我们在配置插件的时候有两个参数是关于 RowBounds 的
upmsUserDao.selectUser(1, 10)
当 rowBoundsWithCount 设置为true时,会有 count 查询,会获得查询记录总数
@Test
public void testSelectUser() {
Page<UpmsUser> upmsUsers = (Page<UpmsUser>)upmsUserDao.selectUser(1, 10);
System.out.println("总页数:"+upmsUsers.getTotal());
for (UpmsUser upmsUser : upmsUsers) {
System.out.println(upmsUser.toString());
}
}
没有设置或为false的话,total 为 -1
如果不设置 rowBoundsWithCount,也可以通过 PageRowBounds 进行 count 查询。
getSqlSession().selectList("com.brave.dao.UpmsUserDao.selectUser", null, new PageRowBounds (offset, limit))
PageRowBounds 继承 RowBounds,多了 total 属性
另外使用接口的时候也可以增加RowBounds参数
List
selectAll(RowBounds rowBounds);
PageHelper 类有 startPage 和 offsetPage 方法去设置分页参数
在需要进行分页的 mybatis 查询方法前调用 PageHelper.startPage 静态方法即可,紧跟在这个方法后的第一个 mybatis 查询方法会被进行分页。
通常是在 service 的 bean 中调用 dao 的查询方法前 或者是 mapper 接口方法前 调用 PageHelper 的静态方法
@Test
public void testSelectUser2() {
// request: url?pageNum=1&pageSize=10
// 支持 ServletRequest,Map,POJO 对象,需要配合 params 参数
// PageHelper.startPage(request);
PageHelper.startPage(1, 10);
Page<UpmsUser> upmsUsers = (Page<UpmsUser>)upmsUserDao.selectUser();
System.out.println("总页数:" + upmsUsers.getTotal());
for (UpmsUser upmsUser : upmsUsers) {
System.out.println(upmsUser.toString());
}
}
使用 mapper 接口的时候
@Service
public class UpmsUserServiceImpl implements UpmsUserService {
@Autowired
private UpmsUserMapper upmsUserMapper;
@Override
public List<UpmsUser> listUser(int pageNum, int pageSize) {
PageHelper.startPage(pageNum, pageSize);
return upmsUserMapper.selectUser();
}
}
使用 PageInfo
PageInfo 包含了非常全面的分页属性
List<UpmsUser> list = (Page<UpmsUser>)upmsUserDao.selectUser();
PageInfo<UpmsUser> pageInfo = new PageInfo(list);
private int pageNum;//当前页
private int pageSize;//每页的数量
private int size; //当前页的数量
//由于startRow和endRow不常用,这里说个具体的用法
//可以在页面中"显示startRow到endRow 共size条数据"
private int startRow;//当前页面第一个元素在数据库中的行号
private int endRow;//当前页面最后一个元素在数据库中的行号
private int pages;//总页数
private int prePage;//前一页
private int nextPage;//下一页
private boolean isFirstPage = false;//是否为第一页
private boolean isLastPage = false;//是否为最后一页
private boolean hasPreviousPage = false;//是否有前一页
private boolean hasNextPage = false;//是否有下一页
private int navigatePages;//导航页码数
private int[] navigatepageNums;//所有导航页号
private int navigateFirstPage;//导航条上的第一页
private int navigateLastPage;//导航条上的最后一页
想要使用参数方式,需要配置 supportMethodsArguments 参数为 true,同时要配置 params 参数
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<property name="supportMethodsArguments" value="true"/>
<property name="params" value="pageNum=pageNum;pageSize=pageSize;"/>
plugin>
plugins>
可以在使用接口声明时就带上 pageNum 和 pageSize
//存在以下 Mapper 接口方法,你不需要在 xml 处理后两个参数
public interface CountryMapper {
List<Country> selectByPageNumSize(
@Param("upmsUser") UpmsUser upmsUser,
@Param("pageNum") int pageNum,
@Param("pageSize") int pageSize);
}
//配置supportMethodsArguments=true
//在代码中直接调用:
List<Country> list = countryMapper.selectByPageNumSize(user, 1, 10);
也可以把 pageNum 和 pageSize 存在于对象中,如 UpmsUser
//存在以下 Mapper 接口方法,不需要在 xml 处理后两个参数
public interface CountryMapper {
List<Country> selectByPageNumSize(UpmsUser upmsUser);
}
//当 user 中的 pageNum!= null && pageSize!= null 时,会自动分页
List<Country> list = countryMapper.selectByPageNumSize(upmsUser);
两种情况都需要 pageNum 和 pageSize 两个属性同时存在才会分页,且在 xml 中的sql中不需要处理这两个参数
另外还可以使用 ISelect 接口方式方式
Page<Country> page = PageHelper.startPage(1, 10).doSelectPage(new ISelect() {
@Override
public void doSelect() {
upmsUserMapper.selectUser();
}
});
// lambda用法
Page<Country> page = PageHelper.startPage(1, 10).doSelectPage(()-> upmsUserMapper.selectUser());
//也可以直接返回PageInfo,注意doSelectPageInfo方法和doSelectPage
pageInfo = PageHelper.startPage(1, 10).doSelectPageInfo(new ISelect() {
@Override
public void doSelect() {
upmsUserMapper.selectUser();
}
});
//对应的lambda用法
pageInfo = PageHelper.startPage(1, 10).doSelectPageInfo(() -> upmsUserMapper.selectUser());
//count查询,返回一个查询语句的count数
long total = PageHelper.count(new ISelect() {
@Override
public void doSelect() {
upmsUserMapper.selectUser(country);
}
});
//lambda
total = PageHelper.count(()->upmsUserMapper.selectUser(country));
ISelect 接口方式除了可以保证安全外,还特别实现了将查询转换为单纯的 count 查询方式,这个方法可以将任意的查询方法,变成一个 select count(*) 的查询方法。
不安全的时候:
PageHelper 方法使用了静态的 ThreadLocal 参数,分页参数和线程是绑定的。
只要你可以保证在 PageHelper 方法调用后紧跟 MyBatis 查询方法,这就是安全的。因为 PageHelper 在 finally 代码段中自动清除了 ThreadLocal 存储的对象。
如果代码在进入 Executor 前发生异常,就会导致线程不可用,这属于人为的 Bug(例如接口方法和 XML 中的不匹配,导致找不到 MappedStatement 时), 这种情况由于线程不可用,也不会导致 ThreadLocal 参数被错误的使用。
下面这样的代码,就是不安全的用法:
//由于 param1 存在 null 的情况,就会导致 PageHelper 生产了一个分页参数,但是没有被消费,这个参数就会一直保留在这个线程上。当这个线程再次被使用时,就可能导致不该分页的方法去消费这个分页参数,这就产生了莫名其妙的分页
PageHelper.startPage(1, 10);
List<UpmsUser> list;
if(param1 != null){
list = UpmsUserMapper.selectUser(param1);
} else {
list = new ArrayList<UpmsUser>();
}
我们只要把 PageHelper.startPage(1, 10);
方法放到if里面查询语句之前就好了