Mybatis源码学习(19)-Mybatis拦截器实现分页插件

导读:本篇文章讲解 Mybatis源码学习(19)-Mybatis拦截器实现分页插件,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

一、概述

  本文主要是通过Mybatis拦截器,实现一个简易的分页插件。本实例旨在学习Mybatis拦截器的用法,所以在实现分页插件的过程中,更重视的是对拦截器使用方法的分析,而非实现的分页插件本身。

二、环境
  • Windows开发环境
  • Eclipse开发工具
  • MySQL数据库
三、分页插件代码

分页插件实现的源码下载 https://gitee.com/hsh2015/mybatis_source_learning

  1. 拦截器类 PaginationInterceptor
package com.hsh.test.modules.interceptor;

import java.lang.reflect.Field;
import java.sql.Connection;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.collections.MapUtils;
import org.apache.ibatis.executor.statement.RoutingStatementHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
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;

@Intercepts({
	@Signature(type = StatementHandler.class, 
			method = "prepare", 
			args = {Connection.class,Integer.class}
	)
})
public class PaginationInterceptor implements Interceptor {
	
	private int DEFAULT_PAGE_SIZE = 10;//默认一页10条数据
	private int DEFAULT_PAGE_NUM = 1;//默认第一页
	
	private String page = "page"; //当前页面参数对应的key
    private String rows = "rows"; //pageSize对应的key
    
    public static final String COUNT_SQL_REG = "^SELECTCOUNT\\(";
    public static final String BLANK_REG = "\\s*|\\t|\\r|\\n|\\\\n";
    private static Pattern pattern = Pattern.compile(COUNT_SQL_REG, Pattern.CASE_INSENSITIVE);
    
	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		if (!(invocation.getTarget() instanceof RoutingStatementHandler)) {
            return invocation.proceed();
        }
		RoutingStatementHandler handler = (RoutingStatementHandler) invocation.getTarget();
        BoundSql boundSql = handler.getBoundSql();
        Object param = boundSql.getParameterObject();
        if (param == null) {//无参数时不做分页处理
        	return invocation.proceed();  
        }
        Map<String, Object> pagination = this.parsePaginationParam(param);
        if(pagination == null || pagination.isEmpty()) {
        	return invocation.proceed();
        }else {
        	return this.processPagination(invocation, boundSql, pagination);
        }
	}
	/**
	 * 处理分页
	 * @param invocation
	 * @param boundSql
	 * @param param
	 * @return
	 * @throws Exception
	 */
	private Object processPagination(Invocation invocation, BoundSql boundSql, Map<String, Object> param) throws Exception {
        String paginationSql = this.buildPaginationSql(boundSql.getSql().trim(),param);
        try {
            Field field = boundSql.getClass().getDeclaredField("sql");
            field.setAccessible(true);
            field.set(boundSql, paginationSql);
            Object result = invocation.proceed();
            return result;
        }catch (Exception e) {
            throw new Exception("myBatis执行SQL失败!(" + paginationSql + ")", e);
        }
    }
	/**
     * 构建分页查询SQL
     *
     * @param sql
     * @param param
     * @return
     */
    private String buildPaginationSql(String sql, Map<String, Object> param) {
    	Integer start = MapUtils.getInteger(param, "page");
    	Integer limit = MapUtils.getInteger(param, "rows");
        String checkSql = sql.replaceAll(BLANK_REG, "");
        Matcher matcher = pattern.matcher(checkSql);
        if(matcher.find()){//排除sql count查询
            return sql;
        }else {
        	String paginationSql = sql;
        	if(limit != null && start != null && start > 0 && limit > 0){
        		start = (start-1) * limit;
        		paginationSql = paginationSql + " limit " + start.toString() + "," + limit.toString();
        	}
            return paginationSql;
        }
    }
	/**
     * 处理分页参数page、rows
     * @param param
     * @return
     */
    private Map<String, Object> parsePaginationParam(Object param) {
    	Map<String, Object> pagination = null;
    	if(param instanceof Map) {//Map类型的参数
    		Map<String, Object> paramMap = (Map<String, Object>) param;
    		if(paramMap.containsKey(this.page) && paramMap.containsKey(this.rows)) {
    			pagination = new HashMap<String, Object>();
        		pagination.put(this.page, MapUtils.getInteger(paramMap, this.page, this.DEFAULT_PAGE_NUM));
                pagination.put(this.rows, MapUtils.getInteger(paramMap, this.rows, this.DEFAULT_PAGE_SIZE));
    		}
    		return pagination;
    	}else {//非Map类型的参数,作为PO对象处理
    		Class clazz = param.getClass();
    		try {
    			if(clazz != Object.class) {
    				 pagination = new HashMap<String, Object>();
    				 Field startField = clazz.getDeclaredField(this.page);
                     startField.setAccessible(true);
                     Field limitField = clazz.getDeclaredField(this.rows);
                     limitField.setAccessible(true);
                     pagination.put(this.page, (Integer) startField.get(param));
                     pagination.put(this.rows, (Integer) limitField.get(param));
    			}
    		}catch (Exception e) {
    			pagination = null;
			}
    		return pagination;
    	}
    }
    @Override
	public Object plugin(Object target) {
		if (target instanceof StatementHandler) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
	}
	@Override
	public void setProperties(Properties properties) {
		 this.page = properties.getProperty("page", "page");
	     this.rows = properties.getProperty("rows", "rows");		
	}

}

  1. 修改Config配置文件
    在mybatis-config.xml文件中,添加插件的配置信息,代码如下:
<plugins>
	<plugin interceptor="com.hsh.test.modules.interceptor.PaginationInterceptor">
		<property name="page" value="page" />
		<property name="rows" value="rows" />
	</plugin>
</plugins>
  1. Mapper映射文件
    这里演示一个普通的查询操作,对应的Mapper配置。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 
	"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hsh.test.modules.mapper.api.TestMapper">
	
	<select id="queryList" resultType="map">
        SELECT * FROM test 
        <!-- WHERE field1 = #{field1} -->
    </select>
    
</mapper>
  1. 数据库表及测试数据
CREATE TABLE `test` (
`id`  varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL ,
`name`  varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL ,
`field1`  varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL ,
PRIMARY KEY (`id`)
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8 COLLATE=utf8_general_ci
ROW_FORMAT=DYNAMIC
;

INSERT INTO `test` VALUES ('1', '1', 'a1');
INSERT INTO `test` VALUES ('2', '2', 'a2');
INSERT INTO `test` VALUES ('3', '3', '3');
INSERT INTO `test` VALUES ('4', '4', '4');
INSERT INTO `test` VALUES ('5', '5', '5');
INSERT INTO `test` VALUES ('6', '6', '6');
INSERT INTO `test` VALUES ('7', '7', '7');
INSERT INTO `test` VALUES ('8', '8', '8');
INSERT INTO `test` VALUES ('9', '9', '9');
  1. 测试方法
@Test
public void testMapper() {
	SqlSession sqlSession = mySqlSessionFactory.openSession(true);//自动提交
	TestMapper testMapper = sqlSession.getMapper(TestMapper.class);
	Map<String, Object> param = new HashMap<String, Object>();
	//param.put("field1", "a1");
	param.put("page", 2);
	param.put("rows", 5);
	List<Map<String, Object>> list = testMapper.queryList(param);
	System.out.println(list.toString());
}
四、Mybatis中Plugin插件工作流程分析
1、Mybatis-config.xml文件中关于插件配置的加载

《Mybatis启动时的初始化过程》中,我们知道在Mybatis启动的过程中,XMLConfigBuilder类的parseConfiguration(XNode root)方法是Mybatis-config.xml配置文件的解析入口,其中pluginElement(root.evalNode(“plugins”));是用来解析Plugin插件的,具体实现如下:

//XMLConfigBuilder.java
/**
   * 解析配置文件中的插件(plugin),并注册到interceptor中
   * @param parent
   * @throws Exception
   */
  private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        String interceptor = child.getStringAttribute("interceptor");
        Properties properties = child.getChildrenAsProperties();
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
        interceptorInstance.setProperties(properties);
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }
//Configuration.java
public void addInterceptor(Interceptor interceptor) {
    interceptorChain.addInterceptor(interceptor);
}

通过上面的代码,我们可以知道,针对plugin的初始化工作,主要是:1、解析配置文件中定义的插件实现类,比如上面实例中的PaginationInterceptor;2、然后把解析得到的类通过Configuration.addInterceptor()方法添加到拦截器链中,即添加到Configuration的InterceptorChain变量中,在InterceptorChain实例中其实就是一个List<Interceptor>对象在存储所有的拦截器。

2、执行查询操作时–根据目标对象生成代理对象

这里以上面的测试方法为例,跟踪整个流程的运行过程。首先是在执行查询时,到了SimpleExecutor的doQuery()方法。代码如下:

//SimpleExecutor.java
 /**
   * 查询操作(返回类型List<E>),真正操作由StatementHandler接口的实现类完成
   */
  @Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.<E>query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }
//Configuration.java
 public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
  }

在上面的代码中,是真正实现数据库查询的方法,其中configuration.newStatementHandler()方法用来生成代理对象StatementHandler实例,即通过拦截器加强StatementHandler对象。实际上就是通过interceptorChain.pluginAll()方法来实现,具体过程如下:

首先,通过调用interceptorChain.pluginAll()方法。在该方法中,interceptors变量是在第一步初始化的时候,已经完成初始化的参数,即Configuration.addInterceptor()方法中进行了初始化。然后循环调用Interceptor接口的plugin()方法实现目标对象的动态代理对象生成,即在原有目标对象上添加了拦截器功能,然后生成目标的代理对象。

//InterceptorChain.java
public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }

在上面代码中,实际上就是执行了PaginationInterceptor的plugin()方法,即如下面代码所示,这里是通过Plugin.wrap()方法实现,在《Mybatis的Plugin模块基础学习》中已经分析了该方法,这里不再重复介绍。

//PaginationInterceptor.java
@Override
public Object plugin(Object target) {
	if (target instanceof StatementHandler) {
           return Plugin.wrap(target, this);
       } else {
           return target;
       }
}
3、执行查询操作时–根据生成的代理对象执行查询

在上述doQuery()方法中,通过configuration.newStatementHandler()方法实现了目标对象生成动态代理对象的逻辑。

首先,目标对象生成的动态代理对象如下所示:
在这里插入图片描述

然后,上述实例中,我们拦截器注解配置如下:

@Intercepts({
	@Signature(type = StatementHandler.class, 
			method = "prepare", 
			args = {Connection.class,Integer.class}
	)
})

根据上述注解的配置,我们可以确定,我们拦截的方法是StatementHandler类中的prepare方法。即在上述doQuery()方法中,执行prepareStatement()方法时,进行了拦截,并进行了功能增强。代码如下:

//SimpleExecutor.java
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    stmt = handler.prepare(connection, transaction.getTimeout());
    handler.parameterize(stmt);
    return stmt;
  }

根据注解可以发现,其实在调用handler.prepare()方法的时候,被定义的拦截器进行了拦截,即在执行该方法的时候,实际上是执行了Plugin类的invoke()方法,代码如下:

//Plugin.java
 @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);
    }
  }

如果符合条件,就会执行interceptor.intercept(new Invocation(target, method, args));代码,这里实际上就是执行了下面代码:

//PaginationInterceptor.java
@Override
	public Object intercept(Invocation invocation) throws Throwable {
		if (!(invocation.getTarget() instanceof RoutingStatementHandler)) {
            return invocation.proceed();
        }
		RoutingStatementHandler handler = (RoutingStatementHandler) invocation.getTarget();
        BoundSql boundSql = handler.getBoundSql();
        Object param = boundSql.getParameterObject();
        if (param == null) {//无参数时不做分页处理
        	return invocation.proceed();  
        }
        Map<String, Object> pagination = this.parsePaginationParam(param);
        if(pagination == null || pagination.isEmpty()) {
        	return invocation.proceed();
        }else {
        	return this.processPagination(invocation, boundSql, pagination);
        }
	}
//PaginationInterceptor.java
private Object processPagination(Invocation invocation, BoundSql boundSql, Map<String, Object> param) throws Exception {
        String paginationSql = this.buildPaginationSql(boundSql.getSql().trim(),param);
        try {
            Field field = boundSql.getClass().getDeclaredField("sql");
            field.setAccessible(true);
            field.set(boundSql, paginationSql);
            Object result = invocation.proceed();
            return result;
        }catch (Exception e) {
            throw new Exception("myBatis执行SQL失败!(" + paginationSql + ")", e);
        }
    }

上述代码主要是添加了拦截器需要增强的功能的代码逻辑,执行完成后,然后在通过Object result = invocation.proceed();方法来实现被代理对象需要执行的方法。代码如下:

//Invocation.java
public Object proceed() throws InvocationTargetException, IllegalAccessException {
    return method.invoke(target, args);
  }
4、总结

在上述过程执行完成后,其实就是一个拦截器的从初始化,到拦截对应的方法,然后应用的全过程。如果存在多个拦截器,本质上没有变化,只是在对应的时间节点进行拦截即可。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/68888.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!