最近在进行SpringSide框架来进行后台开发,又需要使用分页的功能,自己上网找了下资料,最终实现了SpringSide下使用MyBatis进行分页的功能,这里和大家分享一下,给予那些需要的人作为一个参考。
首先,我们需要一个拦截器,在src/main/java下面单独建一个包来存放,比如我的:com.soooft.report.interceptor,然后在里面新建一个拦截器的类,具体代码如下:
package com.soooft.report.interceptor; import java.lang.reflect.Field; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; import java.util.Properties; import org.apache.ibatis.executor.parameter.ParameterHandler; import org.apache.ibatis.executor.statement.RoutingStatementHandler; import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.ParameterMapping; 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.scripting.defaults.DefaultParameterHandler; import com.soooft.report.entity4main.MyPage; /** * * 分页拦截器,用于拦截需要进行分页查询的操作,然后对其进行分页处理。 利用拦截器实现Mybatis分页的原理: * 要利用JDBC对数据库进行操作就必须要有一个对应的Statement对象 * ,Mybatis在执行Sql语句前就会产生一个包含Sql语句的Statement对象,而且对应的Sql语句 * 是在Statement之前产生的,所以我们就可以在它生成Statement之前对用来生成Statement的Sql语句下手 * 。在Mybatis中Statement语句是通过RoutingStatementHandler对象的 * prepare方法生成的。所以利用拦截器实现Mybatis分页的一个思路就是拦截StatementHandler接口的prepare方法 * ,然后在拦截器方法中把Sql语句改成对应的分页查询Sql语句,之后再调用 * StatementHandler对象的prepare方法,即调用invocation.proceed()。 * 对于分页而言,在拦截器里面我们还需要做的一个操作就是统计满足当前条件的记录一共有多少 * ,这是通过获取到了原始的Sql语句后,把它改为对应的统计语句再利用Mybatis封装好的参数和设 * 置参数的功能把Sql语句中的参数进行替换,之后再执行查询记录数的Sql语句进行总记录数的统计。 * */ @Intercepts({ @Signature(method = "prepare", type = StatementHandler.class, args = { Connection.class }) }) public class PageInterceptor implements Interceptor { private String databaseType;// 数据库类型,不同的数据库有不同的分页方法 /** * 拦截后要执行的方法 */ @Override public Object intercept(Invocation invocation) throws Throwable { // 对于StatementHandler其实只有两个实现类,一个是RoutingStatementHandler,另一个是抽象类BaseStatementHandler, // BaseStatementHandler有三个子类,分别是SimpleStatementHandler,PreparedStatementHandler和CallableStatementHandler, // SimpleStatementHandler是用于处理Statement的,PreparedStatementHandler是处理PreparedStatement的,而CallableStatementHandler是 // 处理CallableStatement的。Mybatis在进行Sql语句处理的时候都是建立的RoutingStatementHandler,而在RoutingStatementHandler里面拥有一个 // StatementHandler类型的delegate属性,RoutingStatementHandler会依据Statement的不同建立对应的BaseStatementHandler,即SimpleStatementHandler、 // PreparedStatementHandler或CallableStatementHandler,在RoutingStatementHandler里面所有StatementHandler接口方法的实现都是调用的delegate对应的方法。 // 我们在PageInterceptor类上已经用@Signature标记了该Interceptor只拦截StatementHandler接口的prepare方法,又因为Mybatis只有在建立RoutingStatementHandler的时候 // 是通过Interceptor的plugin方法进行包裹的,所以我们这里拦截到的目标对象肯定是RoutingStatementHandler对象。 RoutingStatementHandler handler = (RoutingStatementHandler) invocation .getTarget(); // 通过反射获取到当前RoutingStatementHandler对象的delegate属性 StatementHandler delegate = (StatementHandler) ReflectUtil .getFieldValue(handler, "delegate"); // 获取到当前StatementHandler的 // boundSql,这里不管是调用handler.getBoundSql()还是直接调用delegate.getBoundSql()结果是一样的,因为之前已经说过了 // RoutingStatementHandler实现的所有StatementHandler接口方法里面都是调用的delegate对应的方法。 BoundSql boundSql = delegate.getBoundSql(); // 拿到当前绑定Sql的参数对象,就是我们在调用对应的Mapper映射语句时所传入的参数对象 Object obj = boundSql.getParameterObject(); // 这里我们简单的通过传入的是Page对象就认定它是需要进行分页操作的。 if (obj instanceof MyPage<?>) { MyPage<?> page = (MyPage<?>) obj; // 通过反射获取delegate父类BaseStatementHandler的mappedStatement属性 MappedStatement mappedStatement = (MappedStatement) ReflectUtil .getFieldValue(delegate, "mappedStatement"); // 拦截到的prepare方法参数是一个Connection对象 Connection connection = (Connection) invocation.getArgs()[0]; // 获取当前要执行的Sql语句,也就是我们直接在Mapper映射语句中写的Sql语句 String sql = boundSql.getSql(); // 给当前的page参数对象设置总记录数 this.setTotalRecord(page, mappedStatement, connection); // 获取分页Sql语句 String pageSql = this.getPageSql(page, sql); // 利用反射设置当前BoundSql对应的sql属性为我们建立好的分页Sql语句 ReflectUtil.setFieldValue(boundSql, "sql", pageSql); } return invocation.proceed(); } /** * 拦截器对应的封装原始对象的方法 */ @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } /** * 设置注册拦截器时设定的属性 */ @Override public void setProperties(Properties properties) { this.databaseType = properties.getProperty("databaseType"); } public String getDatabaseType() { return databaseType; } public void setDatabaseType(String databaseType) { this.databaseType = databaseType; } /** * 根据page对象获取对应的分页查询Sql语句,这里只做了两种数据库类型,Mysql和Oracle * 其它的数据库都 没有进行分页 * * @param page 分页对象 * @param sql 原sql语句 * @return */ private String getPageSql(MyPage<?> page, String sql) { StringBuffer sqlBuffer = new StringBuffer(sql); if ("mysql".equalsIgnoreCase(databaseType)) { return getMysqlPageSql(page, sqlBuffer); } else if ("oracle".equalsIgnoreCase(databaseType)) { return getOraclePageSql(page, sqlBuffer); } return sqlBuffer.toString(); } /** * 获取Mysql数据库的分页查询语句 * @param page 分页对象 * @param sqlBuffer 包含原sql语句的StringBuffer对象 * @return Mysql数据库分页语句 */ private String getMysqlPageSql(MyPage<?> page, StringBuffer sqlBuffer) { //计算第一条记录的位置,Mysql中记录的位置是从0开始的。 int offset = (page.getNumber() - 1) * page.getSize(); sqlBuffer.append(" limit ").append(offset).append(",").append(page.getSize()); return sqlBuffer.toString(); } /** * 获取Oracle数据库的分页查询语句 * @param page 分页对象 * @param sqlBuffer 包含原sql语句的StringBuffer对象 * @return Oracle数据库的分页查询语句 */ private String getOraclePageSql(MyPage<?> page, StringBuffer sqlBuffer) { //计算第一条记录的位置,Oracle分页是通过rownum进行的,而rownum是从1开始的 int offset = (page.getNumber() - 1) * page.getSize() + 1; sqlBuffer.insert(0, "select u.*, rownum r from (").append(") u where rownum < ").append(offset + page.getSize()); sqlBuffer.insert(0, "select * from (").append(") where r >= ").append(offset); //上面的Sql语句拼接之后大概是这个样子: //select * from (select u.*, rownum r from (select * from t_user) u where rownum < 31) where r >= 16 return sqlBuffer.toString(); } /** * 给当前的参数对象page设置总记录数 * * @param page Mapper映射语句对应的参数对象 * @param mappedStatement Mapper映射语句 * @param connection 当前的数据库连接 */ private void setTotalRecord(MyPage<?> page, MappedStatement mappedStatement, Connection connection) { //获取对应的BoundSql,这个BoundSql其实跟我们利用StatementHandler获取到的BoundSql是同一个对象。 //delegate里面的boundSql也是通过mappedStatement.getBoundSql(paramObj)方法获取到的。 BoundSql boundSql = mappedStatement.getBoundSql(page); //获取到我们自己写在Mapper映射语句中对应的Sql语句 String sql = boundSql.getSql(); //通过查询Sql语句获取到对应的计算总记录数的sql语句 String countSql = this.getCountSql(sql); //通过BoundSql获取对应的参数映射 List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); //利用Configuration、查询记录数的Sql语句countSql、参数映射关系parameterMappings和参数对象page建立查询记录数对应的BoundSql对象。 BoundSql countBoundSql = new BoundSql(mappedStatement.getConfiguration(), countSql, parameterMappings, page); //通过mappedStatement、参数对象page和BoundSql对象countBoundSql建立一个用于设定参数的ParameterHandler对象 ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, page, countBoundSql); //通过connection建立一个countSql对应的PreparedStatement对象。 PreparedStatement pstmt = null; ResultSet rs = null; try { pstmt = connection.prepareStatement(countSql); //通过parameterHandler给PreparedStatement对象设置参数 parameterHandler.setParameters(pstmt); //之后就是执行获取总记录数的Sql语句和获取结果了。 rs = pstmt.executeQuery(); if (rs.next()) { int totalRecord = rs.getInt(1); //给当前的参数page对象设置总记录数 page.setTotal(totalRecord); } } catch (SQLException e) { e.printStackTrace(); } finally { try { if (rs != null) rs.close(); if (pstmt != null) pstmt.close(); } catch (SQLException e) { e.printStackTrace(); } } } /** * 根据原Sql语句获取对应的查询总记录数的Sql语句 * @param sql * @return */ private String getCountSql(String sql) { sql = sql.toLowerCase(); int index = sql.lastIndexOf("from"); int groupByIndex = sql.lastIndexOf("group by"); int orderBy = sql.lastIndexOf("order by"); if(groupByIndex!=-1 && groupByIndex>index){ return "select count(*) from (" + (orderBy==-1? sql:sql.substring(0,orderBy))+") " + "_allData"; }else{ return "select count(*) " + (orderBy==-1? sql.substring(index):sql.substring(index,orderBy)); } } /** * 利用反射进行操作的一个工具类 * */ private static class ReflectUtil { /** * 利用反射获取指定对象的指定属性 * @param obj 目标对象 * @param fieldName 目标属性 * @return 目标属性的值 */ public static Object getFieldValue(Object obj, String fieldName) { Object result = null; Field field = ReflectUtil.getField(obj, fieldName); if (field != null) { field.setAccessible(true); try { result = field.get(obj); } catch (IllegalArgumentException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalAccessException e) { // TODO Auto-generated catch block e.printStackTrace(); } } return result; } /** * 利用反射获取指定对象里面的指定属性 * @param obj 目标对象 * @param fieldName 目标属性 * @return 目标字段 */ private static Field getField(Object obj, String fieldName) { Field field = null; for (Class<?> clazz=obj.getClass(); clazz != Object.class; clazz=clazz.getSuperclass()) { try { field = clazz.getDeclaredField(fieldName); break; } catch (NoSuchFieldException e) { //这里不用做处理,子类没有该字段可能对应的父类有,都没有就返回null。 } } return field; } /** * 利用反射设置指定对象的指定属性为指定的值 * @param obj 目标对象 * @param fieldName 目标属性 * @param fieldValue 目标值 */ public static void setFieldValue(Object obj, String fieldName, String fieldValue) { Field field = ReflectUtil.getField(obj, fieldName); if (field != null) { try { field.setAccessible(true); field.set(obj, fieldValue); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } } } }
具体实现,看代码里面的注释。
第一步完成之后,接下来,我们需要建立一个Page类来存放每一页的信息,这里是MyPage,存放到工程的entity实体类包中,代码如下:
package com.soooft.report.entity4main; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; /** * 对分页的基本数据进行一个简单的封装 */ public class MyPage<T> implements Page<T>{ private int number = 1;//页码,默认是第一页 private int size = 15;//每页显示的记录数,默认是15 private int total;//总记录数 private int totalPages;//总页数 private List<T> content;//对应的当前页记录 private Map<String, Object> params = new HashMap<String, Object>();//其他的参数我们把它分装成一个Map对象 public MyPage(){ } public MyPage(Map<String, Object> params){ this.params=params; this.number=1; } public MyPage(Map<String, Object> params, int number){ this.params=params; this.number=number; } public void setTotal(int total) { this.total = total; //在设置总页数的时候计算出对应的总页数,在下面的三目运算中加法拥有更高的优先级,所以最后可以不加括号。 int totalPage = total%size==0 ? total/size : total/size + 1; this.setTotalPages(totalPage); } public Map<String, Object> getParams() { return params; } public void setParams(Map<String, Object> params) { this.params = params; } @Override public List<T> getContent() { return content; } @Override public int getNumber() { return number; } @Override public int getNumberOfElements() { return 0; } @Override public int getSize() { return size; } @Override public Sort getSort() { return null; } @Override public long getTotalElements() { return 0; } @Override public int getTotalPages() { return totalPages; } @Override public boolean hasContent() { return content!=null && content.size()>0; } @Override public boolean hasNextPage() { return number<totalPages; } @Override public boolean hasPreviousPage() { return number>1; } @Override public boolean isFirstPage() { return number==1; } @Override public boolean isLastPage() { return number==totalPages; } @Override public Iterator iterator() { return null; } @Override public Pageable nextPageable() { return null; } @Override public Pageable previousPageable() { return null; } public int getTotal() { return total; } public void setNumber(int number) { this.number = number; } public void setSize(int size) { this.size = size; } public void setTotalPages(int totalPages) { this.totalPages = totalPages; } public void setContent(List<T> content) { this.content = content; } }
这里实现 import org.springframework.data.domain.Page;有的可能提示没有这个Page,这个就需要加载这个jar包,具体到项目的pom.xml文件中添加:
在主要依赖库的版本定义中加入<spring-data-jpa.version>1.4.4.RELEASE</spring-data-jpa.version>,然后在下面的依赖项定义添加:
<!-- spring data access --> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-jpa</artifactId> <version>${spring-data-jpa.version}</version> </dependency>
然后刷新下项目即可,这个时候会自动下载相对应的包,Page就可以导入进来。
最后一步配置是到MyBatis配置的地址进行设置:
找到项目中配置<!-- 显式指定Mapper文件位置 -->的地方,加入如下代码:
<property name="plugins"> <ref bean="pageInterceptor"/> </property>
然后再指定一个bean的配置
<bean id="pageInterceptor" class="com.soooft.report.interceptor.PageInterceptor"> <property name="databaseType" value="mysql"/> </bean>
这里class<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);">指定我们刚刚创建拦截器的位置,具体配置位置,一般在项目src/main/resources下面的applicationContext.xml文件中。</span>
<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);">到这里基本配置已经完成,下面我们就写代码实现分页。</span>
<span style="font-family:Arial, Helvetica, sans-serif;"><span style="background-color: rgb(255, 255, 255);">这里我就拿一个查找所有账户的例子进行介绍,来讲述分页的用法,</span></span>
<span style="font-family:Arial, Helvetica, sans-serif;"><span style="background-color: rgb(255, 255, 255);">在AccountSuitControl中,我们找到查找所有账户的方法,以前我们可能是直接调用service.findAll查出所有的账户,现在要进行更改:</span></span>
<span style="font-family:Arial, Helvetica, sans-serif;"><span style="background-color: rgb(255, 255, 255);"></span></span><pre name="code" class="java">private void getAllAccountSuit(Model model, int pageNumber){ MyPage<Map<String, Object>> page = new MyPage<Map<String, Object>>(); page.setNumber(pageNumber); List<Map<String, Object>> accountSuites = accountService.getAll(page); model.addAttribute("accountSuites", accountSuites); model.addAttribute("action", "create"); model.addAttribute("pageData", page); }
这里我抽出来为一个方法,传入Model,和页数,然后创建一个MyPage对象,并制定返回的数据类型,我这里查出的数据是以Map进行保存,如果你用实体类来接收,那泛型就指定实体类,然后page设置当前页数,最后调用getAll方法查出数据即可,然后通过mode把查出来的数据设置进去,还有就是page也要设置进去。
下面我们再看看Service是怎么写的,
public List<Map<String, Object>> getAll(MyPage<Map<String, Object>> page){ return accountSuitDao.findAll(page); }
很简单,它是直接调用Dao接口,Dao接口如下:
List<Map<String, Object>> findAll(MyPage<Map<String, Object>> page);
最后再看看Mapper,如下:
<select id="findAll" parameterType="MyPage" resultType="hashmap"> <span style="white-space:pre"> </span>select id, code, name, db_trail as dbTrail, remark from account_suites ORDER BY create_time DESC </select>
对比之前,实际上我们只是传入了一个MyPage对象而已,其他的都是一样的,MyPage会自动为我们的数据进行分页处理。
到这一步,基本上实现了分页,最后就是在jsp页面上展示我们分页的效果:
只需要在原有的Jsp页面下面加上如下代码即可:
<tags:pagination paginationSize="15" page="${pageData}"></tags:pagination>
这个时候它会提示找不到tags,还需要在上面加入:<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %>
然后到WEB-INF下面的tags目录下,增加一个pagination.tag文件,由它完成分页下面页面跳转的展示效果:
<%@tag pageEncoding="UTF-8"%> <%@ attribute name="page" type="org.springframework.data.domain.Page" required="true"%> <%@ attribute name="paginationSize" type="java.lang.Integer" required="true"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <% int current = page.getNumber(); int begin = Math.max(1, current - paginationSize/2); int end = Math.min(begin + (paginationSize - 1), page.getTotalPages()); request.setAttribute("current", current); request.setAttribute("begin", begin); request.setAttribute("end", end); %> <div class="pagination" class="vertical-align: middle;"> <ul> <% if (page.hasPreviousPage()){%> <li><a href="?page=1&sortType=${sortType}&${searchParams}"><<</a></li> <li><a href="?page=${current-1}&sortType=${sortType}&${searchParams}"><</a></li> <%}else{%> <li class="disabled"><a href="#"><<</a></li> <li class="disabled"><a href="#"><</a></li> <%} %> <c:forEach var="i" begin="${begin}" end="${end}"> <c:choose> <c:when test="${i == current}"> <li class="active"><a href="?page=${i}&sortType=${sortType}&${searchParams}">${i}</a></li> </c:when> <c:otherwise> <li><a href="?page=${i}&sortType=${sortType}&${searchParams}">${i}</a></li> </c:otherwise> </c:choose> </c:forEach> <% if (page.hasNextPage()){%> <li><a href="?page=${current+1}&sortType=${sortType}&${searchParams}">></a></li> <li><a href="?page=${page.totalPages}&sortType=${sortType}&${searchParams}">>></a></li> <%}else{%> <li class="disabled"><a href="#">></a></li> <li class="disabled"><a href="#">>></a></li> <%} %> </ul> ${pageData.total}条数据 共${pageData.totalPages}页 </div>
到此,分页效果基本上完成了,最新的效果图如下: