前言
其实吧,这个分页的封装是我从mybatis实战上抄的,然后又重构了下代码,形成了自己的。现在之所以会记录一下,主要原因是出现了质变———对foreach的支持,而解决这个问题的过程中,我感觉,应该基本上使用上没有多少局限行了。下面说说实际的吧。
设计
基本的设计思路,是使用mybatis插件,首先是下面这一串注解:
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class PageInterceptor implements Interceptor {
它说明了我们在什么样的生命周期以什么样的参数来进行拦截,具体的记不太清了,有时间再回来整理这个注解的详细内容吧。然后,我们在里面自动得多进行一次count请求,由于结果集不好改变,所以我们塞到入参里返回,然后再自动得把分页参数加到查询的sql末尾。由于我们只用mysql,所以就不做数据库方言了。
实现
不多说了,实现了Interceptor要重写一个方法,如下:
// 获取方法名
DEFAULT_OBJECT_FACTORY,DEFAULT_OBJECT_WRAPPER_FACTORY);
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaStatementHandler = MetaObject.forObject(statementHandler, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY,new DefaultReflectorFactory());
// 分离代理对象链(由于目标类可能被多个拦截器拦截,从而形成多次代理,通过下面的两次循环可以分离出最原始的的目标类)
while (metaStatementHandler.hasGetter("h")) {
Object object = metaStatementHandler.getValue("h");
metaStatementHandler = MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY,new DefaultReflectorFactory());
}
// 分离最后一个代理对象的目标类
while (metaStatementHandler.hasGetter("target")) {
Object object = metaStatementHandler.getValue("target");
metaStatementHandler = MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY,new DefaultReflectorFactory());
}
//获取selectid
MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
String selectId = mappedStatement.getId();
int count = 0;
// 如果以指定词结尾则处理
if (selectId.endsWith(this.endWord)) {
BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");
//获取参数
Map params = (Map) (boundSql.getParameterObject());
count = this.getTotal(invocation, metaStatementHandler, boundSql);
params.put("count", count);
return this.getLimitedData(invocation, metaStatementHandler, boundSql, params);
}
Object result = invocation.proceed();
return result;
前面的一大截都是获取对应的statement,然后就是获取selectId,然后是判断selectId是不是要拦截的,如果是,就先进行一次count,再查询数据。getTotal方法就是获取总条数的方法,返回值就是总条数,会直接塞到入参里,具体代码如下:
private int getTotal(Invocation ivt, MetaObject metaStatementHandler, BoundSql boundSql) throws SQLException, NoSuchFieldException, IllegalAccessException {
//获取当前的mappedStatement
MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
//配置对象
Configuration cfg = mappedStatement.getConfiguration();
//当前需要执行的sql
String sql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");
//改写sql
String countSql = this.concatCountSql(sql);
//获取拦截方法参数,我们知道是Connection对象
Connection connection = (Connection) ivt.getArgs()[0];
PreparedStatement ps = null;
int total = 0;
try {
//预编译统计总数sql
ps = connection.prepareStatement(countSql);
//构建统计总数BoundSql
BoundSql countBoundSql = new BoundSql(cfg, countSql, boundSql.getParameterMappings(), boundSql.getParameterObject());
//将原来的metaParameters复制到新的countBoundSql中
Field metaParamsField = ReflectUtil.getFieldByFieldName(countBoundSql, "metaParameters");
if (metaParamsField != null) {
MetaObject mo = (MetaObject) ReflectUtil.getValueByFieldName(boundSql, "metaParameters");
ReflectUtil.setValueByFieldName(countBoundSql, "metaParameters", mo);
}
//将原来的additionalParameters复制到新的countBoundSql中
Field additionalParametersField = ReflectUtil.getFieldByFieldName(countBoundSql, "additionalParameters");
if (additionalParametersField != null) {
Object mo = ReflectUtil.getValueByFieldName(boundSql, "additionalParameters");
ReflectUtil.setValueByFieldName(countBoundSql, "additionalParameters", mo);
}
//构建Mybatis的ParameterHandler用来设置总数sql参数
ParameterHandler handler = new DefaultParameterHandler(mappedStatement, boundSql.getParameterObject(), countBoundSql);
//设置总数sql参数
handler.setParameters(ps);
//执行查询
ResultSet rs = ps.executeQuery();
while (rs.next()) {
total = rs.getInt(1);
}
} finally {
}
return total;
}
这个方法比较长,实际的基本思路是,获得原来的sql,拼接count sql,再用就的bound构造新的bound,再执行新的查询。之前一直无法使用foreach标签,关键点在于
//将原来的metaParameters复制到新的countBoundSql中
Field metaParamsField = ReflectUtil.getFieldByFieldName(countBoundSql, "metaParameters");
if (metaParamsField != null) {
MetaObject mo = (MetaObject) ReflectUtil.getValueByFieldName(boundSql, "metaParameters");
ReflectUtil.setValueByFieldName(countBoundSql, "metaParameters", mo);
}
//将原来的additionalParameters复制到新的countBoundSql中
Field additionalParametersField = ReflectUtil.getFieldByFieldName(countBoundSql, "additionalParameters");
if (additionalParametersField != null) {
Object mo = ReflectUtil.getValueByFieldName(boundSql, "additionalParameters");
ReflectUtil.setValueByFieldName(countBoundSql, "additionalParameters", mo);
}
这四行将构造新bound没带过来,但是执行这些绑定又必须的东西带过来了,然后,才匹配上了。最后我们看看getLimitedData
private Object getLimitedData(Invocation invocation, MetaObject metaStatementHandler, BoundSql boundSql, Map param) throws InvocationTargetException, IllegalAccessException {
//获取当前需要执行的sql
String sql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");
//修改sql,这里使用的是mysql,如果是其他数据库则需要修改
if(null != param.get(this.pageNo) && null != param.get(this.pageSize)){
sql = this.concatPageSql(sql, param);
}
//修改当前需要执行的sql
metaStatementHandler.setValue("delegate.boundSql.sql", sql);
//相当于调用StatementeHandler的prepare方法,预编译了当前sql,并设置原有的参数,但是少了两个分页参数,它返回的是一个PreparedStatement对象
PreparedStatement ps = (PreparedStatement) invocation.proceed();
return ps;
}
其实很简单,就是改了下sql,然后执行查询返回。
虽然现在遇见的问题都解决了,但是很可能我的实现还是有问题的,因为Interceptor提供要重写的方法不止这一个,而我还不知道其它的都干什么用,后面慢慢研究吧。
配置
插件写出来了,就要想想怎么生效
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="configLocation" value="classpath:/mybatis/mybatis-config.xml" />
<property name="dataSource" ref="dataSource" />
<!-- 自动扫描mapping.xml文件 -->
<property name="mapperLocations" value="classpath*:/net/dgg/tmd/foundation/platform/**/*Mapper.xml"></property>
<!--配置分页插件-->
<property name="plugins">
<list>
<bean class="net.dgg.tmd.foundation.platform.common.page.PageInterceptor" />
</list>
</property>
</bean>
好了,打完收工,后面整理出来具体的例子再详细维护吧,这里已经基本说完了这个插件的关键实现了。