拦截器实现分页

因为项目中一般肯定不止一个列表页面,很多地方需要实习分页功能,因此用拦截器来实现分页大大减少了我们的代码量


在这之前可以看一下这篇文章了解一下Mybatis通过动态代理实现拦截器(插件)的原理:http://zhangbo-peipei-163-com.iteye.com/blog/2033832



拦截器主要做的事有三件:


1.要拦截住


2.拦截下来做什么事


3.事情完成后交回主权



注意事项:


要拦截什么样的对象


拦截对象什么行为


什么时候拦截



因为Mybatis作为持久层框架其底层封装的也只是jdbc的代码,如果我们能通过动态代理对执行查询操作的sql加以分页改造,不就实现了分页拦截器么,比如log4j它就能把即将执行的sql打印出来。动态代理是关键!


Myabtis早已为我们提供了"拦截器"接口,以便我们按自己的需要对源码的动作进行修改




参照一下jdbc的代码,我们分析得知应该在


Mybatis源码中执行类似PreparedStatement   statement=conn.prepareStatement("...........");时拦截住,并做相应分页修改




为了配合拦截器新增一个查询列表方法


Command.xml


<select id="queryCommandListByPage" parameterType="java.util.Map" resultMap="command">
    select a.id c_id,a.name,a.description,b.content,b.id,b.command_id 
    from command a left join command_content b
    on a.id=b.command_id
    <where>
   
    	<if test="command.name != null and !"".equals(command.name.trim())">
	    	and a.name=#{command.name}
	    </if>
	    <if test="command.description != null and !"".equals(command.description.trim())">
	    	and a.description like '%' #{command.description} '%'
	    </if>
	 
    </where>
    order by id 
  </select>

跟原方法相比去掉了最后的limit操作






新建com.csdn.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.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
**
 * 分页拦截器
 */
//@Intercepts注解描述要拦截的接口的方法及方法参数
@Intercepts({@Signature(type=StatementHandler.class,method="prepare",args={Connection.class})})
public class PageInterceptor implements Interceptor{
	//invocation中有将被我们拦截下来的对象,里面有sql语句和参数,通过对sql的分页改造实现分页功能
	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		//被拦截对象StatementHandler
		StatementHandler statementHandler=(StatementHandler)invocation.getTarget();
		MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
		MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
		//此id为Mapper映射文件中sql语句的id
		String id = mappedStatement.getId();
		if(id.matches(".+ByPage$")){
			BoundSql boundSql = statementHandler.getBoundSql();
			//等待处理的sql
			String sql = boundSql.getSql();
			// 查询总条数的SQL语句
			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();
			Map<?,?> parameter = (Map<?,?>)boundSql.getParameterObject();
			Page page = (Page)parameter.get("page");
			if(rs.next()) {
				page.setTotalNumber(rs.getInt(1));
			}
			// 改造后带分页查询的SQL语句
			String pageSql = sql + " limit " + page.getDbIndex() + "," + page.getDbNumber();
			metaObject.setValue("delegate.boundSql.sql", pageSql);
		}
		return invocation.proceed();
	}
	//对拦截的对象进行过滤
	@Override
	public Object plugin(Object target) {
		return Plugin.wrap(target, this);
	}

	@Override
	public void setProperties(Properties properties) {
		
	}
	
	
}

Myabtis是在StatementHandler中获取到Statement的


StatementHandler.class:


public interface StatementHandler {

  Statement prepare(Connection connection)
      throws SQLException;

  void parameterize(Statement statement)
      throws SQLException;

  void batch(Statement statement)
      throws SQLException;

  int update(Statement statement)
      throws SQLException;

  <E> List<E> query(Statement statement, ResultHandler resultHandler)
      throws SQLException;

  BoundSql getBoundSql();

  ParameterHandler getParameterHandler();

}



第一个方法prepare方法的返回值是Statement且参数是Connection,我们应该可以通过此方法拦截Statement


StatementHandler接口有两个实现类:BaseStatementHandler,RoutingStatementHandler



RoutingStatementHandler.class:


public class RoutingStatementHandler implements StatementHandler {

  private final StatementHandler delegate;

  public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {

    switch (ms.getStatementType()) {
      case STATEMENT:
        delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case PREPARED:
        delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case CALLABLE:
        delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      default:
        throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
    }

  }

  @Override
  public Statement prepare(Connection connection) throws SQLException {
    return delegate.prepare(connection);
  }
..............................................
}

这里的prepare方法只是调用的SimpleStatementHandler等BaseStatementHandler子类的prepare方法


不过观察这些子类的源码,他们并没有重写父类BaseStatementHandler的prepare方法,可见调用的还是父类的方法



BaseStatementHandler.class:


public abstract class BaseStatementHandler implements StatementHandler {

  protected final Configuration configuration;
  protected final ObjectFactory objectFactory;
  protected final TypeHandlerRegistry typeHandlerRegistry;
  protected final ResultSetHandler resultSetHandler;
  protected final ParameterHandler parameterHandler;

  protected final Executor executor;
  protected final MappedStatement mappedStatement;
  protected final RowBounds rowBounds;

  protected BoundSql boundSql;

  protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    this.configuration = mappedStatement.getConfiguration();
    this.executor = executor;
    this.mappedStatement = mappedStatement;
    this.rowBounds = rowBounds;

    this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
    this.objectFactory = configuration.getObjectFactory();

    if (boundSql == null) { // issue #435, get the key before calculating the statement
      generateKeys(parameterObject);
      boundSql = mappedStatement.getBoundSql(parameterObject);
    }

    this.boundSql = boundSql;

    this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
    this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
  }

..................................................
@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);
    }
  }
  
  
 protected abstract Statement instantiateStatement(Connection connection) throws SQLException;
 
...........................................
 
 
 }

prepare方法中的statement来自instantiateStatement(connection);



不过父类的instantiateStatement方法只是抽象方法,其"实现"在三个子类中:CallableStatementHandler,PreparedStatementHandler,SimpleStatementHandler


推断应在PreparedStatementHandler中查找




public class PreparedStatementHandler extends BaseStatementHandler {

  public PreparedStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    super(executor, mappedStatement, parameter, rowBounds, resultHandler, boundSql);
  }
............................
@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(sql........);方法以及String sql = boundSql.getSql();我们就可以在这里对Sql进行改造并塞回去让程序继续进行





OK,源码看的差不多了,现在回到PageInterceptor,它有三个重写的方法


先看plugin方法


//对拦截的对象进行过滤
	@Override
	public Object plugin(Object target) {
		return Plugin.wrap(target, this);
	}



查看Plugin.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源码:


private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
    // issue #251
    if (interceptsAnnotation == null) {
      throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());      
    }
    Signature[] sigs = interceptsAnnotation.value();
    Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
    for (Signature sig : sigs) {
      Set<Method> methods = signatureMap.get(sig.type());
      if (methods == null) {
        methods = new HashSet<Method>();
        signatureMap.put(sig.type(), methods);
      }
      try {
        Method method = sig.type().getMethod(sig.method(), sig.args());
        methods.add(method);
      } catch (NoSuchMethodException e) {
        throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
      }
    }
    return signatureMap;
  }




Proxy.newProxyInstance( 
 
           type.getClassLoader(), 
 
           interfaces, 
 
           new Plugin(target, interceptor, signatureMap));根据newProxyInstance方法参数,我们继续看Plugin.class:


这是Plugin中的invoke方法


@Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      if (methods != null && methods.contains(method)) {
        return interceptor.intercept(new Invocation(target, method, args));
      }
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }

学过动态代理的应该知道代理实例调用的方法中最关键的就是调用了invoke方法,在invoke中就调用了interceptor的intercept方法。


因此开始编写intercept方法:


//invocation中有将被我们拦截下来的对象,里面有sql语句和参数,通过对sql的分页改造实现分页功能
	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		//被拦截对象StatementHandler
		StatementHandler statementHandler=(StatementHandler)invocation.getTarget();
		/*根据BaseStatementHandler源码得知,其属性mappedStatement是protected类型的,而且类中也没有提供mappedStatement的public的get方法,
		 * 那么如何获得mappedStatement呢?用反射!MetaObject是Mybatis封装好的一个通过反射来获得一个对象中属性的工具类
		*/
		MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
		/*getValue()的参数以OGNL表达式形式书写。被拦截到的应是RoutingStatementHandler对象,其属性delegate在构造函数中被PreparedStatementHandler
		 * 实例赋值,而mappedStatement是PreparedStatementHandler父类的属性,因此书写delegate.mappedStatement
		*/
		MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
		//此id为Mapper映射文件中sql语句的id
		String id = mappedStatement.getId();
		//".+ByPage$"是正则表达式,指以ByPage结尾的方法名
		if(id.matches(".+ByPage$")){
			BoundSql boundSql = statementHandler.getBoundSql();
			//等待处理的sql
			String sql = boundSql.getSql();
			// 查询总条数的SQL语句
			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用它所掌握的参数信息给countStatement的参数赋值
			parameterHandler.setParameters(countStatement);
			ResultSet rs = countStatement.executeQuery();
			Map<?,?> parameter = (Map<?,?>)boundSql.getParameterObject();
			Page page = (Page)parameter.get("page");
			if(rs.next()) {
				page.setTotalNumber(rs.getInt(1));
			}
			// 改造后带分页查询的SQL语句
			String pageSql = sql + " limit " + page.getDbIndex() + "," + page.getDbNumber();
			//偷天换日
			metaObject.setValue("delegate.boundSql.sql", pageSql);
		}
		return invocation.proceed();
	}





至此,拦截器编写完毕,亲测有效!






最后别忘了在Configurtion.xml中注册拦截器


<plugins>
  	<plugin interceptor="com.csdn.interceptor.PageInterceptor"/>
  </plugins>