mybatis3入门学习--自定义拦截器实现分页功能

在web开发中分页是使用非常频繁的一个功能,那么在mybatis中是如何分页的呢?

首先分页的原理需要明白:

  1. 查询出满足条件的记录总数,用于计算总页码,以及页面显示记录总数。

  2. 根据页码和每页显示数,计算一个分页的偏移量。

上面的第一步在特殊情况下也可以省略,比如在不显示总页码、总记录数、查询页码允许超过最大页的时候就可以直接忽略第一步。

当然在大部分都会要求显示总页码,这个时候第一步查询总记录数就不能省略。

通过上网查询文档和已有的网友发出的分页代码,可以知道在mybatis中可利用拦截器功能来实现自动分页。

大概逻辑就是: 拦截要执行的查询方法,然后拼接查询总记录数的SQL并执行,最后把分页的sql代码添加到开始要执行查询的SQL后面。

添加UserDao和UserMapper代码和配置

public List<User> getUserList(Page page);
<select id="getUserList" resultType="user">
    select * from user
</select>

也就是给方法增加一个Page对象, 注意如果方法有多个参数则需要使用@Param注解来标识每个属性的名称。

Page 类的代码

package me.zhaojunling.demo.mybatis.page;

import java.util.List;

public class Page {

	private int pageSize = 15;
	private int pageNum = 1;
	private int totalPage = 1;
	private int totalSize;
	
	// 数据列表,实际项目中可根据需要确认是否添加这个属性
	private List<?> itemList;
	
	public int getPageSize() {
		return pageSize;
	}
	public void setPageSize(int pageSize) {
		this.pageSize = pageSize;
	}
	public int getPageNum() {
		return pageNum;
	}
	public void setPageNum(int pageNum) {
		this.pageNum = pageNum;
	}
	public int getTotalPage() {
		return totalPage;
	}
	public void setTotalPage(int totalPage) {
		this.totalPage = totalPage;
	}
	public int getTotalSize() {
		return totalSize;
	}
	public void setTotalSize(int totalSize) {
		if (this.totalSize % this.pageSize != 0) {
			this.totalPage = (int)(this.totalSize / this.pageSize + 1);
		}else {
			this.totalPage = (int)(this.totalSize / this.pageSize);
		}
		if(totalPage == 0) totalPage = 1;
		this.totalSize = totalSize;
	}
	public List<?> getItemList() {
		return itemList;
	}
	public void setItemList(List<?> itemList) {
		this.itemList = itemList;
	}
}

下面是拦截器的代码

package me.zhaojunling.demo.mybatis.page;

import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
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.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;

/**
 * 拦截查询SQL,根据分页对象自动设置分页参数
 */
@Intercepts(@Signature(method = "prepare", type = StatementHandler.class, args = { Connection.class }))
public class PageInterceptor implements Interceptor {

	public Object intercept(Invocation invocation) throws Throwable {
		RoutingStatementHandler handler = (RoutingStatementHandler) invocation.getTarget();
		BoundSql boundSql = handler.getBoundSql();
		Object obj = boundSql.getParameterObject();

		if (obj == null)
			return invocation.proceed();

		Page page = null;
		if (obj instanceof Page) {
			page = (Page) obj;
		} else if (obj instanceof Map) {
			// 如果Dao中有多个参数,则分页的注解参数名必须是page
			try {
				page = (Page) ((Map) obj).get("page");
			} catch (Exception e) {
				//e.printStackTrace();
			}
		}

		// 不存在分页对象,则忽略下面的分页逻辑
		if (page == null)
			return invocation.proceed();

		// 查询总记录数
		setTotalSize(handler.getParameterHandler(), boundSql, (Connection) invocation.getArgs()[0], page);
		
		// 获取最终执行的sql字段,方面下面重新设置值
		Field field = BoundSql.class.getDeclaredField("sql");
		field.setAccessible(true);
		
		// 查询的页面不能小于0
		if(page.getPageNum() <= 0) page.setPageNum(1);
		// 查询的页面不能超过最大页码
		if(page.getPageNum() > page.getTotalPage()) page.setPageNum(page.getTotalPage());
		// 设置分页的SQL代码
		field.set(boundSql, boundSql.getSql()+" limit "+(page.getPageNum()-1)*page.getPageSize()+", "+page.getPageSize());
		
		return invocation.proceed();
	}

	public Object plugin(Object target) {
		return Plugin.wrap(target, this);
	}

	public void setProperties(Properties properties) {

	}

	/**
	 * 查询总记录数
	 * @param parameterHandler
	 * @param boundSql
	 * @param conn
	 * @param page
	 */
	private void setTotalSize(ParameterHandler parameterHandler, BoundSql boundSql, Connection conn, Page page) {
		String countSql = "select count(1) from (" + boundSql.getSql() + ") t";

		PreparedStatement pstmt = null;
		ResultSet rs = null;
		try {
			pstmt = conn.prepareStatement(countSql);
			parameterHandler.setParameters(pstmt);
			rs = pstmt.executeQuery();
			if (rs.next()) {
				// 设置总记录数
				page.setTotalSize(rs.getInt(1));
			}
		} catch (SQLException e) {
			e.printStackTrace();
		} finally {
			try {
				if (rs != null)
					rs.close();
				if (pstmt != null)
					pstmt.close();
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}
	}
}

将拦截器配置到项目中

     <!-- Spring Mybatis -->
     <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
      <property name="dataSource" ref="dataSource" />
      <!-- 
      <property name="configLocation" value="WEB-INF/mybatis-config"/>
       -->
      <property name="typeAliasesPackage" value="me.zhaojunling.demo.mybatis.model" />
      <property name="mapperLocations" value="classpath*:mapper/*.xml" />
      <property name="plugins">
        <array>
             <!--此处是新增的分页拦截器-->
            <bean class="me.zhaojunling.demo.mybatis.page.PageInterceptor"/>
        </array>
      </property>
    </bean>

执行时的log日志如下

DEBUG JakartaCommonsLoggingImpl.java:49 - ==>  Preparing: select count(1) from (select * from user) t 

DEBUG JakartaCommonsLoggingImpl.java:49 - ==> Parameters: 

DEBUG JakartaCommonsLoggingImpl.java:49 - ==>  Preparing: select * from user limit 0, 15 

DEBUG JakartaCommonsLoggingImpl.java:49 - ==> Parameters: 

DEBUG JakartaCommonsLoggingImpl.java:49 - <==      Total: 4

到此就完成了mybatis的分页功能,以后使用时主要在Dao层的方法参数中添加一个Page对象,则执行结果就会自动进行分页。


期待mybatis以后能提供分页的原生支持,使用拦截器的方式虽然也能实现功能,但毕竟是使用代理和反射完成,如果代码不优化对性能会造成一些影响。

提交评论