Mybatis中分页拦截器的实现说明

需要入手即用的案例源码,可跳过本章前面介绍部分,直接看中间部分的案例

首先我们了解一下,sqlsession中四个核心对象的关系:

        mybatis 拦截器默认可拦截的类型只有四种,即四种接口类型 Executor、StatementHandler、ParameterHandler 和 ResultSetHandler

 

 

四个对象之间的调用关系:

(1)Executor实现类中无论是doUpdate()还是doQuery()方法都会调用Configuration.newStatementHandler(),在其中通过RouteStatementHandler的构造方法,根据语句类型,委派到不同的语句处理器(SimpleStatementHandler、PreparedStatementHandler、CallableStatementHandler)

(2)再调用BaseStatementHandler里prepare()方法预编译SQL语句

(3)用parameterize()方法来使用ParameterHandler对象设置参数,完成预编译(如果执行的sql有参数的话,会用到该对象)

(4)执行查询的话,使用ResultHandler将结果返回给调用者,其他操作也类似。

可拦截的对象及方法:

 

 

mybatis 自定义拦截器,三步骤:

  1. 实现 intercepts接口
  2. 添加拦截注解 @Intercepts

         mybatis 拦截器默认可拦截的类型只有四种,即四种接口类型 Executor、StatementHandler、ParameterHandler 和 ResultSetHandler

对于我们的自定义拦截器必须使用 mybatis 提供的注解来指明我们要拦截的是四类中的哪一个类接口

具体规则如下:

a:Intercepts 标识我的类是一个拦截器

b:Signature 则是指明我们的拦截器需要拦截哪一个接口的哪一个方法

  •         type 对应四类接口中的某一个,比如是 Executor
  •         method 对应接口中的哪类方法,比如 Executor 的 update 方法
  •         args 对应接口中的哪一个方法,比如 Executor 中 query 因为重载原因,方法有多个,args 就是指明参数类型,从而确定是哪一个方法

    3.配置文件中添加拦截器

拦截器其实就是一个 plugin,在 mybatis 核心配置文件中我们需要配置我们的 plugin

(这里注意一下我们添加plugins标签的位置):

<plugins>
       <plugin interceptor="com.intercept.MyInterceptor">
	       <property name="username" value="Fitz"/>
	       <property name="password" value="123456"/>
   		</plugin>
</plugins>

如果与Spring整合后,则可在applicationContext.xml中的配置如下:

<bean id="myInterceptor" class="com.intercept.MyInterceptor">
    <property name="username" value="Fitz"/>
    <property name="password" value="123456"/>
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<!-- 数据源 -->
		<property name="dataSource" ref="dataSource"/>
		<!-- mybatis配置文件 -->
		<property name="configLocation" value="classpath:sqlMapcfg.xml"/>
    <property name="plugins">
        <ref bean="myInterceptor" />
    </property>
</bean>

拦截器顺序

1. 配置的拦截器拦截顺序:

     相对于同一个拦截对象(如:StatementHandler)而言,在 mybatis 核心配置文件根据配置的位置,拦截顺序是 从上往下进行拦截创建代理对象的,而执行的intercept()拦截方法则是逆序执行的(因为最后拦截的拦截器产生的代理对象,包裹了前面拦截器的代理对象),借用网上一哥们的图,如下:

2.同一个拦截器中不同拦截对象的拦截顺序:

     Executor -> ParameterHandler -> StatementHandler -> ResultSetHandler

首先我们需要了解拦截器中需要用到的各个对象的作用:

1.MappedStatement

        MappedStatement类在Mybatis框架中用于表示XML文件中一个sql语句节点,比如Mapper.xml中一个<select />节点:

<select id="selectAuthorLinkedHashMap" resultType="java.util.LinkedHashMap"> 
    select id, username from author where id = #{value} 
</select>

        Mybatis对Mapper.xml文件的配置读取和解析后,会根据mapper的id注册多个MappedStatement对象,分别对应其中id为“selectAuthorLinkedHashMap”和“xxx”等等的<select />节点,通过org.apache.ibatis.session.Configuration类中的getMappedStatement(String id)方法,可以检索到一个特定的MappedStatement。为了区分不同的Mapper文件中的sql节点,其中的String id方法参数,是以Mapper文件的namespace作为前缀,再加上该节点本身的id值。

该类属性信息如下:

public final class MappedStatement {
  private String resource;
  private Configuration configuration;
  private String id;
  private Integer fetchSize;
  private Integer timeout;
  private StatementType statementType;
  private ResultSetType resultSetType;
  private SqlSource sqlSource;
  private Cache cache;
  private ParameterMap parameterMap;
  private List<ResultMap> resultMaps;
  private boolean flushCacheRequired;
  private boolean useCache;
  private boolean resultOrdered;
  private SqlCommandType sqlCommandType;
  private KeyGenerator keyGenerator;
  private String[] keyProperties;
  private String[] keyColumns;
  private boolean hasNestedResultMaps;
  private String databaseId;
  private Log statementLog;
  private LanguageDriver lang;
    //省略各属性set/get方法
}

需要注意:

MappedStatement是一个共享的缓存对象,这个对象是存在并发问题的,所以几乎任何情况下都不能去修改这个对象(通用Mapper除外),想要对MappedStatement做修改该怎么办呢?

解决办法:Executor中的拦截器方法参数中都有MappedStatement ms,这个ms就是后续方法执行要真正用到的MappedStatement,这样一来,问题就容易解决了,根据自己的需要,深层复制MappedStatement对象中自己需要修改的属性,然后修改这部分属性,之后将修改后的ms通过上面代码中args[0]=ms这种方式替换原有的参数,这样就能实现对ms的修改而且不会有并发问题了。

 

2.SqlSource

SqlSource是一个接口类,在MappedStatement对象中是作为一个属性出现的。SqlSource接口只有一个getBoundSql(Object parameterObject)方法,返回一个BoundSql对象。一个BoundSql对象,代表了一次sql语句的实际执行,而SqlSource对象的责任,就是根据传入的参数对象,动态计算出这个BoundSql,也就是说Mapper文件中的<if />节点的计算,是由SqlSource对象完成的。负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回,如下是该对象中的属性信息,从中我们可以动态获取执行sql中的传入参数

 

3.BoundSql

表示动态生成的SQL语句以及相应的参数信息,当调用SqlSource的getBoundSql方法,传入的就是parameterMappings相对应的参数,最终生成BoundSql对象,有了BoundSql就可以执行sql语句了

 

案例(详见其中注释):

package com.po;

public class PagePOJO {
	private int totalNumber;//当前表中总条目数量  
    private int currentPage;//当前页的位置  
    private int totalPage;//总页数  
    private int pageSize;//页面大小  
    private int startIndex;//检索的起始位置  
    private int totalSelect;//检索的总数目  
    
      
    public PagePOJO(int totalNumber, int currentPage, int totalPage, int pageSize, int startIndex, int totalSelect) {  
        super();  
        this.totalNumber = totalNumber;  
        this.currentPage = currentPage;  
        this.totalPage = totalPage;  
        this.pageSize = pageSize;  
        this.startIndex = startIndex;  
        this.totalSelect = totalSelect;  
    }  
    //这里省略其他set、get方法
    //...
}  

 

package com.intercept;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Properties;

import org.apache.ibatis.executor.Executor;
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.mapping.MappedStatement.Builder;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlSource;
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.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import com.po.PagePOJO;

@Intercepts({
        @Signature(type=Executor.class,method="query",args={MappedStatement.class,Object.class,
				RowBounds.class,ResultHandler.class}),
        //无论是query还是update(增删改)动作,都会执行statementhandler子类对象中的prepare
		//,所以我们在statementhandler中拦截可以满足大部分需求,当然具体的拦截对象以及拦截方法根据具体需求而定
        @Signature(type=StatementHandler.class,method="prepare",args={Connection.class,Integer.class})
})
public class MyInterceptor implements Interceptor {
        /**
         *方法参数Invocation就是我们具体拦截到的对象(Executor、StatementHandler、ParameterHandler 和 ResultSetHandler)
         *Invocation 就是这个对象,Invocation 里面有三个参数 target method args
         *          target 就是 Executor
         *          method 就是 update
         *          args   就是 MappedStatement ms, Object parameter
         */
	    public Object intercept(Invocation invocation) throws Exception{
	    	//很多需求其实是可以通过不同的拦截对象实现的,为了使得说明更加全面,以下分别通过
			//Executor和StatementHandler对象实现拦截进行分页处理*/
                Invocation retInvocation=null;
			//通过拦截Executor实现
			//retInvocation=ByExecutor(invocation);
			
			//通过拦截StatementHandler实现
	    	retInvocation=ByStatementHandler(invocation);
	    	
	        return  retInvocation.proceed();
	    }
	     
        /* 
         * 用当前这个拦截器生成对目标target的代理,实际是通过Plugin.wrap(target,this) 
         *来完成的,把目标target和拦截器this传给了包装函数,并返回代理对象
         */
	    public Object plugin(Object target) { 
	    	//先判断一下目标类型,是本插件要拦截的对象才执行
	    	//Plugin.wrap方法,否则直接返回目标对象,这样可以减少目标被代理的次数。
	    	if (target instanceof StatementHandler) {  
	            return Plugin.wrap(target, this);  
	        } else {  
	            return target;  
	        }  
	    	
	    }
         
         /*如何使用?
         * 只需要在 mybatis 配置文件中加入类似如下配置
         *      <plugin interceptor="com.intercept.MyInterceptor">
         *           <property name="username" value="fitz_fu"/>
         *           <property name="password" value="123456"/>
         *      </plugin>
         *方法中获取参数:properties.getProperty("username");
         *若mybatis与spring一起用,则可以通过@Value("${}")从application.properties文件获取
         */
	    public void setProperties(Properties properties) {
	        String username = properties.getProperty("username");
	        String password = properties.getProperty("password");
	    }
        
        private Invocation ByExecutor(Invocation invocation) throws Exception {
			 //先拦截到RoutingStatementHandler,里面有个StatementHandler类型的delegate变量
	        //,其实现类是BaseStatementHandler,然后就到BaseStatementHandler的成员变量mappedStatement
	        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
	        Object parameter =invocation.getArgs()[1];
	        BoundSql boundSql=mappedStatement.getBoundSql(parameter);
	        //mapper.xml中sql的参数是何种类型,这个地方就用什么类型来接收参数,比如下面的String或者是Map
            //String params = (String)boundSql.getParameterObject();
            Map<String,Object> params = (Map<String,Object>)boundSql.getParameterObject();
            //在SQL的参数中获取分页对象
            PagePOJO page = (PagePOJO)params.get("page");  
	        String initialSql=boundSql.getSql();
	        //Object parameterObject=boundSql.getParameterObject();
	        //mapper.xml文件中sql语句的唯一id,可利用此id命名规则判断是否进行分页处理
	        String id = mappedStatement.getId().split("\\.")[3];
	        if(id.startsWith("search")) {//如果mapperid是search开头的,则是先分页逻辑
	        	//先统计原始sql查询的总条数,以此确定各个分页参数
	        	String countSql = "select count(*) from ( "+initialSql+" ) a";  
	            
				Connection connection = mappedStatement.getConfiguration().getEnvironment().getDataSource().getConnection(); 
				PreparedStatement countStatement = connection.prepareStatement(countSql);
				BoundSql countBs=copyAndNewBS(mappedStatement,boundSql,countSql);
				//当sql带有参数时,下面的这句话就是获取查询条件的参数 
				DefaultParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement,params,countBs);
				//经过set方法,就可以正确的执行sql语句  
				parameterHandler.setParameters(countStatement);  
				ResultSet rs = countStatement.executeQuery(); 
				//当结果集中有值时,表示页面数量大于等于1 
				if(rs.next()){  
					//根据业务需要对分页对象进行设置
				    page.setTotalNumber(rs.getInt(1));  
				}
				//注意:这里与StatementHandler拦截不同的是需要对连接进行关闭
				rs.close();
				countStatement.close();
				connection.close();
				//构造并执行分页sql
				//String pageSql = sql+" limit "+page.getStartIndex()+","+page.getTotalSelect(); //Mysql 
	            String pageSql = "SELECT * FROM ( SELECT ROWNUM ROWNUMS,TMP.* FROM ("+initialSql
	            		+" ) TMP ) WHERE ROWNUMS <= "+page.getTotalSelect()+" AND ROWNUMS >"+page.getStartIndex();  //Oracle
	            BoundSql newBs=copyAndNewBS(mappedStatement, boundSql, pageSql);
                 /* mappedStatement对象有可能产生并发问题,所以我们一般不直接对其进行属性修改
    			 * ,而是新建一个MS对象,并复制一个该对象相关属性给新对象,并按需修改器中相关属性,再将invocation中的arg0替换成新的对象*/
              //这个地方传入的SonOfSqlSource对象是自定义实现了SqlSource子类,实现类在下面作为内部类
	            MappedStatement newMs=copyAndNewMS(mappedStatement,new SonOfSqlSource(newBs));
	            
	            invocation.getArgs()[0]=newMs;
	        }
	        
			 return  invocation;
		}
		private Invocation ByStatementHandler(Invocation invocation) throws Exception{
			StatementHandler statementHandler = (StatementHandler)invocation.getTarget();
	    	//因为StatementHandler中部分方法为protected,所以通过MetaObject优雅访问对象的属性
	    	//,这里是访问statementHandler的属性
	        MetaObject metaObject = MetaObject.forObject(statementHandler
	        		, SystemMetaObject.DEFAULT_OBJECT_FACTORY
	        		, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY
	        		, new DefaultReflectorFactory());  
	       
	        if("prepare".equals(invocation.getMethod().getName())){  
	              
	            BoundSql boundSql = statementHandler.getBoundSql();
	            //mapper.xml中sql的参数是何种类型,这个地方就用什么类型来接收参数,比如下面的String或者是Map
	            //String params = (String)boundSql.getParameterObject();
	            Map<String,Object> params = (Map<String,Object>)boundSql.getParameterObject();
	            //在SQL的参数中获取分页对象
	            PagePOJO page = (PagePOJO)params.get("page");  
	            String initialSql = boundSql.getSql();//获取原始sql  
	            String countSql = "select count(*) from ( "+initialSql+" ) a";  
	            
				Connection connection = (Connection) invocation.getArgs()[0];  
				PreparedStatement countStatement = connection.prepareStatement(countSql);
				//当sql带有参数时,下面的这句话就是获取查询条件的参数 
				ParameterHandler parameterHandler = (ParameterHandler) metaObject.getValue("delegate.parameterHandler");
				//经过set方法,就可以正确的执行sql语句  
				parameterHandler.setParameters(countStatement);  
				ResultSet rs = countStatement.executeQuery();  
				//当结果集中有值时,表示页面数量大于等于1 
				if(rs.next()){
					//根据业务需要对分页对象进行设置
				    page.setTotalNumber(rs.getInt(1));  
				}
				//构造并执行分页sql
				//String pageSql = sql+" limit "+page.getStartIndex()+","+page.getTotalSelect(); //Mysql 
	            String pageSql = "SELECT * FROM ( SELECT ROWNUM ROWNUMS,TMP.* FROM ("+initialSql
	            		+" ) TMP ) WHERE ROWNUMS <= "+page.getTotalSelect()+" AND ROWNUMS >"+page.getStartIndex();  //Oracle
	            metaObject.setValue("delegate.boundSql.sql", pageSql);  
	        }  
			return  invocation;
		}
		/**
		 *构建一个新的BoundSql
		 */
		private BoundSql copyAndNewBS(MappedStatement mappedStatement, BoundSql boundSql, String countSql) {
			//根据新的sql构建一个全新的boundsql对象,并将原来的boundsql中的各属性复制过来
			BoundSql newBs=new BoundSql(mappedStatement.getConfiguration(),countSql
					,boundSql.getParameterMappings(),boundSql.getParameterObject());
			for(ParameterMapping mapping:boundSql.getParameterMappings()) {
				String prop=mapping.getProperty();
				if(boundSql.hasAdditionalParameter(prop)) {
					newBs.setAdditionalParameter(prop, boundSql.getAdditionalParameter(prop));
				}
			}
			return newBs;
		}

		/**
		 *复制一个新的MappedStatement
		 */
	    private MappedStatement copyAndNewMS(MappedStatement ms,SqlSource ss) {
	    	//通过builder对象重新构建一个MappedStatement对象
	    	Builder builder =new Builder(ms.getConfiguration(),ms.getId(),ss,ms.getSqlCommandType());
	    	builder.resource(ms.getResource());
	    	builder.fetchSize(ms.getFetchSize());
	    	builder.statementType(ms.getStatementType());
	    	builder.keyGenerator(ms.getKeyGenerator());
	    	builder.timeout(ms.getTimeout());
	    	builder.parameterMap(ms.getParameterMap());
	    	builder.resultMaps(ms.getResultMaps());
	    	builder.resultSetType(ms.getResultSetType());
	    	builder.cache(ms.getCache());
	    	builder.flushCacheRequired(ms.isFlushCacheRequired());
	    	builder.useCache(ms.isUseCache());
			return builder.build();
			
		}
	    class SonOfSqlSource implements SqlSource {
	    	private BoundSql boundSql;
	    	
	    	public SonOfSqlSource(BoundSql boundSql) {
	    		this.boundSql=boundSql;
	    	}
			public BoundSql getBoundSql(Object arg0) {
				// TODO Auto-generated method stub
				return boundSql;
			}
	    	
	    }

}

 

说明:Plugin的wrap(),它根据当前的Interceptor上面的注解定义哪些接口需要拦截,然后判断当前目标对象是否有实现对应需要拦截的接口,如果没有则返回目标对象本身,如果有则返回一个代理对象。而这个代理对象的InvocationHandler正是一个Plugin。所以当目标对象在执行接口方法时,如果是通过代理对象执行的,则会调用对应InvocationHandler的invoke方法,也就是Plugin的invoke方法。所以接着我们来看一下该invoke方法的内容。这里invoke方法的逻辑是:如果当前执行的方法是定义好的需要拦截的方法,则把目标对象、要执行的方法以及方法参数封装成一个Invocation对象,再把封装好的Invocation作为参数传递给当前拦截器的intercept方法。如果不需要拦截,则直接调用当前的方法。Invocation中定义了定义了一个proceed(),其逻辑就是调用当前方法,所以如果在intercept中需要继续调用当前方法的话可以调用invocation的procced方法。

如下为执行一条查询语句,进入拦截器执行了拦截方法后,控制台的打印语句绿色字体为添加的注释,其余的都是控制台打印语句):

#创建会话

Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6c1f45b4] was not registered for synchronization because synchronization is not active
JDBC Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@24151d39] will not be managed by Spring

#执行查询记录统计SQL
==>  Preparing: select count(*) from ( select * from users where ?=? ) a 
==> Parameters: 00(String), 00(String)
<==    Columns: COUNT(*)
<==        Row: 13

#执行我们业务逻辑中的sql,并实现分页的SQL
==>  Preparing: SELECT * FROM ( SELECT ROWNUM ROWNUMS,TMP.* FROM (select * from users where ?=? ) TMP ) WHERE ROWNUMS <= 10 AND ROWNUMS >2 
==> Parameters: 00(String), 00(String)
<==    Columns: ROWNUMS, KEYID, NAME, GENDER
<==        Row: 3, 3, ‘Fitz’, male
<==        Row: 4, 4, ‘Fitz’, male
<==        Row: 5, 5, ‘Fitz’, male
<==        Row: 6, 6, ‘Fitz’, male
<==        Row: 7, 7, ‘Fitz’, male
<==        Row: 8, 8, ‘Fitz’, male
<==        Row: 9, 9, ‘Fitz’, male
<==        Row: 10, 10, ‘Fitz’, male
<==      Total: 8

#关闭会话
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6c1f45b4]

说明:由上面的控制台打印可知,本次拦截器中执行的多次查询为同一次会话同一次连接中所做的动作,因此对数据库的多次操作,对我们的代码性能并不会影响多少 

总结

我们假设在MyBatis配置了一个插件,在运行时会发生什么?

1)       所有可能被拦截的处理类都会生成一个代理

2)       处理类代理在执行对应方法时,判断要不要执行插件中的拦截方法

3)       执行插件中的拦截方法后,推进目标的执行

如果有N个插件,就有N个代理,每个代理都要执行上面的逻辑。这里面的层层代理要多次生成动态代理,是比较影响性能的。虽然能指定插件拦截的位置,但这个是在执行方法时动态判断,初始化的时候就是简单的把插件包装到了所有可以拦截的地方。

因此,在编写插件时需注意:

基于第1、2点实现plugin方法时可先判断一下目标类型,是本插件要拦截的对象才执行Plugin.wrap方法,否则直接返回目标对象,这样可以减少目标被代理的次数。

 

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值