(一)功能
能够实现不加“limit *,*”也能实现分页查询。
(二)具体实现
1.建立一个分页对象
package com.imooc.entity;
/**
* @author 潘畅
* @date 2018/5/10 20:12
*/
public class Page {
/**
* 总条数(传过来的,数据库查询)
*/
private int totalNumber;
/**
* 当前页(传过来的)
*/
private int currentPage;
/**
* 每页显示数量(已知)
*/
private int pageNumber = 3;
/**
* 总页数(需计算)
*/
private int totalPage;
/**
* 每页的开始条目对应的数据库中查询的偏移量(需计算)
*/
private int dbIndex;
/**
* 每页需要从数据库中查询多少数据(其实就等于pageNumber)
*/
private int dbNumber;
public Page() {
}
/**
* 计算及规范各项数据
*/
private void count(){
/**
* 首先计算总页数
*/
int totalPageTemp = totalNumber/pageNumber;
int plus = (totalNumber % pageNumber) == 0 ? 0:1;
totalPageTemp += plus;
if (totalPageTemp <= 0){
totalPageTemp = 1;
}
totalPage = totalPageTemp;
/**
* 规范当前页
*/
if (currentPage < 1){
currentPage = 1;
}
if (currentPage > totalPage){
currentPage = totalPage;
}
/**
* 计算每页的开始条目对应的数据库中查询的偏移量
*/
dbIndex = (currentPage - 1) * pageNumber;
/**
* 每页需要从数据库中查询多少数据
*/
dbNumber = pageNumber;
}
public int getTotalPage() {
return totalPage;
}
public void setTotalNumber(int totalNumber) {
this.totalNumber = totalNumber;
/**
* currentPage在计算totalNumber之前已经知道了,所以计算好totalNumber之后,向Page中设置的时候,
* 就可以触发“count()”方法了!
*/
count();
}
public int getCurrentPage() {
return currentPage;
}
public void setCurrentPage(int currentPage) {
this.currentPage = currentPage;
}
public int getPageNumber() {
return pageNumber;
}
public void setPageNumber(int pageNumber) {
this.pageNumber = pageNumber;
}
public void setTotalPage(int totalPage) {
this.totalPage = totalPage;
}
public int getTotalNumber() {
return totalNumber;
}
public int getDbIndex() {
return dbIndex;
}
public void setDbIndex(int dbIndex) {
this.dbIndex = dbIndex;
}
public int getDbNumber() {
return dbNumber;
}
public void setDbNumber(int dbNumber) {
this.dbNumber = dbNumber;
}
}
2.创建分页拦截器
package com.imooc.interceptor;
import com.imooc.entity.Page;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Map;
import java.util.Properties;
/**
* 实现的接口“Interceptor”是Mybatis带的,不是JDK原生的
* 这个类的原理:
* 这个拦截器主要实现拦截“特定的方法所对应的sql语句”,通过对sql语句进行拼接
*“limit offset,number”,来实现分页查询。
* 而Mybatis中获取sql语句的方法,就是“StatementHandler”接口下的“prepare”方法,
* 参数就是“Connection、Integer”,即 Statement prepare(Connection var1, Integer var2)。
* 以上就对应“PageInterceptor”(分页拦截器)上的注解。
* 备注:@Intercepts()中是一个数组,记得外围要加“{}”
* @author 潘畅
* @date 2018/5/11 9:50
*/
@Intercepts({@Signature(type = StatementHandler.class ,method = "prepare",args = {Connection.class, Integer.class})})
public class PageInterceptor implements Interceptor {
/**
* 实现“Interceptor”接口需要实现的“intercept()、plugin()、setProperties()”
* 三个方法的执行顺序?
* 一、先调用setProperties()方法,获取拦截器注册时的属性
* 二、再调用plugin()方法,过滤拦截对象
* 三、最后调用intercept()方法,执行拦截的逻辑
*/
/**
* 这个方法,只有在“plugin(Object target)”方法中,返回target的代理对象
* (关于如何触发target的代理,见方法说明),才会被执行。
* @param invocation 通过该参数,可以获取被拦截的对象(也就是实现了“StatementHandler”
* 接口的对象),此时的对象已经过代理处理
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
/**
* Invocation可以获取被拦截的对象(PS:被拦截的对象肯定实现了“StatementHandler”
* 接口,所以可以进行强转)
*/
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
/**
* 问题:
* StatementHandler接口的prepare()方法获取sql语句,但它是抽象方法,具体实现是
* 由“BaseStatementHandler”类,该类有一个成员变量“MappedStatement”,它存放配置
* 文件中的每条sql语句。通过“MappedStatement”可以获取到配置文件中sql语句的详细信息,
* 但是“BaseStatementHandler”类中的“MappedStatement”是“protected”类型,没有get()
* 方法,所以无法直接获取到“MappedStatement”。
* 解决办法:
* Mybatis对“StatementHandler”进行了封装,通过“MetaObject”对象实现!此时“metaObject”
* 就等价于“statementHandler”,通过“metaObject”就可以获取成员变量“MappedStatement”
*/
MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY,
SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory());
/**
* 关于“delegate.mappedStatement”详解:
* “PageInterceptor”拦截到“StatementHandler”接口以后,首先访问的是
* “RoutingStatementHandler”类,该类下有个成员变量“StatementHandler delegate”,
* 然后再通过这个成员变量访问到“BaseStatementHandler”类,才可以继续 访问到成员
* 变量“mappedStatement”,所以键值是“delegate.mappedStatement”(键的写法遵循OGNL表达式)
*/
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
//获取配置文件中sql语句的id
String id = mappedStatement.getId();
//拦截以“ByPage”结尾的id,只要符合该条件的“StatementHandler”才能继续执行(正则表达式)
if (id.matches(".+ByPage$")){
/**
* 在“StatementHandler”接口的实现类“BaseStatementHandler”中的
* “prepare()”方法中可以看到,是通过“this.boundSql.getSql()”获取sql
* 语句的,所以我们要得到“boundSql”对象,“BoundSql”对象不同于
* “MappedStatement”对象,它是有"get()"方法的,所以直接用“get()”方法获取即可。
*/
BoundSql boundSql = statementHandler.getBoundSql();
//获取原始sql语句(此时的sql语句中若有“id=#{id}”这样的参数,则已经被Mybatis转换成了“?”)
String sql = boundSql.getSql();
/**
* 拼接分页sql语句
* 问题:
* 没有参数,“limit *,*”没法写?所以接下来,需要拿到传递过来的参数
*/
/**
* 通过boundSql获取参数,然后进行强转(PS:一般来说,分页查询最少需要用
* 到一个Page对象和者其他对象,所以一般遇到分页查询,我们设置个规定,参数必
* 须是map类型,取出Page对象的键值就是“page”)
*/
Map , params = (Map , )boundSql.getParameterObject();
Page page = (Page) params.get("page");
/**
* 问题:
* 此时Service层传递过来的Page对象只有“currentPage”(当前页码),还缺少
* 一个“totalNumber”(总条数),否则无法计算出“totalPage”(总页数),返回
* 的“Page”对象是残缺的?(前端需要用到这个Page对象)
* 解决办法:
* 查询“totalNumber”(总条数),完善Page对象!
*/
/**
* 查询总条数的sql语句
* 说明:
* 我们从Mybatis中拿到的sql语句,就是目标sql语句(查询出我们想要的所有条目),
* 所以,我们可以将拿到的sql语句作为一个子查询,计算总条目即可。
*/
String totalNumberSql = "select count(*) from (" + sql + ")a";
/**
* 问题1:
* 这里为什么用原生的JDK来执行“查询总条数”sql语句?
* 原因:
* 由于我们需要在sql放回Mybatis框架之前执行!
* 因为在将改造后的“分页sql”放回Mybatis框架之前,我们必须保证Page对象是
* 完整的,因为这个Page对象,在Mybatis框架处理之后,需要返回去的(我们前端需要
* 这个Page对象)。所以这里我们只有先获取总条数,完善Page对象!
* 问题2:
* 使用JDK原生,则需要“Connection”对象,如何获取?
* 解决:
* 我们的“PageInterceptor”(分页过滤器)拦截的“StatementHandler.prepare(Connection var1, Integer var2)”
* (PS:见注解和前面说明),里面的第一个参数就是“Connection”对象,可以通过
* “Invocation”对象获取,明显“Connection”对象是第一个参数!
*/
Connection connection = (Connection) invocation.getArgs()[0];
/*--------以下就是执行sql语句,获取总条数--------*/
/**
* 问题:
* 我们是将从Mybatis中拿到的sql语句作为子查询语句的,这样有一个问题,原
* sql语句可能需要Mybatis框架将这样“ #{id}”(“id = #{id}”)转换成“?”号,
* 并且记录“?”号和参数之间的对应关系(比如第一个“?”号对应哪一个参数),
* 我们怎么确定这个对应关系?
* 分析:
* Mybatis再将sql语句中的“#{id}”替换成“?”之后,肯定记录了“?”和参数的
* 对应关系。而记录这些信息的类就是"ParameterHandler"接口。"ParameterHandler"接
* 口中有一个方法“setParameters(PreparedStatement var1)”,就是根据Mybatis框架掌
* 握的“?”和参数的对应关系,来将参数设置到“PreparedStatement”对象中。当然,这
* 样做的前提是我们不能在新的语句中加入新的参数了,否则就破坏了"ParameterHandler"接
* 口掌握的“?”和参数的对应关系。
* 解决:
* 获取“ParameterHandler”对象,将其掌握的“?”和参数的对应关系设置到PreparedStatement”对象中。
* 参数说明:
* 和前面类似,“PageInterceptor”拦截到“StatementHandler”接口以后,首先访问
* 的是“RoutingStatementHandler”类,该类下有个成员变量“StatementHandler delegate”,
* 然后再通过这个成员变量访问到“BaseStatementHandler”类,该类下有个成员变量
* “ParameterHandler parameterHandler”,所以键值是“delegate.parameterHandler”(遵循OGNL表达式)
*/
ParameterHandler parameterHandler = (ParameterHandler) metaObject.getValue("delegate.parameterHandler");
PreparedStatement statement = connection.prepareStatement(totalNumberSql);
/**
* 调用ParameterHandler对象的“setParameters(PreparedStatement var1)”方法,
* 将其掌握的“?”和参数的对应关系设置到PreparedStatement”对象中
*/
parameterHandler.setParameters(statement);
ResultSet resultSet = statement.executeQuery();
//因为就1条数据,就不使用“while”了
if (resultSet.next()){
//列下标是从“1”开始
page.setTotalNumber(resultSet.getInt(1));
}
/*--------以上就是执行sql语句,获取总条数--------*/
String pageSql = sql + " LIMIT " + page.getDbIndex() + "," + page.getDbNumber();
/**
* 问题:
* 我们从Mybatis中取出sql语句并进行改造,改造过后,我们应该再将sql语句放回去。
* 但是,我们取sql语句有get()方法(boundSql.getSql()),却没有“set()”方法?
* 解决:
* 依然是通过上面的MetaObject对象来将sql语句放回去
* 键值“delegate.boundSql.sql”说明:
* “PageInterceptor”拦截到“StatementHandler”接口以后,首先访问的是
*“RoutingStatementHandler”类,该类下有个成员变量“StatementHandler delegate”,
* 然后再通过这个成员变量访问到“BaseStatementHandler”类,该类下有个成员
* 变量“BoundSql boundSql”,然后继续访问“BoundSql”类,该类下有个“String sql”
* 对象,所以键值是“delegate.boundSql.sql”(遵循OGNL表达式)
*/
//将改造后的sql语句再放回Mybatis中
metaObject.setValue("delegate.boundSql.sql", pageSql);
}
/**
* invocation.proceed():通过反射继续执行获取SQL语句之后的代码(即Mybatis
* 框架代为进行的执行sql语句,返回结果。。)
*/
return invocation.proceed();
//
/**
* 最后一步,拦截器一定一定要在mybatis总配置文件中注册该“拦截器”
* <plugins>
* <plugin interceptor="com.imooc.interceptor.PageInterceptor"/>
* </plugins>
*/
}
/**
* 方法的功能:判断被拦截的对象,是否需要进行某些处理
* @param target 被拦截的对象
* @return
*/
@Override
public Object plugin(Object target) {
/**
* 下面的意思是,若target满足了"PageInterceptor"(分页过滤器)的过滤
* 要求,则对target进行代理,返回被代理对象,否则直接返回对象。(PS:对于
* 分页过滤器而言,它的拦截条件就是,只要涉及到与数据库交互(获取SQL语句),
* 都会被拦截)
*/
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
/**
* 若注册拦截器时这样写:
* <plugins>
* <plugin interceptor="com.imooc.interceptor.PageInterceptor">
* <property name="test" value="abc"/>
* </plugin>
* </plugins>
* 那么就以用“properties.getProperty("test")”获取到值“abc”
*/
}
}
3.在Mybatis总配置文件中注册“分页拦截器”(千万不能忘记)
<plugins>
<plugin interceptor="com.imooc.interceptor.PageInterceptor"/>
</plugins>