(一)功能
能够实现不加“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>