jeesite分页的使用
可行的分页姿势
jeesite的分页,限定了查询方法的参数只能有一个,多个参数就会失效。这个参数可以是以下三种类型:jeesite的Page、Page的子类、包含Page属性的类。
下面是几个试验:
//dao层的定义
/**
* 参数:Page类型
* 结果:分页成功
*/
List<String> test(Page<String> page);
/**
* 参数:Page的子类
* 结果:分页成功
*/
List<String> test(ExtendPageEntity page);
/**
* 参数:包含Page属性的类
* 结果:分页成功
*/
List<String> test(WithPageEntity page);
/**
* 参数:完全跟Page没关系的类+Page类
* 结果:分页失败
*/
List<String> test(Page page,NoPageEntity entity);
实验用到的相关类型
public class ExtendPageEntity extends Page {
}
@Data
public class WithPageEntity {
private Page page;
}
public class NoPageEntity {
}
只能存在一个参数,就很不习惯,jeesite的实体类都默认继承了个BaseEntity,里面都有Page属性,这就很容易造成我不想分页的时候,自动分页了,事实上也有同事踩过坑,所以下面先看下为什么只能有一个参数,其次尝试改造,让其可以向MybatiePlue的分页一样可以传多个参数,把Page独立出来。
源码探索
在mybatis-config.xml里面,我们可以看到分页插件的配置,定位到拦截的类。
<plugins>
<plugin interceptor="com.jeesite.common.persistence.interceptor.PaginationInterceptor"/>
</plugins>
/**
* 数据库分页插件,只拦截查询语句.
*
* @author poplar.yfyang / thinkgem
* @version 2013-8-28
*/
@Intercepts({@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class PaginationInterceptor extends BaseInterceptor {
private static final long serialVersionUID = 1L;
@Override
public Object intercept(Invocation invocation) throws Throwable {
final MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
// //拦截需要分页的SQL
if (mappedStatement.getId().matches(_SQL_PATTERN)) {
// if (StringUtils.indexOfIgnoreCase(mappedStatement.getId(), _SQL_PATTERN) != -1) {
Object parameter = invocation.getArgs()[1];
BoundSql boundSql = mappedStatement.getBoundSql(parameter);
Object parameterObject = boundSql.getParameterObject();
//获取分页参数对象
Page<Object> page = null;
if (parameterObject != null) {
page = convertParameter(parameterObject, page);
}
//如果设置了分页对象,则进行分页
if (page != null && page.getPageSize() != -1) {
if (StringUtils.isBlank(boundSql.getSql())) {
return null;
}
String originalSql = boundSql.getSql().trim();
//得到总记录数
page.setCount(SQLHelper.getCount(originalSql, null, mappedStatement, parameterObject, boundSql, log));
//分页查询 本地化对象 修改数据库注意修改实现
String pageSql = SQLHelper.generatePageSql(originalSql, page, DIALECT);
// if (log.isDebugEnabled()) {
// log.debug("PAGE SQL:" + StringUtils.replace(pageSql, "\n", ""));
// }
invocation.getArgs()[2] = new RowBounds(RowBounds.NO_ROW_OFFSET, RowBounds.NO_ROW_LIMIT);
BoundSql newBoundSql = new BoundSql(mappedStatement.getConfiguration(), pageSql, boundSql.getParameterMappings(), boundSql.getParameterObject());
//解决MyBatis 分页foreach 参数失效 start
if (Reflections.getFieldValue(boundSql, "metaParameters") != null) {
MetaObject mo = (MetaObject) Reflections.getFieldValue(boundSql, "metaParameters");
Reflections.setFieldValue(newBoundSql, "metaParameters", mo);
}
//解决MyBatis 分页foreach 参数失效 end
MappedStatement newMs = copyFromMappedStatement(mappedStatement, new BoundSqlSqlSource(newBoundSql));
invocation.getArgs()[0] = newMs;
}
// }
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
super.initProperties(properties);
}
private MappedStatement copyFromMappedStatement(MappedStatement ms,
SqlSource newSqlSource) {
MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(),
ms.getId(), newSqlSource, ms.getSqlCommandType());
builder.resource(ms.getResource());
builder.fetchSize(ms.getFetchSize());
builder.statementType(ms.getStatementType());
builder.keyGenerator(ms.getKeyGenerator());
if (ms.getKeyProperties() != null) {
for (String keyProperty : ms.getKeyProperties()) {
builder.keyProperty(keyProperty);
}
}
builder.timeout(ms.getTimeout());
builder.parameterMap(ms.getParameterMap());
builder.resultMaps(ms.getResultMaps());
builder.cache(ms.getCache());
builder.useCache(ms.isUseCache());
return builder.build();
}
public static class BoundSqlSqlSource implements SqlSource {
BoundSql boundSql;
public BoundSqlSqlSource(BoundSql boundSql) {
this.boundSql = boundSql;
}
public BoundSql getBoundSql(Object parameterObject) {
return boundSql;
}
}
}
直接挑重点看,很明显一个给page赋值的地方:
page = convertParameter(parameterObject, page);
/**
* 对参数进行转换和检查
* @param parameterObject 参数对象
* @param page 分页对象
* @return 分页对象
* @throws NoSuchFieldException 无法找到参数
*/
protected static Page<Object> convertParameter(Object parameterObject, Page<Object> page) {
try{
if (parameterObject instanceof Page) {
return (Page<Object>) parameterObject;
} else {
return (Page<Object>)Reflections.getFieldValue(parameterObject, PAGE);
}
}catch (Exception e) {
return null;
}
}
convertParameter这个方法,首先是判断这个参数是否Page类型,是就直接返回自己就好了,否则进Reflections.getFieldValue()方法。
/**
* 直接读取对象属性值, 无视private/protected修饰符, 不经过getter函数.
*/
public static Object getFieldValue(final Object obj, final String fieldName) {
Field field = getAccessibleField(obj, fieldName);
if (field == null) {
throw new IllegalArgumentException("Could not find field [" + fieldName + "] on target [" + obj + "]");
}
Object result = null;
try {
result = field.get(obj);
} catch (IllegalAccessException e) {
logger.error("不可能抛出的异常{}", e.getMessage());
}
return result;
}
很明显,里面就第一行里面的getAccessibleField方法是实际干活的。
/**
* 循环向上转型, 获取对象的DeclaredField, 并强制设置为可访问.
*
* 如向上转型到Object仍无法找到, 返回null.
*/
public static Field getAccessibleField(final Object obj, final String fieldName) {
Validate.notNull(obj, "object can't be null");
Validate.notBlank(fieldName, "fieldName can't be blank");
for (Class<?> superClass = obj.getClass(); superClass != Object.class; superClass = superClass.getSuperclass()) {
try {
Field field = superClass.getDeclaredField(fieldName);
makeAccessible(field);
return field;
} catch (NoSuchFieldException e) {//NOSONAR
// Field不在当前类定义,继续向上转型
continue;// new add
}
}
return null;
}
很明显getAccessibleField这方法就是不断的尝试获取名为page的属性,没有就向上找父类的属性,直到object类结束。
从上面几个方法就可以看到,jeesite只会判断一个入参,然后判断这个参数是否Page类型或者是否包含Page属性,有就取里面的值加工sql。当有多个入参的时候,通过打断点debug发现,拦截器拿到的属性,就会转换成一个Map类型,虽然这个map里面的内容包含了page,但Map类型本身既不是Page的子类,也不包含Page属性,自然就拿不到page参数了。
改造让其允许多个入参
上面看完源码可以发现,问题在于jeesite的分页没有尝试去取map里面的内容,导致错过了page信息,那改造思路就很清晰了,判断入参类型是map,就遍历这个map,找出Page类型的参数,ok。
干活,改造convertParameter方法,其实就是加多一个if。
protected static Page<Object> convertParameter(Object parameterObject, Page<Object> page) {
try{
if (parameterObject instanceof Page) {
return (Page<Object>) parameterObject;
}
if (parameterObject instanceof Map){
Method method = parameterObject.getClass().getMethod("get",Object.class);
return (Page<Object>) method.invoke(parameterObject, "page");
}
return (Page<Object>)Reflections.getFieldValue(parameterObject, PAGE);
}catch (Exception e) {
e.printStackTrace();
return null;
}
}
结果发现分页还是失败,奇怪了,debug看下,发现map的key命名是0,1,param1,param2,就没有一个叫"page"的。那就便利这个map,每个成员值再判断下是否Page类型。
if (parameterObject instanceof Map) {
Page<Object> page1 = null;
try {
Map<String, Object> map = (Map<String, Object>) parameterObject;
for (Map.Entry<String, Object> entry : map.entrySet()) {
if (entry.getValue() instanceof Page) {
page1 = (Page<Object>) entry.getValue();
break;
}
}
} catch (Exception e) {
}
return page1;
}
结果很喜人,分页成功。
经过这番研究,其实还是看出了jeestie分页的一些问题,原版是根据命名取值,当入参对象里面的一个参数叫"page"时,就会取这个参数强转成Page类型,那如果我这个page不是Page类型呢,就出问题了吧。