[一步是咫尺,一步即天涯]
前文,我们演示了物理分页的Sql实现方式,这种方式使得我们每次在编写查询服务时,不断的重复造轮子。这样的代码实现方式就显得十分的笨拙了。本文是Mybatis分页查询的最后一片内容,我们将介绍基于拦截器的,精巧的实现方式。在阅读这篇文章之前,强烈建议各位看官能够先阅读上文。这样就能对下文我们提及的各种对象及他们之间的关系有一个清晰的关系。好了,废话不多讲,开始我们的正文部分吧。
准备工作:
a.操作系统 :win7 x64
b.基本软件:MySQL,Mybatis,SQLyog
-------------------------------------------------------------------------------------------------------------------------------------
【本文只作为原理分析及简单分页功能实现。实际生产环节,建议各位读者选择已经广泛应用的第三方jar包或平台性支持的实现】
-------------------------------------------------------------------------------------------------------------------------------------
1.创建本文我们将使用的工程Mybatis12,工程结构图如下:【重点文件我们给出,其他配置文件请读者参考前文工程】
2.管理分页的对象PagePOJO的具体内容如下:【这里我们给出基本分页的示例,更多需求请读者自行完成】
package com.csdn.ingo.entity; /** *@author 作者 E-mail:ingo *@version 创建时间:2016年4月27日下午6:27:05 *类说明 */ public class PagePOJO { private int totalNumber;//当前表中总条目数量 private int currentPage;//当前页的位置 private int totalPage;//总页数 private int pageSize;//页面大小 private int startIndex;//检索的起始位置 private int totalSelect;//检索的总数目 //...省略其他set,get方法 public void setTotalNumber(int totalNumber) { this.totalNumber = totalNumber; this.count(); } //...省略其他set,get方法 public PagePOJO(int totalNumber, int currentPage, int totalPage, int pageSize, int startIndex, int totalSelect) { super(); this.totalNumber = totalNumber; this.currentPage = currentPage; this.totalPage = totalPage; this.pageSize = pageSize; this.startIndex = startIndex; this.totalSelect = totalSelect; } public void count(){ int totalPageTemp = this.totalNumber/this.pageSize; int plus = (this.totalNumber%this.pageSize)==0?0:1; totalPageTemp = totalPageTemp+plus; if(totalPageTemp<=0){ totalPageTemp=1; } this.totalPage = totalPageTemp;//总页数 if(this.totalPage<this.currentPage){ this.currentPage = this.totalPage; } if(this.currentPage<1){ this.currentPage=1; } this.startIndex = (this.currentPage-1)*this.pageSize;//起始位置等于之前所有页面输乘以页面大小 this.totalSelect = this.pageSize;//检索数量等于页面大小 } }
3.新增单元测试方法,如下:【测试数据读者可以自行更换】
@Test public void testSelectPage() { try { // 创建分页对象 //(int totalNumber, int currentPage, int totalPage, int pageSize, int startIndex, int totalSelect) PagePOJO page = new PagePOJO(5, 1, 1, 3, 1, 4); Map<String,Object> params = new HashMap<String,Object>(); params.put("page", page); UserInfoDao userInfo = sqlSession.getMapper(UserInfoDao.class); List<UserInfo> re = userInfo.selectByPage(params); System.out.println(re); } catch (Exception e) { e.printStackTrace(); } }
-------------------------------------------------------------------------------------------------------------------------------------
上面与分页有关的,比较简单的内容我们先提供给大家。下面我们来详细的解释,拦截器实现分页的原理,各位看官睁大眼睛啊!
-------------------------------------------------------------------------------------------------------------------------------------
分页开始之前的问题:
- Mybatis如何找到我们新增的拦截服务。
- 自定义的拦截服务应该在什么时间拦截查询动作。即什么时间截断Mybatis执行流。
- 自定义的拦截服务应该拦截什么样的对象。不能拦截什么样的对象。
- 自定义的拦截服务拦截的对象应该具有什么动作才能被拦截。
- 自定义的拦截服务如何获取上下文中传入的参数信息。
- 如何把简单查询,神不知鬼不觉的,无侵入性的替换为分页查询语句。
- 最后,拦截器应该如何交还被截断的Mybatis执行流。
带着这些问题,我们来看看我们自定义的拦截服务是如何实现的。
-------------------------------------------------------------------------------------------------------------------------------------------------------
1.首先,我们看看查询语句的时序图,如下:
2.回顾一下,Mybatis允许我们能够进行切入的点,如下:【上文的运行细节图中也有描述,具体内容请参考前文。】
【老外其实从命名上已经给我们了充足的信息,我们结合时序图已经能够猜到其大概的含义及执行时间,鉴于篇幅的关系,具体的功能及作用,请参考前文详细内容。】
【结论】从上面的1.2两步,我们知道分页拦截的合理时机是在StatementHandler中。
3.现在,来看看这个StatementHandler的具体内容,如下:
【成员方法】
【类间关系】
这里请读者回顾上文中讲述的查询执行流,即这里将会执行prepare(Connection)方法,如下:
@Override public Statement prepare(Connection connection) throws SQLException { ErrorContext.instance().sql(boundSql.getSql()); Statement statement = null; try { statement = instantiateStatement(connection); setStatementTimeout(statement); setFetchSize(statement); return statement; } catch (SQLException e) { closeStatement(statement); throw e; } catch (Exception e) { closeStatement(statement); throw new ExecutorException("Error preparing statement. Cause: " + e, e); } }
执行到instantiateStatement(connection);如下:【PreparedStatementHandler中】
@Override protected Statement instantiateStatement(Connection connection) throws SQLException { String sql = boundSql.getSql(); if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) { String[] keyColumnNames = mappedStatement.getKeyColumns(); if (keyColumnNames == null) { return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS); } else { return connection.prepareStatement(sql, keyColumnNames); } } else if (mappedStatement.getResultSetType() != null) { return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY); } else { return connection.prepareStatement(sql); } }
【重点!!!】【这里我们就发现真正执行查询语句的地方connection.prepareStatement(...)】
由此,就解释了为什么在StatementHandler中进行拦截。
4.于是,我们可以得到拦截器的对应注解内容如下:【继承自Mybatis的Interceptor,必须实现3个方法,如下:】
@Intercepts({@Signature(type=StatementHandler.class,method="prepare",args={Connection.class})})
5.现在,自定义的拦截器的基本结构如下:
package com.csdn.ingo.interceptor; /** *@author 作者 E-mail:ingo *@version 创建时间:2016年4月27日下午6:55:09 */ @Intercepts({@Signature(type=StatementHandler.class,method="prepare",args={Connection.class})}) public class PageInterceptor implements Interceptor{ /* (non-Javadoc) * 拦截器要执行的方法 */ public Object intercept(Invocation invocation) throws Throwable { //... } /* (non-Javadoc) * 拦截器需要拦截的对象 */ public Object plugin(Object target) { //... } /* (non-Javadoc) * 设置初始化的属性值 */ public void setProperties(Properties properties) { //... } }
6.我们先看看拦截器上面基本结构中需要拦截的对象的具体方法实现:
/* (non-Javadoc) * 拦截器需要拦截的对象,target。this,当前类的实例 */ public Object plugin(Object target) { return Plugin.wrap(target, this); }
接下来,深入的看看wrap方法,如下:
public static Object wrap(Object target, Interceptor interceptor) { Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); Class<?> type = target.getClass(); Class<?>[] interfaces = getAllInterfaces(type, signatureMap); if (interfaces.length > 0) { return Proxy.newProxyInstance( type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); } return target; }
接着,再看看getSignatureMap(interceptor);的详细内容,如下:
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) { Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class); // ... return signatureMap; }
由此看到:annotation注解等其他相关内容都放在Map中返回。最后,判断出是否生成代理对象。
-------------------------------------------------------------------------------------------------------------------------------------
7.现在,我们已经成功的拦截到了目标对象,然后,就开始要改变查询过程了。在这里,出现了两个基本的问题
- 如何获取原始的查询语句,即mapper文件中的sql语句。
- 如何获取分页信息。
其实,在方法intercept(Invocation invocation)的参数中,已经包含了StatementHandler的信息,我们需要做的就是取出其中的信息,操作步骤如下:
a.先取出StatementHandler,如下:
StatementHandler statementHandler = (StatementHandler)invocation.getTarget();
·关于StatementHandler其有两个实现,【见上文】,默认情况下,程序会执行BaseStatementHandler下的内容。如下:
其中的mappedStatement中就包含着我们需要找的sql信息。如下:【如果读者已经看过前面的配置详解部分的内容,已经不会感到陌生】
现在,我们已经找到了我们想要的内容的具体位置,那我们应该怎样取出来呢?此时应该有看官大喊一声:这还不简单!可是我们来看看这部分的源码内容,如下:
protected final MappedStatement mappedStatement;
【注意】
其在BaseStatementHandler中是protected的,我们在没有继承等关系的条件下,是无法直接取出来的。
因此,我们就需要引入一个Mybatis已经实现了的对象:MetaObject。关于这个对象,可以先暂时的理解为帮助我们获取或设置该对象的原本不可访问的属性。
在3.3.1版本中,其内部细节如下:
这里,我们先只用到了public static MetaObject forObject(.....) ,其他内容有兴趣的读者可以自行学习。
于是,我们操作的对象就由StatementHandler的实例,变为MetaObject的实例,如下:
StatementHandler statementHandler = (StatementHandler)invocation.getTarget(); MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,new DefaultReflectorFactory());
现在对象已经获取了,就要获取其中的值了,在具体操作之前,我们再来梳理一下这里的类间关系,如下:【这里需要阅读过前文程序执行流程,或者之前已有了解】
现在,我们终于可以去获取其属性值了,获取的方法为参数名,OGNL表达式。具体内容如下:
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement"); String id = mappedStatement.getId();
8.获得目标对象之后,现在要做进一步判断,即,当前被代理的对象是普通查询,还是分页查询。这里具体方式有很多,我们给出一种简单实现,更多实现及用法就行读者自行尝试吧。
如,我们在这里定义,id中(即mapper文件中sql语句的唯一id)以ByPage字符串结尾的,我们就认为该sql语句按照分页插叙来执行。具体的代码试下如下:
if(id.matches(".+ByPage$")){ //.... }
-------------------------------------------------------------------------------------------------------------------------------------
9.通过上面一系列的步骤,我们已经成功的捕获了目标对象。接下来,我们就要开始加入我们“神不知鬼不觉‘”的代码了。
【具体分为下面的几个步骤】
- 获取原始sql语句
- 执行满足一定条件的查询,查询出结果总数,用于计算分页页面总数
- 获取将dao层传入的分页参数
- 将获取分页查询的参数,用于重新拼装分页查询语句。
- 查询结束之后,交还程序执行流,退出拦截器
这里仍然需要前面执行流程的知识,默认情况下,执行的是PreparedStatementHandler。因此,我们可以从PreparedStatementHandler中寻找需要的sql语句。具体见上文第3步。这里仅给出省略内容,如下:
@Override protected Statement instantiateStatement(Connection connection) throws SQLException { String sql = boundSql.getSql(); //... }
由此,我们发现Sql语句隐藏在boundSql中。回头看看,已经获得了的StatementHandler,其具体内容我们也在上文截图给大家。
于是,就得到了下面的代码:
BoundSql boundSql = statementHandler.getBoundSql(); Map<String,Object> params = (Map<String,Object>)boundSql.getParameterObject();
现在,各位看官可以尖叫了!!!所有的材料已经准备齐了。
-------------------------------------------------------------------------------------------------------------------------------------
10.查询总数对应的Mysql语句。【其他数据库原理一致,读者自行完成即可】
String sql = boundSql.getSql(); String countSql = "select count(*)from ("+sql+")a";
【注意:这里我们已经获得了传入参数与完整的sql语句,因此,select count还有更加高效的写法,请读者自行完成。】
11.接下来就是执行查询总数的语句。其总数的值用于完成分页页面数量控制。执行方法如下:
【回顾我们的注解,args配置】
Connection connection = (Connection) invocation.getArgs()[0]; //利用原始sql语句的方法执行 PreparedStatement countStatement = connection.prepareStatement(countSql); //在本例中,查询参数为空,但实际应用时,多为带有条件的分页查询,下面的这句话就是获取查询条件的参数 ParameterHandler parameterHandler = (ParameterHandler) metaObject.getValue("delegate.parameterHandler"); //经过set方法,就可以正确的执行sql语句 parameterHandler.setParameters(countStatement); ResultSet rs = countStatement.executeQuery(); //当结果集中有值时,表示页面数量大于等于1 if(rs.next()){ page.setTotalNumber(rs.getInt(1)); }
到此,select count就执行完毕了。
11.分页的查询语句为:【其他数据库原理一致,读者自行完成即可】
String pageSql = sql+" limit "+page.getStartIndex()+","+page.getTotalSelect();
12.查询总数,分页查询的语句已经准备完成,如何替换原来的查询语句呢?再回头看看MetaObject对象,刚才,我们说它提供给我们获取或设置该对象的原本不可访问的属性。因此,就来利用它实现替换sql语句的功能。如下:【备注:这句话具体解释与上文getValue一致,请参考上文即可】
metaObject.setValue("delegate.boundSql.sql", pageSql);
到此,我们成功的把原有的简单查询语句替换为分页查询语句了,现在是时候将程序的控制权交还给Mybatis了,具体代码如下:
return invocation.proceed();
13.最后一步,自定义的拦截器需要交给Mybatis管理,这样才能使得Mybatis的执行与拦截器的执行结合在一起,即,拦截器需要注册到mybatis-config配置文件中。具体内容如下:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <properties resource="jdbc.properties"/> <settings> <setting name="logImpl" value="LOG4J"/> </settings> <typeAliases> <package name="com.csdn.ingo.entity"/> </typeAliases> <plugins> <plugin interceptor="com.csdn.ingo.interceptor.PageInterceptor"></plugin> </plugins> <environments default="development"> <environment id="development"> <transactionManager type="JDBC" /> <dataSource type="POOLED"> <property name="driver" value="${jdbc.driverClassName}" /> <property name="url" value="${jdbc.url}" /> <property name="username" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> </dataSource> </environment> </environments> <mappers> <mapper resource="mappers/UserInfoMapper.xml"/> </mappers> </configuration>
----------------------------------------------------------------------------------------------------------------------------------
上面是所有的原理内容,我们再给出完整的代码供大家学习
----------------------------------------------------------------------------------------------------------------------------------
14.PageInterceptor的完整内容如下:
package com.csdn.ingo.interceptor; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.util.Map; import java.util.Properties; 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.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.reflection.DefaultReflectorFactory; import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.reflection.ReflectorFactory; import org.apache.ibatis.reflection.SystemMetaObject; import com.csdn.ingo.entity.PagePOJO; /** *@author 作者 E-mail:ingo *@version 创建时间:2016年4月27日下午6:55:09 *类说明 */ @Intercepts({@Signature(type=StatementHandler.class,method="prepare",args={Connection.class})}) public class PageInterceptor implements Interceptor{ /* (non-Javadoc) * 拦截器要执行的方法 */ public Object intercept(Invocation invocation) throws Throwable { StatementHandler statementHandler = (StatementHandler)invocation.getTarget(); MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,new DefaultReflectorFactory()); MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement"); String id = mappedStatement.getId(); if(id.matches(".+ByPage$")){ BoundSql boundSql = statementHandler.getBoundSql(); Map<String,Object> params = (Map<String,Object>)boundSql.getParameterObject(); PagePOJO page = (PagePOJO)params.get("page"); String sql = boundSql.getSql(); String countSql = "select count(*)from ("+sql+")a"; Connection connection = (Connection) invocation.getArgs()[0]; PreparedStatement countStatement = connection.prepareStatement(countSql); ParameterHandler parameterHandler = (ParameterHandler) metaObject.getValue("delegate.parameterHandler"); parameterHandler.setParameters(countStatement); ResultSet rs = countStatement.executeQuery(); if(rs.next()){ page.setTotalNumber(rs.getInt(1)); } String pageSql = sql+" limit "+page.getStartIndex()+","+page.getTotalSelect(); metaObject.setValue("delegate.boundSql.sql", pageSql); } return invocation.proceed(); } /* (non-Javadoc) * 拦截器需要拦截的对象 */ public Object plugin(Object target) { return Plugin.wrap(target, this); } /* (non-Javadoc) * 设置初始化的属性值 */ public void setProperties(Properties properties) { } }
15.Mapper.xml文件中的sql语句如下:
<select id="selectByPage" parameterType="Map" resultMap="UserInfoResult"> select * from userinfo </select>
【注意:】
这里的sql语句就是简单查询
传入参数为Map,供多参,条件查询的等场景使用。【最好这么使用,因为拦截器只有1个】
16.执行单元测试方法,看看查询结果,如下:
如果看到类似上文输出,表明已经成功执行了分页查询!掌声!!!
----------------------------------------------------------------------------------------------------------------------------------
至此, Mybatis最入门---分页查询(拦截器分页原理及实现)结束
备注:
1.本文仅作为原理解释,实际应用时,建议各位看官最好使用更加严谨的第三方jar包,或者平台性的支持。
2.上文未给出的代码,在前文中均有给出,请读者自行查阅前文。
参考资料:
百度百科
特别感谢:http://blog.csdn.net/hupanfeng/article/details/9247379