mybais分页

Mybatis 的物理分页是应用中的一个难点,特别是配合检索和排序功能叠加时更是如此。
我在最近的项目中开发了这个通用分页器,过程中参考了站内不少好文章,新年第一天,特此发文回馈网站。
特别鸣谢 paginator项目 (https://github.com/miemiedev/mybatis-paginator ) ,阅读源码帮助很大。

【背景】
项目框架是 SpringMVC+Mybatis, 需求是想采用自定义的分页标签,同时,要尽量少的影响业务程序开发的。
如果你已经使用了JS框架( 如:Ext,EasyUi等)自带的分页机能,是属于前端分页,不在本文讨论范围。

【关于问题】
大多数分页器会使用在查询页面,要考虑以下问题:
1)分页时是要随时带有最近一次查询条件
2)不能影响现有的sql,类似aop的效果
3)mybatis提供了通用的拦截接口,要选择适当的拦截方式和时点
4)尽量少的影响现有service等接口

【关于依赖库】
Google Guava    作为基础工具包
Commons JXPath  用于对象查询  (1/23日版改善后,不再需要)
Jackson  向前台传送Json格式数据转换用

【关于适用数据库】
现在只适用mysql
(如果需要用在其他数据库可参考 paginator的Dialect部分,改动都不大)

首先是Page类,比较简单,保存分页相关的所有信息,涉及到分页算法。虽然“其貌不扬”,但很重要。后面会看到这个page类对象会以“信使”的身份出现在全部与分页相关的地方。
Java代码  收藏代码
/**
* 封装分页数据
*/ 
import java.util.List; 
import java.util.Map; 
 
import org.codehaus.jackson.map.ObjectMapper; 
import org.slf4j.Logger; 
import org.slf4j.LoggerFactory; 
 
import com.google.common.base.Joiner; 
import com.google.common.collect.Lists; 
import com.google.common.collect.Maps; 
 
public class Page { 
 
  private static final Logger logger = LoggerFactory.getLogger(Page.class); 
  private static ObjectMapper mapper = new ObjectMapper(); 
 
  public static String DEFAULT_PAGESIZE = "10"; 
  private int pageNo;          //当前页码 
  private int pageSize;        //每页行数 
  private int totalRecord;      //总记录数 
  private int totalPage;        //总页数 
  private Map<String, String> params;  //查询条件 
  private Map<String, List<String>> paramLists;  //数组查询条件 
  private String searchUrl;      //Url地址 
  private String pageNoDisp;       //可以显示的页号(分隔符"|",总页数变更时更新) 
 
  private Page() { 
    pageNo = 1; 
    pageSize = Integer.valueOf(DEFAULT_PAGESIZE); 
    totalRecord = 0; 
    totalPage = 0; 
    params = Maps.newHashMap(); 
    paramLists = Maps.newHashMap(); 
    searchUrl = ""; 
    pageNoDisp = ""; 
  } 
    
  public static Page newBuilder(int pageNo, int pageSize, String url){ 
    Page page = new Page(); 
    page.setPageNo(pageNo); 
    page.setPageSize(pageSize); 
    page.setSearchUrl(url); 
    return page; 
  } 
   
  /**
   * 查询条件转JSON
   */ 
  public String getParaJson(){ 
    Map<String, Object> map = Maps.newHashMap(); 
    for (String key : params.keySet()){ 
      if ( params.get(key) != null  ){ 
        map.put(key, params.get(key)); 
      } 
    } 
    String json=""; 
    try { 
      json = mapper.writeValueAsString(map); 
    } catch (Exception e) { 
      logger.error("转换JSON失败", params, e); 
    } 
    return json; 
  } 
 
  /**
   * 数组查询条件转JSON
   */ 
  public String getParaListJson(){ 
    Map<String, Object> map = Maps.newHashMap(); 
    for (String key : paramLists.keySet()){ 
      List<String> lists = paramLists.get(key); 
      if ( lists != null && lists.size()>0 ){ 
        map.put(key, lists); 
      } 
    } 
    String json=""; 
    try { 
      json = mapper.writeValueAsString(map); 
    } catch (Exception e) { 
      logger.error("转换JSON失败", params, e); 
    } 
    return json; 
  } 
 
  /**
   * 总件数变化时,更新总页数并计算显示样式
   */ 
  private void refreshPage(){ 
    //总页数计算 
    totalPage = totalRecord%pageSize==0 ? totalRecord/pageSize : (totalRecord/pageSize + 1); 
    //防止超出最末页(浏览途中数据被删除的情况) 
    if ( pageNo > totalPage && totalPage!=0){ 
        pageNo = totalPage; 
    } 
    pageNoDisp = computeDisplayStyleAndPage(); 
  } 
   
  /**
   * 计算页号显示样式
   *  这里实现以下的分页样式("[]"代表当前页号),可根据项目需求调整
   *   [1],2,3,4,5,6,7,8..12,13
   *   1,2..5,6,[7],8,9..12,13
   *   1,2..6,7,8,9,10,11,12,[13]
   */ 
  private String computeDisplayStyleAndPage(){ 
    List<Integer> pageDisplays = Lists.newArrayList(); 
    if ( totalPage <= 11 ){ 
      for (int i=1; i<=totalPage; i++){ 
        pageDisplays.add(i); 
      } 
    }else if ( pageNo < 7 ){ 
      for (int i=1; i<=8; i++){ 
        pageDisplays.add(i); 
      } 
      pageDisplays.add(0);// 0 表示 省略部分(下同) 
      pageDisplays.add(totalPage-1);        
      pageDisplays.add(totalPage); 
    }else if ( pageNo> totalPage-6 ){ 
      pageDisplays.add(1); 
      pageDisplays.add(2); 
      pageDisplays.add(0); 
      for (int i=totalPage-7; i<=totalPage; i++){ 
        pageDisplays.add(i); 
      }        
    }else{ 
      pageDisplays.add(1); 
      pageDisplays.add(2); 
      pageDisplays.add(0); 
      for (int i=pageNo-2; i<=pageNo+2; i++){ 
        pageDisplays.add(i); 
      } 
      pageDisplays.add(0); 
      pageDisplays.add(totalPage-1); 
      pageDisplays.add(totalPage); 
    } 
    return Joiner.on("|").join(pageDisplays.toArray()); 
  } 
  
  public int getPageNo() { 
     return pageNo; 
  } 
  
  public void setPageNo(int pageNo) { 
     this.pageNo = pageNo; 
  } 
  
  public int getPageSize() { 
     return pageSize; 
  } 
  
  public void setPageSize(int pageSize) { 
     this.pageSize = pageSize; 
  } 
  
  public int getTotalRecord() { 
     return totalRecord; 
  } 
  
  public void setTotalRecord(int totalRecord) { 
    this.totalRecord = totalRecord; 
    refreshPage();      
  } 
 
  public int getTotalPage() { 
     return totalPage; 
  } 
  
  public void setTotalPage(int totalPage) { 
     this.totalPage = totalPage; 
  } 
  
  public Map<String, String> getParams() { 
     return params; 
  } 
    
  public void setParams(Map<String, String> params) { 
     this.params = params; 
  } 
   
  public Map<String, List<String>> getParamLists() { 
    return paramLists; 
  } 
 
  public void setParamLists(Map<String, List<String>> paramLists) { 
    this.paramLists = paramLists; 
  } 
  public String getSearchUrl() { 
    return searchUrl; 
  } 
  public void setSearchUrl(String searchUrl) { 
    this.searchUrl = searchUrl; 
  } 
  public String getPageNoDisp() { 
    return pageNoDisp; 
  } 
  public void setPageNoDisp(String pageNoDisp) { 
    this.pageNoDisp = pageNoDisp; 
  } 


然后是最核心的拦截器了。涉及到了mybatis的核心功能,期间阅读大量mybatis源码几经修改重构,辛苦自不必说。
核心思想是将拦截到的select语句,改装成select count(*)语句,执行之得到,总数据数。再根据page中的当前页号算出limit值,拼接到select语句后。
为简化代码使用了Commons JXPath 包,做对象查询。
Java代码  收藏代码
/**
* 分页用拦截器
*/ 
import java.sql.Connection; 
import java.sql.PreparedStatement; 
import java.sql.ResultSet; 
import java.util.Properties; 
 
import org.apache.commons.jxpath.JXPathContext; 
import org.apache.commons.jxpath.JXPathNotFoundException; 
import org.apache.ibatis.executor.Executor; 
import org.apache.ibatis.executor.parameter.DefaultParameterHandler; 
import org.apache.ibatis.mapping.BoundSql; 
import org.apache.ibatis.mapping.MappedStatement; 
import org.apache.ibatis.mapping.MappedStatement.Builder; 
import org.apache.ibatis.mapping.ParameterMapping; 
import org.apache.ibatis.mapping.SqlSource; 
import org.apache.ibatis.plugin.Interceptor; 
import org.apache.ibatis.plugin.Intercepts; 
import org.apache.ibatis.plugin.Invocation; 
import org.apache.ibatis.plugin.Plugin; 
import org.apache.ibatis.plugin.Signature; 
import org.apache.ibatis.session.ResultHandler; 
import org.apache.ibatis.session.RowBounds; 
 
@Intercepts({@Signature(type=Executor.class,method="query",args={ MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class })}) 
public class PageInterceptor implements Interceptor{ 
 
  public Object intercept(Invocation invocation) throws Throwable { 
     
    //当前环境 MappedStatement,BoundSql,及sql取得 
    MappedStatement mappedStatement=(MappedStatement)invocation.getArgs()[0];     
    Object parameter = invocation.getArgs()[1];  
    BoundSql boundSql = mappedStatement.getBoundSql(parameter);  
    String originalSql = boundSql.getSql().trim(); 
    Object parameterObject = boundSql.getParameterObject(); 
 
    //Page对象获取,“信使”到达拦截器! 
    Page page = searchPageWithXpath(boundSql.getParameterObject(),".","page","*/page"); 
 
    if(page!=null ){ 
      //Page对象存在的场合,开始分页处理 
      String countSql = getCountSql(originalSql); 
      Connection connection=mappedStatement.getConfiguration().getEnvironment().getDataSource().getConnection()  ;           
      PreparedStatement countStmt = connection.prepareStatement(countSql);   
      BoundSql countBS = copyFromBoundSql(mappedStatement, boundSql, countSql); 
      DefaultParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, parameterObject, countBS); 
      parameterHandler.setParameters(countStmt); 
      ResultSet rs = countStmt.executeQuery(); 
      int totpage=0; 
      if (rs.next()) {   
        totpage = rs.getInt(1);   
      } 
      rs.close();   
      countStmt.close();   
      connection.close(); 
       
      //分页计算 
      page.setTotalRecord(totpage); 
       
      //对原始Sql追加limit 
      int offset = (page.getPageNo() - 1) * page.getPageSize(); 
      StringBuffer sb = new StringBuffer(); 
      sb.append(originalSql).append(" limit ").append(offset).append(",").append(page.getPageSize()); 
      BoundSql newBoundSql = copyFromBoundSql(mappedStatement, boundSql, sb.toString()); 
      MappedStatement newMs = copyFromMappedStatement(mappedStatement,new BoundSqlSqlSource(newBoundSql));   
      invocation.getArgs()[0]= newMs;   
    } 
    return invocation.proceed(); 
     
  } 
   
  /**
   * 根据给定的xpath查询Page对象
   */ 
  private Page searchPageWithXpath(Object o,String... xpaths) { 
    JXPathContext context = JXPathContext.newContext(o); 
    Object result; 
    for(String xpath : xpaths){ 
      try { 
        result = context.selectSingleNode(xpath); 
      } catch (JXPathNotFoundException e) { 
        continue; 
      } 
      if ( result instanceof Page ){ 
        return (Page)result; 
      } 
    } 
    return null; 
  } 
 
  /**
   * 复制MappedStatement对象
   */ 
  private MappedStatement copyFromMappedStatement(MappedStatement ms,SqlSource newSqlSource) { 
    Builder builder = new Builder(ms.getConfiguration(),ms.getId(),newSqlSource,ms.getSqlCommandType()); 
     
    builder.resource(ms.getResource()); 
    builder.fetchSize(ms.getFetchSize()); 
    builder.statementType(ms.getStatementType()); 
    builder.keyGenerator(ms.getKeyGenerator()); 
    builder.keyProperty(ms.getKeyProperty()); 
    builder.timeout(ms.getTimeout()); 
    builder.parameterMap(ms.getParameterMap()); 
    builder.resultMaps(ms.getResultMaps()); 
    builder.resultSetType(ms.getResultSetType()); 
    builder.cache(ms.getCache()); 
    builder.flushCacheRequired(ms.isFlushCacheRequired()); 
    builder.useCache(ms.isUseCache()); 
     
    return builder.build(); 
  } 
 
  /**
   * 复制BoundSql对象
   */ 
  private BoundSql copyFromBoundSql(MappedStatement ms, BoundSql boundSql, String sql) { 
    BoundSql newBoundSql = new BoundSql(ms.getConfiguration(),sql, boundSql.getParameterMappings(), boundSql.getParameterObject()); 
    for (ParameterMapping mapping : boundSql.getParameterMappings()) { 
        String prop = mapping.getProperty(); 
        if (boundSql.hasAdditionalParameter(prop)) { 
            newBoundSql.setAdditionalParameter(prop, boundSql.getAdditionalParameter(prop)); 
        } 
    } 
    return newBoundSql; 
  } 
 
  /**
   * 根据原Sql语句获取对应的查询总记录数的Sql语句
   */ 
  private String getCountSql(String sql) { 
    return "SELECT COUNT(*) FROM (" + sql + ") aliasForPage"; 
  } 
 
  public class BoundSqlSqlSource implements SqlSource {   
      BoundSql boundSql;   
      public BoundSqlSqlSource(BoundSql boundSql) {   
        this.boundSql = boundSql;   
      }   
      public BoundSql getBoundSql(Object parameterObject) {   
        return boundSql;   
      }   
    }   
  public Object plugin(Object arg0) { 
     return Plugin.wrap(arg0, this); 
  } 
  public void setProperties(Properties arg0) { 
  } 


到展示层终于可以轻松些了,使用了文件标签来简化前台开发。
采用临时表单提交,CSS使用了Bootstrap。
Html代码  收藏代码
<%@tag pageEncoding="UTF-8"%> 
<%@ attribute name="page" type="cn.com.intasect.ots.common.utils.Page" required="true"%> 
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> 
 
<% 
int current =  page.getPageNo(); 
int begin = 1; 
int end = page.getTotalPage(); 
 
request.setAttribute("current", current); 
request.setAttribute("begin", begin); 
request.setAttribute("end", end); 
request.setAttribute("pList", page.getPageNoDisp()); 
 
%> 
<script type="text/javascript"> 
  var paras = '<%=page.getParaJson()%>'; 
  var paraJson = eval('(' + paras + ')'); 
 
  //将提交参数转换为JSON 
  var paraLists = '<%=page.getParaListJson()%>'; 
  var paraListJson = eval('(' + paraLists + ')'); 
  function pageClick( pNo ){ 
    paraJson["pageNo"] = pNo; 
    paraJson["pageSize"] = "<%=page.getPageSize()%>"; 
     
    var jsPost = function(action, values, valueLists) { 
      var id = Math.random(); 
      document.write('<form id="post' + id + '" name="post'+ id +'" action="' + action + '" method="post">'); 
      for (var key in values) { 
        document.write('<input type="hidden" name="' + key + '" value="' + values[key] + '" />'); 
      } 
      for (var key2 in valueLists) { 
        for (var index in valueLists[key2]) { 
          document.write('<input type="hidden" name="' + key2 + '" value="' + valueLists[key2][index] + '" />'); 
        } 
      } 
      document.write('</form>');     
      document.getElementById('post' + id).submit(); 
    } 
     
    //发送POST 
    jsPost("<%=page.getSearchUrl()%>", paraJson, paraListJson); 
  } 
</script> 
<div class="page-pull-right"> 
  <% if (current!=1 && end!=0){%> 
    <button class="btn btn-default btn-sm" onclick="pageClick(1)">首页</button> 
    <button class="btn btn-default btn-sm" onclick="pageClick(${current-1})">前页</button> 
  <%}else{%> 
    <button class="btn btn-default btn-sm" >首页</button> 
    <button class="btn btn-default btn-sm" >前页</button> 
  <%} %> 
  <c:forTokens items="${pList}" delims="|" var="pNo"> 
    <c:choose> 
      <c:when test="${pNo == 0}"> 
        <label style="font-size: 10px; width: 20px; text-align: center;">•••</label> 
      </c:when> 
      <c:when test="${pNo != current}"> 
        <button class="btn btn-default btn-sm" onclick="pageClick(${pNo})">${pNo}</button> 
      </c:when> 
      <c:otherwise> 
        <button class="btn btn-primary btn-sm" style="font-weight:bold;">${pNo}</button> 
      </c:otherwise> 
    </c:choose> 
  </c:forTokens> 
  <% if (current<end && end!=0){%> 
    <button class="btn btn-default btn-sm" onclick="pageClick(${current+1})">后页</button> 
    <button class="btn btn-default btn-sm" onclick="pageClick(${end})">末页</button> 
  <%}else{%> 
    <button class="btn btn-default btn-sm">后页</button> 
    <button class="btn btn-default btn-sm">末页</button> 
  <%} %> 
</div> 
注意“信使”在这里使出了浑身解数,7个主要的get方法全部用上了。
Java代码  收藏代码
page.getPageNo()        //当前页号 
page.getTotalPage()     //总页数 
page.getPageNoDisp()    //可以显示的页号 
page.getParaJson()      //查询条件 
page.getParaListJson()  //数组查询条件 
page.getPageSize()      //每页行数 
page.getSearchUrl()     //Url地址(作为action名称) 
到这里三个核心模块完成了。然后是拦截器的注册。

【拦截器的注册】
需要在mybatis-config.xml 中加入拦截器的配置
Java代码  收藏代码
<plugins> 
   <plugin interceptor="cn.com.dingding.common.utils.PageInterceptor">   
   </plugin> 
</plugins>   

【相关代码修改】
首先是后台代码的修改,Controller层由于涉及到查询条件,需要修改的内容较多。
1)入参需增加 pageNo,pageSize 两个参数
2)根据pageNo,pageSize 及你的相对url构造page对象。(
3)最重要的是将你的其他入参(查询条件)保存到page中
4)Service层的方法需要带着page这个对象(最终目的是传递到sql执行的入参,让拦截器识别出该sql需要分页,同时传递页号)
5)将page对象传回Mode中
修改前
Java代码  收藏代码
@RequestMapping(value = "/user/users") 
public String list( 
  @ModelAttribute("name") String name, 
  @ModelAttribute("levelId") String levelId, 
  @ModelAttribute("subjectId") String subjectId, 
  Model model) { 
  model.addAttribute("users",userService.selectByNameLevelSubject( 
          name, levelId, subjectId)); 
  return USER_LIST_JSP; 

修改后
Java代码  收藏代码
@RequestMapping(value = "/user/users") 
public String list( 
  @RequestParam(required = false, defaultValue = "1") int pageNo, 
  @RequestParam(required = false, defaultValue = "5") int pageSize, 
  @ModelAttribute("name") String name, 
  @ModelAttribute("levelId") String levelId, 
  @ModelAttribute("subjectId") String subjectId, 
  Model model) { 
  // 这里是“信使”诞生之地,一出生就加载了很多重要信息! 
  Page page = Page.newBuilder(pageNo, pageSize, "users"); 
  page.getParams().put("name", name);           //这里再保存查询条件 
  page.getParams().put("levelId", levelId); 
  page.getParams().put("subjectId", subjectId); 
     
  model.addAttribute("users",userService.selectByNameLevelSubject( 
          name, levelId, subjectId, page)); 
  model.addAttribute("page", page);             //这里将page返回前台 
  return USER_LIST_JSP; 

注意pageSize的缺省值决定该分页的每页数据行数 ,实际项目更通用的方式是使用配置文件指定。

Service层
拦截器可以自动识别在Map或Bean中的Page对象。
如果使用Bean需要在里面增加一个page项目,Map则比较简单,以下是例子。
Java代码  收藏代码
@Override 
public List<UserDTO> selectByNameLevelSubject(String name, String levelId, String subjectId, Page page) { 
  Map<String, Object> map = Maps.newHashMap(); 
  levelId = DEFAULT_SELECTED.equals(levelId)?null: levelId; 
  subjectId = DEFAULT_SELECTED.equals(subjectId)?null: subjectId; 
  if (name != null && name.isEmpty()){ 
    name = null; 
  } 
  map.put("name", name); 
  map.put("levelId", levelId); 
  map.put("subjectId", subjectId); 
  map.put("page", page);             //MAP的话加这一句就OK 
  return userMapper.selectByNameLevelSubject(map); 


前台页面方面,由于使用了标签,在适当的位置加一句就够了。
Html代码  收藏代码
<tags:page page="${page}"/> 
“信使”page在这里进入标签,让分页按钮最终展现。

至此,无需修改一句sql,完成分页自动化。

【效果图】


【总结】
现在回过头来看下最开始提出的几个问题:
1)分页时是要随时带有最近一次查询条件
  回答:在改造Controller层时,通过将提交参数设置到 Page对象的 Map<String, String> params(单个基本型参数) 和 Map<String, List<String>> paramLists(数组基本型)解决。
  顺便提一下,例子中没有涉及参数是Bean的情况,实际应用中应该比较常见。简单的方法是将Bean转换层Map后加入到params。

2)不能影响现有的sql,类似aop的效果
  回答:利用Mybatis提供了 Interceptor 接口,拦截后改头换面去的件数并计算limit值,自然能神不知鬼不觉。

3)mybatis提供了通用的拦截接口,要选择适当的拦截方式和时点
  回答:@Signature(method = "query", type = Executor.class, args = {  MappedStatement.class, Object.class, RowBounds.class,  ResultHandler.class }) 只拦截查询语句,其他增删改查不会影响。

4)尽量少的影响现有service等接口
  回答:这个自认为本方案做的还不够好,主要是Controller层改造上,感觉代码量还比较大。如果有有识者知道更好的方案还请多指教。

【遗留问题】
1)一个“明显”的性能问题,是每次检索前都要去 select count(*)一次。在很多时候(数据变化不是特别敏感的场景)是不必要的。调整也不难,先Controller参数增加一个 totalRecord 总记录数 ,在稍加修改一下Page相关代码即可。
2)要排序怎么办?本文并未讨论排序,但是方法是类似的。以上面代码为基础,可以较容易地实现一个通用的排序标签。

===================================== 分割线 (1/8)=======================================
对于Controller层需要将入参传入Page对象的问题已经进行了改善,思路是自动从HttpServletRequest 类中提取入残,减低了分页代码的侵入性,详细参看文章 http://duanhengbin.iteye.com/blogs/2001142
===================================== 分割线 (1/23)=======================================
再次改善,使用ThreadLocal类封装Page对象,让Service层等无需传Page对象,减小了侵入性。拦截器也省去了查找Page对象的动作,性能也同时改善。整体代码改动不大。
===================================== 分割线 (2/21)=======================================
今天比较闲,顺便聊下这个分页的最终版,当然从来只有不断变化的需求,没有完美的方案,这里所说的最终版其实是一个优化后的“零侵入”的方案。为避免代码混乱还是只介绍思路。在上一个版本(1/23版)基础上有两点改动:
一是增加一个配置文件,按Url 配置初始的每页行数。如下面这样(pagesize 指的是每页行数):
Xml代码  收藏代码
<pager url="/user/users" pagesize="10" /> 
二是增加一个过滤器,并将剩下的位于Control类中 唯一侵入性的分页相关代码移入过滤器。发现当前的 Url  在配置文件中有匹配是就构造Page对象,并加入到Response中。

使用最终版后,对于开发者需要分页时,只要在配置文件中加一行,并在前端页面上加一个分页标签即可,其他代码,SQL等都不需要任何改动,可以说简化到了极限。

【技术列表】
总结下最终方案用到的技术:
Mybatis 提供的拦截器接口实现(实现分页sql自动 select count 及limit 拼接)
Servlet过滤器+ThreadLocal  (生成线程共享的Page对象)
标签文件   (实现前端共通的分页效果)
临时表单提交 (减少页面体积)
【其他分页方案比较】
时下比较成熟的 JPA 的分页方案,(主要应用在 Hibernate + Spring Data 的场合),主要切入点在DAO层,而Controller等各层接口依然需要带着pageNumber,pageSize 这些的参数,另外框架开发者还要掌握一些必须的辅助类,如:
  org.springframework.data.repository.PagingAndSortingRepository    可分页DAO基类
  org.springframework.data.domain.Page            抽取结果封装类
  org.springframework.data.domain.Pageable     分页信息类
比较来看 本方案 做到了分页与业务逻辑的完全解耦,开发者无需关注分页,全部通过配置实现。通过这个例子也可以反映出Mybatis在底层开发上有其独特的优势。

【备选方案】
最后再闲扯下,上面的最终案是基于 Url 配置的,其实也可以基于方法加自定义注解来做。这样配置文件省了,但是要增加一个注解解析类。注解中参数 为初始的每页行数。估计注解fans会喜欢,如下面的样子:
Java代码  收藏代码
@RequestMapping(value = "/user/users") 
@Pagination(size=10) 
public String list( 
... 
同样与过滤器配合使用,只是注解本身多少还是有“侵入性”。在初始行数基本不会变更时,这个比较直观的方案也是不错的选择。大家自行决定吧。

你可能感兴趣的:(mybatis)