Bootstrap

使用MyBatis 拦截器(Interceptor)实现自定义分页插件 使用@Param注解使得分页失效

今天再工作的过程当中,在使用分页插件的时候再dao层使用了@Param注解导致了分页失效。先上没改之前的代码。

PaginationInterceptor

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Map;
import java.util.Properties;

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
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.MetaObject;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import cn.jts.framework.component.Global;
import cn.jts.framework.persistence.Page;
import cn.jts.framework.utils.ReflectionUtils;
import cn.jts.framework.utils.StringUtils;


@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;

	public Object intercept(Invocation invocation) throws Throwable {
		final MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
		Object parameter  = invocation.getArgs()[1];
		BoundSql boundSql = mappedStatement.getBoundSql(parameter);
		Object parameterObject = boundSql.getParameterObject();
		String jdbcType = Global.getConfig("jdbc.type");

		// 获取分页参数对象
		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, jdbcType));

			// 分页查询 本地化对象 修改数据库注意修改实现
			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 (ReflectionUtils.getFieldValue(boundSql, "metaParameters") != null) {
				MetaObject mo = (MetaObject) ReflectionUtils.getFieldValue(boundSql, "metaParameters");
				ReflectionUtils.setFieldValue(newBoundSql, "metaParameters", mo);
			}
			// 解决MyBatis 分页foreach 参数失效 end
			MappedStatement newMs = copyFromMappedStatement(mappedStatement, new BoundSqlSqlSource(newBoundSql));

			invocation.getArgs()[0] = newMs;
		}
		return invocation.proceed();
	}

	public Object plugin(Object target) {
		return Plugin.wrap(target, this);
	}

	public void setProperties(Properties properties) {
		String jdbcType = Global.getConfig("jdbc.type");
		super.initProperties(properties, jdbcType);
	}

	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;
		}
	}

	public static Object mapToObject(Map<String, Object> paramMap, Class<?> beanClass) throws Exception {
		if (paramMap == null)
			return null;
		Object obj = beanClass.newInstance();
		Field[] fields = obj.getClass().getDeclaredFields();
		for (Field field : fields) {
			int mod = field.getModifiers();
			if (Modifier.isStatic(mod) || Modifier.isFinal(mod)) {
				continue;
			}
			field.setAccessible(true);
			if (paramMap.containsKey(field.getName())) {
				field.set(obj, paramMap.get(field.getName()));
			}
		}
		return obj;


	}
}

SQLHelper

public class SQLHelper {

	/**
	 * 对SQL参数(?)设值,参考org.apache.ibatis.executor.parameter.DefaultParameterHandler
	 *
	 * @param ps              表示预编译的 SQL 语句的对象。
	 * @param mappedStatement MappedStatement
	 * @param boundSql        SQL
	 * @param parameterObject 参数对象
	 * @throws java.sql.SQLException 数据库异常
	 */
	@SuppressWarnings("unchecked")
	public static void setParameters(PreparedStatement ps, MappedStatement mappedStatement, BoundSql boundSql, Object parameterObject) throws SQLException {
		ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
		List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
		if (parameterMappings != null) {
			Configuration configuration = mappedStatement.getConfiguration();
			TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
			MetaObject metaObject = parameterObject == null ? null : configuration.newMetaObject(parameterObject);
			for (int i = 0; i < parameterMappings.size(); i++) {
				ParameterMapping parameterMapping = parameterMappings.get(i);
				if (parameterMapping.getMode() != ParameterMode.OUT) {
					Object value;
					String propertyName = parameterMapping.getProperty();
					PropertyTokenizer prop = new PropertyTokenizer(propertyName);
					if (parameterObject == null) {
						value = null;
					} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
						value = parameterObject;
					} else if (boundSql.hasAdditionalParameter(propertyName)) {
						value = boundSql.getAdditionalParameter(propertyName);
					} else if (propertyName.startsWith(ForEachSqlNode.ITEM_PREFIX) && boundSql.hasAdditionalParameter(prop.getName())) {
						value = boundSql.getAdditionalParameter(prop.getName());
						if (value != null) {
							value = configuration.newMetaObject(value).getValue(propertyName.substring(prop.getName().length()));
						}
					} else {
						value = metaObject == null ? null : metaObject.getValue(propertyName);
					}
					@SuppressWarnings("rawtypes")
					TypeHandler typeHandler = parameterMapping.getTypeHandler();
					if (typeHandler == null) {
						throw new ExecutorException("There was no TypeHandler found for parameter " + propertyName + " of statement " + mappedStatement.getId());
					}
					typeHandler.setParameter(ps, i + 1, value, parameterMapping.getJdbcType());
				}
			}
		}
	}

	/**
	 * 查询总纪录数
	 * @param sql             SQL语句
	 * @param connection      数据库连接
	 * @param mappedStatement mapped
	 * @param parameterObject 参数
	 * @param boundSql        boundSql
	 * @return 总记录数
	 * @throws SQLException sql查询错误
	 */
	public static int getCount(final String sql, final Connection connection, final MappedStatement mappedStatement, final Object parameterObject, final BoundSql boundSql, Log log, String jdbcType) throws SQLException {
		String dbName = jdbcType;
		final String countSql;
		if ("oracle".equals(dbName)) {
			countSql = "select count(1) from (" + sql + ") tmp_count";
		} else {
			countSql = "select count(1) from (" + removeOrders(sql) + ") tmp_count";
		}
		Connection conn = connection;
		PreparedStatement ps = null;
		ResultSet rs = null;
		try {
			if (log.isDebugEnabled()) {
				log.debug("COUNT SQL: " + StringUtils.replaceEach(countSql, new String[] { "\n", "\t" }, new String[] { " ", " " }));
			}
			if (conn == null) {
				conn = mappedStatement.getConfiguration().getEnvironment().getDataSource().getConnection();
			}
			ps = conn.prepareStatement(countSql);
			BoundSql countBS = new BoundSql(mappedStatement.getConfiguration(), countSql, boundSql.getParameterMappings(), parameterObject);
			// 解决MyBatis 分页foreach 参数失效 start
			if (ReflectionUtils.getFieldValue(boundSql, "metaParameters") != null) {
				MetaObject mo = (MetaObject) ReflectionUtils.getFieldValue(boundSql, "metaParameters");
				ReflectionUtils.setFieldValue(countBS, "metaParameters", mo);
			}
			// 解决MyBatis 分页foreach 参数失效 end
			SQLHelper.setParameters(ps, mappedStatement, countBS, parameterObject);
			rs = ps.executeQuery();
			int count = 0;
			if (rs.next()) {
				count = rs.getInt(1);
			}
			return count;
		} finally {
			if (rs != null) {
				rs.close();
			}
			if (ps != null) {
				ps.close();
			}
			if (conn != null) {
				conn.close();
			}
		}
	}

	/**
	 * 根据数据库方言,生成特定的分页sql
	 * @param sql     Mapper中的Sql语句
	 * @param page    分页对象
	 * @param dialect 方言类型
	 * @return 分页SQL
	 */
	public static String generatePageSql(String sql, Page<Object> page, Dialect dialect) {
		if (dialect.supportsLimit()) {
			return dialect.getLimitString(sql, page.getFirstResult(), page.getMaxResults());
		} else {
			return sql;
		}
	}

	/** 
	 * 去除qlString的select子句。 
	 * @param hql 
	 * @return 
	 */
	@SuppressWarnings("unused")
	private static String removeSelect(String qlString) {
		int beginPos = qlString.toLowerCase().indexOf("from");
		return qlString.substring(beginPos);
	}

	/** 
	 * 去除hql的orderBy子句。 
	 * @param hql 
	 * @return 
	 */
	@SuppressWarnings("unused")
	private static String removeOrders(String qlString) {
		Pattern p = Pattern.compile("order\\s*by[\\w|\\W|\\s|\\S]*", Pattern.CASE_INSENSITIVE);
		Matcher m = p.matcher(qlString);
		StringBuffer sb = new StringBuffer();
		while (m.find()) {
			m.appendReplacement(sb, "");
		}
		m.appendTail(sb);
		return sb.toString();
	}

}




BaseInterceptor

public abstract class BaseInterceptor implements Interceptor, Serializable {

	private static final long serialVersionUID = 1L;

	protected static final String PAGE = "page";

	protected static final String DELEGATE = "delegate";

	protected static final String MAPPED_STATEMENT = "mappedStatement";

	protected Log log = LogFactory.getLog(this.getClass());

	protected Dialect DIALECT;

	/**
	 * 对参数进行转换和检查
	 * @param parameterObject 参数对象
	 * @param page            分页对象
	 * @return 分页对象
	 * @throws NoSuchFieldException 无法找到参数
	 */
	@SuppressWarnings("unchecked")
	protected static Page<Object> convertParameter(Object parameterObject, Page<Object> page) {
		try {
			if (parameterObject instanceof Page) {
				return (Page<Object>) parameterObject;
			} else {
				return (Page<Object>) ReflectionUtils.getFieldValue(parameterObject, PAGE);
			}
		} catch (Exception e) {
			return null;
		}
	}

	/**
	 * 设置属性,支持自定义方言类和制定数据库的方式
	 * <code>dialectClass</code>,自定义方言类。可以不配置这项
	 * <ode>dbms</ode> 数据库类型,插件支持的数据库
	 * <code>sqlPattern</code> 需要拦截的SQL ID
	 * @param p 属性
	 */
	protected void initProperties(Properties p, String jdbcType) {
		Dialect dialect = null;
		String dbType = jdbcType;
		if ("mysql".equals(dbType)) {
			dialect = new MySQLDialect();
		}
		if (dialect == null) {
			throw new RuntimeException("mybatis dialect error.");
		}
		DIALECT = dialect;
	}
}

在我在调试代码的过程中发现了就是这一段代码出了问题:

在这里插入图片描述
*

我们先看看没有加@Param注解代码是如何走的:

在这里插入图片描述

这里再拦截当中invocation.getArgs()[1];是解析到了dao层的参数。所以往下走:

在这里插入图片描述
这里也是能成功解析到了Pag对象。

接着们看一下加了@Param注解代码是如何走的(导致分页失效):

在这里插入图片描述

之前我们在之前invocation.getArgs()[1]是拦截到的实体类这里确是在实体类外面包了一成Map所以导致了我们解析Page对象失败了导致了分页失效:

在这里插入图片描述

然后为了解决这个问题我特意看了一下@Param注解源码实现如下:

跟踪到源码进入org.apache.ibatis.binding.MapperMethod

MapperMethod类是处理mapper接口中方法的真正处理器,该类内部的execute明确了代理的方法参数要怎么处理,查询得到的结果怎么封装然后返回

public class MapperMethod {
  //对执行的SQL标签的封装,包含SQL类型和任务的完整ID等信息
  private final SqlCommand command; 
  //代理方法的签名,其内部封装了一系列操作,如方法多参数时对@Param注解的处理
  private final MethodSignature method; 

  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) { //针对DML/DQL操作执行
      case INSERT: {
    	Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      case UPDATE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
      }
      case DELETE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.delete(command.getName(), param));
        break;
      }
      case SELECT:
        if (method.returnsVoid() && method.hasResultHandler()) {
          executeWithResultHandler(sqlSession, args);
          result = null;
        } else if (method.returnsMany()) {
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
          result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {
          result = executeForCursor(sqlSession, args);
        } else {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = sqlSession.selectOne(command.getName(), param);
        }
        break;
      case FLUSH:
        result = sqlSession.flushStatements();
        break;
      default:
        throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
      throw new BindingException("Mapper method '" + command.getName() 
          + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
  }
}

这里我们注意一下这段代码:
在这里插入图片描述
method.convertArgsToSqlCommandParam(args)获取到实际输入的参数,获取到的是个Map集合,对于单一实体例如User那么获取到的就是该实体
然后进入convertArgsToSqlCommandParam

在这里插入图片描述
在进入:
在这里插入图片描述
好了看到这段代码,意思如下在使用了这个注解的实体类,先会获取到实体类然后把丢到一个ParaMap当中多个实体类也会循环丢入ParaMap,这样就可以共通过@Param(value = “attributeBo”)(K,V)的形式来获取到对应的实体类。!

解决方案

当知道使用@Param的时候回吧实体封装到一个ParaMap数组里面之后,这样我们只需把实体类从ParaMap解析出来就可以了:

新增代码:修改BaseInterceptor类中的convertParameter方法
				if(parameterObject instanceof Map){
					Map<String, Object> param = (Map<String, Object>)parameterObject;
					Collection<Object> values = param.values() ;// 得到全部的value
					Iterator<Object> iter = values.iterator() ;
					while(iter.hasNext()){
						Object mapValue = iter.next() ;
						if(mapValue  != null){
							parameterObject = mapValue;
						}
					}
				}
全部代码:修改完之后BaseInterceptor全部代码:
public abstract class BaseInterceptor implements Interceptor, Serializable {

	private static final long serialVersionUID = 1L;

	protected static final String PAGE = "page";

	protected static final String DELEGATE = "delegate";

	protected static final String MAPPED_STATEMENT = "mappedStatement";

	protected Log log = LogFactory.getLog(this.getClass());

	protected Dialect DIALECT;

	/**
	 * 对参数进行转换和检查
	 * @param parameterObject 参数对象
	 * @param page            分页对象
	 * @return 分页对象
	 * @throws NoSuchFieldException 无法找到参数
	 */
	@SuppressWarnings("unchecked")
	protected static Page<Object> convertParameter(Object parameterObject, Page<Object> page) {
		try {
			if (parameterObject instanceof Page) {
				return (Page<Object>) parameterObject;
			} else {
				if(parameterObject instanceof Map){
					Map<String, Object> param = (Map<String, Object>)parameterObject;
					Collection<Object> values = param.values() ;// 得到全部的value
					Iterator<Object> iter = values.iterator() ;
					while(iter.hasNext()){
						Object mapValue = iter.next() ;
						if(mapValue  != null){
							parameterObject = mapValue;
						}
					}
				}	
				return (Page<Object>) ReflectionUtils.getFieldValue(parameterObject, PAGE);
			}
		} catch (Exception e) {
			return null;
		}
	}

	/**
	 * 设置属性,支持自定义方言类和制定数据库的方式
	 * <code>dialectClass</code>,自定义方言类。可以不配置这项
	 * <ode>dbms</ode> 数据库类型,插件支持的数据库
	 * <code>sqlPattern</code> 需要拦截的SQL ID
	 * @param p 属性
	 */
	protected void initProperties(Properties p, String jdbcType) {
		Dialect dialect = null;
		String dbType = jdbcType;
		if ("mysql".equals(dbType)) {
			dialect = new MySQLDialect();
		}
		if (dialect == null) {
			throw new RuntimeException("mybatis dialect error.");
		}
		DIALECT = dialect;
	}
}

添加之后我们再看一下效果:
在这里插入图片描述
ok。没事收工

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;