zoukankan      html  css  js  c++  java
  • 分享一个完整的Mybatis分页解决方案

    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代码  收藏代码
    1. /** 
    2.  * 封装分页数据 
    3.  */  
    4. import java.util.List;  
    5. import java.util.Map;  
    6.   
    7. import org.codehaus.jackson.map.ObjectMapper;  
    8. import org.slf4j.Logger;  
    9. import org.slf4j.LoggerFactory;  
    10.   
    11. import com.google.common.base.Joiner;  
    12. import com.google.common.collect.Lists;  
    13. import com.google.common.collect.Maps;  
    14.   
    15. public class Page {  
    16.   
    17.   private static final Logger logger = LoggerFactory.getLogger(Page.class);  
    18.   private static ObjectMapper mapper = new ObjectMapper();  
    19.   
    20.   public static String DEFAULT_PAGESIZE = "10";  
    21.   private int pageNo;          //当前页码  
    22.   private int pageSize;        //每页行数  
    23.   private int totalRecord;      //总记录数  
    24.   private int totalPage;        //总页数  
    25.   private Map<String, String> params;  //查询条件  
    26.   private Map<String, List<String>> paramLists;  //数组查询条件  
    27.   private String searchUrl;      //Url地址  
    28.   private String pageNoDisp;       //可以显示的页号(分隔符"|",总页数变更时更新)  
    29.   
    30.   private Page() {  
    31.     pageNo = 1;  
    32.     pageSize = Integer.valueOf(DEFAULT_PAGESIZE);  
    33.     totalRecord = 0;  
    34.     totalPage = 0;  
    35.     params = Maps.newHashMap();  
    36.     paramLists = Maps.newHashMap();  
    37.     searchUrl = "";  
    38.     pageNoDisp = "";  
    39.   }  
    40.      
    41.   public static Page newBuilder(int pageNo, int pageSize, String url){  
    42.     Page page = new Page();  
    43.     page.setPageNo(pageNo);  
    44.     page.setPageSize(pageSize);  
    45.     page.setSearchUrl(url);  
    46.     return page;  
    47.   }  
    48.     
    49.   /** 
    50.    * 查询条件转JSON 
    51.    */  
    52.   public String getParaJson(){  
    53.     Map<String, Object> map = Maps.newHashMap();  
    54.     for (String key : params.keySet()){  
    55.       if ( params.get(key) != null  ){  
    56.         map.put(key, params.get(key));  
    57.       }  
    58.     }  
    59.     String json="";  
    60.     try {  
    61.       json = mapper.writeValueAsString(map);  
    62.     } catch (Exception e) {  
    63.       logger.error("转换JSON失败", params, e);  
    64.     }  
    65.     return json;  
    66.   }  
    67.   
    68.   /** 
    69.    * 数组查询条件转JSON 
    70.    */  
    71.   public String getParaListJson(){  
    72.     Map<String, Object> map = Maps.newHashMap();  
    73.     for (String key : paramLists.keySet()){  
    74.       List<String> lists = paramLists.get(key);  
    75.       if ( lists != null && lists.size()>0 ){  
    76.         map.put(key, lists);  
    77.       }  
    78.     }  
    79.     String json="";  
    80.     try {  
    81.       json = mapper.writeValueAsString(map);  
    82.     } catch (Exception e) {  
    83.       logger.error("转换JSON失败", params, e);  
    84.     }  
    85.     return json;  
    86.   }  
    87.   
    88.   /** 
    89.    * 总件数变化时,更新总页数并计算显示样式 
    90.    */  
    91.   private void refreshPage(){  
    92.     //总页数计算  
    93.     totalPage = totalRecord%pageSize==0 ? totalRecord/pageSize : (totalRecord/pageSize + 1);  
    94.     //防止超出最末页(浏览途中数据被删除的情况)  
    95.     if ( pageNo > totalPage && totalPage!=0){  
    96.         pageNo = totalPage;  
    97.     }  
    98.     pageNoDisp = computeDisplayStyleAndPage();  
    99.   }  
    100.     
    101.   /** 
    102.    * 计算页号显示样式 
    103.    *  这里实现以下的分页样式("[]"代表当前页号),可根据项目需求调整 
    104.    *   [1],2,3,4,5,6,7,8..12,13 
    105.    *   1,2..5,6,[7],8,9..12,13 
    106.    *   1,2..6,7,8,9,10,11,12,[13] 
    107.    */  
    108.   private String computeDisplayStyleAndPage(){  
    109.     List<Integer> pageDisplays = Lists.newArrayList();  
    110.     if ( totalPage <= 11 ){  
    111.       for (int i=1; i<=totalPage; i++){  
    112.         pageDisplays.add(i);  
    113.       }  
    114.     }else if ( pageNo < 7 ){  
    115.       for (int i=1; i<=8; i++){  
    116.         pageDisplays.add(i);  
    117.       }  
    118.       pageDisplays.add(0);// 0 表示 省略部分(下同)  
    119.       pageDisplays.add(totalPage-1);         
    120.       pageDisplays.add(totalPage);  
    121.     }else if ( pageNo> totalPage-6 ){  
    122.       pageDisplays.add(1);  
    123.       pageDisplays.add(2);  
    124.       pageDisplays.add(0);  
    125.       for (int i=totalPage-7; i<=totalPage; i++){  
    126.         pageDisplays.add(i);  
    127.       }         
    128.     }else{  
    129.       pageDisplays.add(1);  
    130.       pageDisplays.add(2);  
    131.       pageDisplays.add(0);  
    132.       for (int i=pageNo-2; i<=pageNo+2; i++){  
    133.         pageDisplays.add(i);  
    134.       }  
    135.       pageDisplays.add(0);  
    136.       pageDisplays.add(totalPage-1);  
    137.       pageDisplays.add(totalPage);  
    138.     }  
    139.     return Joiner.on("|").join(pageDisplays.toArray());  
    140.   }  
    141.    
    142.   public int getPageNo() {  
    143.      return pageNo;  
    144.   }  
    145.    
    146.   public void setPageNo(int pageNo) {  
    147.      this.pageNo = pageNo;  
    148.   }  
    149.    
    150.   public int getPageSize() {  
    151.      return pageSize;  
    152.   }  
    153.    
    154.   public void setPageSize(int pageSize) {  
    155.      this.pageSize = pageSize;  
    156.   }  
    157.    
    158.   public int getTotalRecord() {  
    159.      return totalRecord;  
    160.   }  
    161.    
    162.   public void setTotalRecord(int totalRecord) {  
    163.     this.totalRecord = totalRecord;  
    164.     refreshPage();       
    165.   }  
    166.   
    167.   public int getTotalPage() {  
    168.      return totalPage;  
    169.   }  
    170.    
    171.   public void setTotalPage(int totalPage) {  
    172.      this.totalPage = totalPage;  
    173.   }  
    174.    
    175.   public Map<String, String> getParams() {  
    176.      return params;  
    177.   }  
    178.      
    179.   public void setParams(Map<String, String> params) {  
    180.      this.params = params;  
    181.   }  
    182.     
    183.   public Map<String, List<String>> getParamLists() {  
    184.     return paramLists;  
    185.   }  
    186.   
    187.   public void setParamLists(Map<String, List<String>> paramLists) {  
    188.     this.paramLists = paramLists;  
    189.   }  
    190.   public String getSearchUrl() {  
    191.     return searchUrl;  
    192.   }  
    193.   public void setSearchUrl(String searchUrl) {  
    194.     this.searchUrl = searchUrl;  
    195.   }  
    196.   public String getPageNoDisp() {  
    197.     return pageNoDisp;  
    198.   }  
    199.   public void setPageNoDisp(String pageNoDisp) {  
    200.     this.pageNoDisp = pageNoDisp;  
    201.   }  
    202. }  

    然后是最核心的拦截器了。涉及到了mybatis的核心功能,期间阅读大量mybatis源码几经修改重构,辛苦自不必说。

    核心思想是将拦截到的select语句,改装成select count(*)语句,执行之得到,总数据数。再根据page中的当前页号算出limit值,拼接到select语句后。

    为简化代码使用了Commons JXPath 包,做对象查询。

    Java代码  收藏代码
    1. /** 
    2.  * 分页用拦截器 
    3.  */  
    4. import java.sql.Connection;  
    5. import java.sql.PreparedStatement;  
    6. import java.sql.ResultSet;  
    7. import java.util.Properties;  
    8.   
    9. import org.apache.commons.jxpath.JXPathContext;  
    10. import org.apache.commons.jxpath.JXPathNotFoundException;  
    11. import org.apache.ibatis.executor.Executor;  
    12. import org.apache.ibatis.executor.parameter.DefaultParameterHandler;  
    13. import org.apache.ibatis.mapping.BoundSql;  
    14. import org.apache.ibatis.mapping.MappedStatement;  
    15. import org.apache.ibatis.mapping.MappedStatement.Builder;  
    16. import org.apache.ibatis.mapping.ParameterMapping;  
    17. import org.apache.ibatis.mapping.SqlSource;  
    18. import org.apache.ibatis.plugin.Interceptor;  
    19. import org.apache.ibatis.plugin.Intercepts;  
    20. import org.apache.ibatis.plugin.Invocation;  
    21. import org.apache.ibatis.plugin.Plugin;  
    22. import org.apache.ibatis.plugin.Signature;  
    23. import org.apache.ibatis.session.ResultHandler;  
    24. import org.apache.ibatis.session.RowBounds;  
    25.   
    26. @Intercepts({@Signature(type=Executor.class,method="query",args={ MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class })})  
    27. public class PageInterceptor implements Interceptor{  
    28.   
    29.   public Object intercept(Invocation invocation) throws Throwable {  
    30.       
    31.     //当前环境 MappedStatement,BoundSql,及sql取得  
    32.     MappedStatement mappedStatement=(MappedStatement)invocation.getArgs()[0];      
    33.     Object parameter = invocation.getArgs()[1];   
    34.     BoundSql boundSql = mappedStatement.getBoundSql(parameter);   
    35.     String originalSql = boundSql.getSql().trim();  
    36.     Object parameterObject = boundSql.getParameterObject();  
    37.   
    38.     //Page对象获取,“信使”到达拦截器!  
    39.     Page page = searchPageWithXpath(boundSql.getParameterObject(),".","page","*/page");  
    40.   
    41.     if(page!=null ){  
    42.       //Page对象存在的场合,开始分页处理  
    43.       String countSql = getCountSql(originalSql);  
    44.       Connection connection=mappedStatement.getConfiguration().getEnvironment().getDataSource().getConnection()  ;            
    45.       PreparedStatement countStmt = connection.prepareStatement(countSql);    
    46.       BoundSql countBS = copyFromBoundSql(mappedStatement, boundSql, countSql);  
    47.       DefaultParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, parameterObject, countBS);  
    48.       parameterHandler.setParameters(countStmt);  
    49.       ResultSet rs = countStmt.executeQuery();  
    50.       int totpage=0;  
    51.       if (rs.next()) {    
    52.         totpage = rs.getInt(1);    
    53.       }  
    54.       rs.close();    
    55.       countStmt.close();    
    56.       connection.close();  
    57.         
    58.       //分页计算  
    59.       page.setTotalRecord(totpage);  
    60.         
    61.       //对原始Sql追加limit  
    62.       int offset = (page.getPageNo() - 1) * page.getPageSize();  
    63.       StringBuffer sb = new StringBuffer();  
    64.       sb.append(originalSql).append(" limit ").append(offset).append(",").append(page.getPageSize());  
    65.       BoundSql newBoundSql = copyFromBoundSql(mappedStatement, boundSql, sb.toString());  
    66.       MappedStatement newMs = copyFromMappedStatement(mappedStatement,new BoundSqlSqlSource(newBoundSql));    
    67.       invocation.getArgs()[0]= newMs;    
    68.     }  
    69.     return invocation.proceed();  
    70.       
    71.   }  
    72.     
    73.   /** 
    74.    * 根据给定的xpath查询Page对象 
    75.    */  
    76.   private Page searchPageWithXpath(Object o,String... xpaths) {  
    77.     JXPathContext context = JXPathContext.newContext(o);  
    78.     Object result;  
    79.     for(String xpath : xpaths){  
    80.       try {  
    81.         result = context.selectSingleNode(xpath);  
    82.       } catch (JXPathNotFoundException e) {  
    83.         continue;  
    84.       }  
    85.       if ( result instanceof Page ){  
    86.         return (Page)result;  
    87.       }  
    88.     }  
    89.     return null;  
    90.   }  
    91.   
    92.   /** 
    93.    * 复制MappedStatement对象 
    94.    */  
    95.   private MappedStatement copyFromMappedStatement(MappedStatement ms,SqlSource newSqlSource) {  
    96.     Builder builder = new Builder(ms.getConfiguration(),ms.getId(),newSqlSource,ms.getSqlCommandType());  
    97.       
    98.     builder.resource(ms.getResource());  
    99.     builder.fetchSize(ms.getFetchSize());  
    100.     builder.statementType(ms.getStatementType());  
    101.     builder.keyGenerator(ms.getKeyGenerator());  
    102.     builder.keyProperty(ms.getKeyProperty());  
    103.     builder.timeout(ms.getTimeout());  
    104.     builder.parameterMap(ms.getParameterMap());  
    105.     builder.resultMaps(ms.getResultMaps());  
    106.     builder.resultSetType(ms.getResultSetType());  
    107.     builder.cache(ms.getCache());  
    108.     builder.flushCacheRequired(ms.isFlushCacheRequired());  
    109.     builder.useCache(ms.isUseCache());  
    110.       
    111.     return builder.build();  
    112.   }  
    113.   
    114.   /** 
    115.    * 复制BoundSql对象 
    116.    */  
    117.   private BoundSql copyFromBoundSql(MappedStatement ms, BoundSql boundSql, String sql) {  
    118.     BoundSql newBoundSql = new BoundSql(ms.getConfiguration(),sql, boundSql.getParameterMappings(), boundSql.getParameterObject());  
    119.     for (ParameterMapping mapping : boundSql.getParameterMappings()) {  
    120.         String prop = mapping.getProperty();  
    121.         if (boundSql.hasAdditionalParameter(prop)) {  
    122.             newBoundSql.setAdditionalParameter(prop, boundSql.getAdditionalParameter(prop));  
    123.         }  
    124.     }  
    125.     return newBoundSql;  
    126.   }  
    127.   
    128.   /** 
    129.    * 根据原Sql语句获取对应的查询总记录数的Sql语句 
    130.    */  
    131.   private String getCountSql(String sql) {  
    132.     return "SELECT COUNT(*) FROM (" + sql + ") aliasForPage";  
    133.   }  
    134.   
    135.   public class BoundSqlSqlSource implements SqlSource {    
    136.       BoundSql boundSql;    
    137.       public BoundSqlSqlSource(BoundSql boundSql) {    
    138.         this.boundSql = boundSql;    
    139.       }    
    140.       public BoundSql getBoundSql(Object parameterObject) {    
    141.         return boundSql;    
    142.       }    
    143.     }    
    144.   public Object plugin(Object arg0) {  
    145.      return Plugin.wrap(arg0, this);  
    146.   }  
    147.   public void setProperties(Properties arg0) {  
    148.   }  
    149. }  

    到展示层终于可以轻松些了,使用了文件标签来简化前台开发。

    采用临时表单提交,CSS使用了Bootstrap。

    Html代码  收藏代码
    1. <%@tag pageEncoding="UTF-8"%>  
    2. <%@ attribute name="page" type="cn.com.intasect.ots.common.utils.Page" required="true"%>  
    3. <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>  
    4.   
    5. <%  
    6. int current =  page.getPageNo();  
    7. int begin = 1;  
    8. int end = page.getTotalPage();  
    9.   
    10. request.setAttribute("current", current);  
    11. request.setAttribute("begin", begin);  
    12. request.setAttribute("end", end);  
    13. request.setAttribute("pList", page.getPageNoDisp());  
    14.   
    15. %>  
    16. <script type="text/javascript">  
    17.   var paras = '<%=page.getParaJson()%>';  
    18.   var paraJson = eval('(' + paras + ')');  
    19.   
    20.   //将提交参数转换为JSON  
    21.   var paraLists = '<%=page.getParaListJson()%>';  
    22.   var paraListJson = eval('(' + paraLists + ')');  
    23.   function pageClick( pNo ){  
    24.     paraJson["pageNo"] = pNo;  
    25.     paraJson["pageSize"] = "<%=page.getPageSize()%>";  
    26.       
    27.     var jsPost = function(action, values, valueLists) {  
    28.       var id = Math.random();  
    29.       document.write('<form id="post' + id + '" name="post'+ id +'" action="' + action + '" method="post">');  
    30.       for (var key in values) {  
    31.         document.write('<input type="hidden" name="' + key + '" value="' + values[key] + '" />');  
    32.       }  
    33.       for (var key2 in valueLists) {  
    34.         for (var index in valueLists[key2]) {  
    35.           document.write('<input type="hidden" name="' + key2 + '" value="' + valueLists[key2][index] + '" />');  
    36.         }  
    37.       }  
    38.       document.write('</form>');      
    39.       document.getElementById('post' + id).submit();  
    40.     }  
    41.       
    42.     //发送POST  
    43.     jsPost("<%=page.getSearchUrl()%>", paraJson, paraListJson);  
    44.   }  
    45. </script>  
    46. <div class="page-pull-right">  
    47.   <% if (current!=1 && end!=0){%>  
    48.     <button class="btn btn-default btn-sm" onclick="pageClick(1)">首页</button>  
    49.     <button class="btn btn-default btn-sm" onclick="pageClick(${current-1})">前页</button>  
    50.   <%}else{%>  
    51.     <button class="btn btn-default btn-sm" >首页</button>  
    52.     <button class="btn btn-default btn-sm" >前页</button>  
    53.   <%} %>  
    54.   <c:forTokens items="${pList}" delims="|" var="pNo">  
    55.     <c:choose>  
    56.       <c:when test="${pNo == 0}">  
    57.         <label style="font-size: 10px;  20px; text-align: center;">•••</label>  
    58.       </c:when>  
    59.       <c:when test="${pNo != current}">  
    60.         <button class="btn btn-default btn-sm" onclick="pageClick(${pNo})">${pNo}</button>  
    61.       </c:when>  
    62.       <c:otherwise>  
    63.         <button class="btn btn-primary btn-sm" style="font-weight:bold;">${pNo}</button>  
    64.       </c:otherwise>  
    65.     </c:choose>  
    66.   </c:forTokens>  
    67.   <% if (current<end && end!=0){%>  
    68.     <button class="btn btn-default btn-sm" onclick="pageClick(${current+1})">后页</button>  
    69.     <button class="btn btn-default btn-sm" onclick="pageClick(${end})">末页</button>  
    70.   <%}else{%>  
    71.     <button class="btn btn-default btn-sm">后页</button>  
    72.     <button class="btn btn-default btn-sm">末页</button>  
    73.   <%} %>  
    74. </div>  

    注意“信使”在这里使出了浑身解数,7个主要的get方法全部用上了。

    Java代码  收藏代码
    1. page.getPageNo()        //当前页号  
    2. page.getTotalPage()     //总页数  
    3. page.getPageNoDisp()    //可以显示的页号  
    4. page.getParaJson()      //查询条件  
    5. page.getParaListJson()  //数组查询条件  
    6. page.getPageSize()      //每页行数  
    7. page.getSearchUrl()     //Url地址(作为action名称)  

    到这里三个核心模块完成了。然后是拦截器的注册。

    【拦截器的注册】

    需要在mybatis-config.xml 中加入拦截器的配置

    Java代码  收藏代码
    1. <plugins>  
    2.    <plugin interceptor="cn.com.dingding.common.utils.PageInterceptor">    
    3.    </plugin>  
    4. </plugins>    

    【相关代码修改】

    首先是后台代码的修改,Controller层由于涉及到查询条件,需要修改的内容较多。

    1)入参需增加 pageNo,pageSize 两个参数

    2)根据pageNo,pageSize 及你的相对url构造page对象。(

    3)最重要的是将你的其他入参(查询条件)保存到page中

    4)Service层的方法需要带着page这个对象(最终目的是传递到sql执行的入参,让拦截器识别出该sql需要分页,同时传递页号)

    5)将page对象传回Mode中

    修改前

    Java代码  收藏代码
    1. @RequestMapping(value = "/user/users")  
    2. public String list(  
    3.   @ModelAttribute("name") String name,  
    4.   @ModelAttribute("levelId") String levelId,  
    5.   @ModelAttribute("subjectId") String subjectId,  
    6.   Model model) {  
    7.   model.addAttribute("users",userService.selectByNameLevelSubject(  
    8.           name, levelId, subjectId));  
    9.   return USER_LIST_JSP;  
    10. }  

     修改后

    Java代码  收藏代码
    1. @RequestMapping(value = "/user/users")  
    2. public String list(  
    3.   @RequestParam(required = false, defaultValue = "1"int pageNo,  
    4.   @RequestParam(required = false, defaultValue = "5"int pageSize,  
    5.   @ModelAttribute("name") String name,  
    6.   @ModelAttribute("levelId") String levelId,  
    7.   @ModelAttribute("subjectId") String subjectId,  
    8.   Model model) {  
    9.   // 这里是“信使”诞生之地,一出生就加载了很多重要信息!  
    10.   Page page = Page.newBuilder(pageNo, pageSize, "users");  
    11.   page.getParams().put("name", name);           //这里再保存查询条件  
    12.   page.getParams().put("levelId", levelId);  
    13.   page.getParams().put("subjectId", subjectId);  
    14.       
    15.   model.addAttribute("users",userService.selectByNameLevelSubject(  
    16.           name, levelId, subjectId, page));  
    17.   model.addAttribute("page", page);             //这里将page返回前台  
    18.   return USER_LIST_JSP;  
    19. }  

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

    Service层

    拦截器可以自动识别在Map或Bean中的Page对象。

    如果使用Bean需要在里面增加一个page项目,Map则比较简单,以下是例子。

    Java代码  收藏代码
    1. @Override  
    2. public List<UserDTO> selectByNameLevelSubject(String name, String levelId, String subjectId, Page page) {  
    3.   Map<String, Object> map = Maps.newHashMap();  
    4.   levelId = DEFAULT_SELECTED.equals(levelId)?null: levelId;  
    5.   subjectId = DEFAULT_SELECTED.equals(subjectId)?null: subjectId;  
    6.   if (name != null && name.isEmpty()){  
    7.     name = null;  
    8.   }  
    9.   map.put("name", name);  
    10.   map.put("levelId", levelId);  
    11.   map.put("subjectId", subjectId);  
    12.   map.put("page", page);             //MAP的话加这一句就OK  
    13.   return userMapper.selectByNameLevelSubject(map);  
    14. }  

    前台页面方面,由于使用了标签,在适当的位置加一句就够了。

    Html代码  收藏代码
    1. <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/blog/2001142

    ===================================== 分割线 (1/23)=======================================

    再次改善,使用ThreadLocal类封装Page对象,让Service层等无需传Page对象,减小了侵入性。拦截器也省去了查找Page对象的动作,性能也同时改善。整体代码改动不大。

    ===================================== 分割线 (2/21)=======================================

    今天比较闲,顺便聊下这个分页的最终版,当然从来只有不断变化的需求,没有完美的方案,这里所说的最终版其实是一个优化后的“零侵入”的方案。为避免代码混乱还是只介绍思路。在上一个版本(1/23版)基础上有两点改动:

    一是增加一个配置文件,按Url 配置初始的每页行数。如下面这样(pagesize 指的是每页行数):

    Xml代码  收藏代码
    1. <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代码  收藏代码
    1. @RequestMapping(value = "/user/users")  
    2. @Pagination(size=10)  
    3. public String list(  
    4. ...  

    同样与过滤器配合使用,只是注解本身多少还是有“侵入性”。在初始行数基本不会变更时,这个比较直观的方案也是不错的选择。大家自行决定吧。

  • 相关阅读:
    gc的real时间比user时间长
    java 反射: 当Timestamp类型的属性值为null时,设置默认值
    多线程注意点
    多线程socket编程示例
    一个类有两个方法,其中一个是同步的,另一个是非同步的; 现在又两个线程A和B,请问:当线程A访问此类的同步方法时,线程B是否能访问此类的非同步方法?
    含有Date和Timestamp的Java和Json互相转化
    java bean、List、数组、map和Json的相互转化
    并发抢购
    SQL 性能调优日常积累【转】
    String和包装类IntegerDoubleLongFloatCharacter 都是final类型
  • 原文地址:https://www.cnblogs.com/jpfss/p/9179125.html
Copyright © 2011-2022 走看看