简介

  拦截器的一个作用就是拦截某些方法的调用,可以选择在这些被拦截的方法执行前后加上某些逻辑,也可以在执行这些被拦截的方法时,执行自己的逻辑而不再执行被拦截的方法。

  Mybatis拦截器设计的一个初衷就是为了供用户在某些时候,可以实现自己的逻辑而不必去动Mybatis固有的逻辑。

Interceptor接口

       对于拦截器,Mybatis提供一个Interceptor接口,通过实现该接口就可以定义自己的拦截器。看一下这个接口的定义:

package org.apache.ibatis.plugin;
import java.util.Properties;
 
public interface Interceptor {
 
  Object intercept(Invocation invocation) throws Throwable;
  Object plugin(Object target);
  void setProperties(Properties properties);
 
}

   该接口中一共定义有三个方法,interceptpluginsetPropertiesplugin方法是拦截器用于封装目标对象的,通过该方法可以返回目标对象本身,也可以返回一个它的代理。当返回的是代理的时候,可以对其中的方法进行拦截来调用intercept方法setProperties方法是用于在Mybatis配置文件中指定一些属性的

       自定义的Interceptor最重要的是实现plugin方法intercept方法,在plugin方法中可以决定是否要进行拦截进而决定要返回一个什么样的目标对象。而intercept方法,就是要进行拦截的时候要执行的方法

       对于plugin方法而言,其实Mybatis已经提供一个实现。Mybatis中有一个叫做Plugin的类,里面有一个静态方法wrap(Object target,Interceptor interceptor),通过该方法可以决定要返回的对象是目标对象还是对应的代理。先来看一下Plugin的源码:

package org.apache.ibatis.plugin;
 
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
 
import org.apache.ibatis.reflection.ExceptionUtil;
 
public class Plugin implements InvocationHandler {
 
  private Object target;
  private Interceptor interceptor;
  private Map<Class<?>, Set<Method>> signatureMap;
 
  private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
    this.target = target;
    this.interceptor = interceptor;
    this.signatureMap = signatureMap;
  }
 
  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;
  }
 
  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);
    }
  }
 
  private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
    if (interceptsAnnotation == null) { // issue #251
      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;
  }
 
  private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
    Set<Class<?>> interfaces = new HashSet<Class<?>>();
    while (type != null) {
      for (Class<?> c : type.getInterfaces()) {
        if (signatureMap.containsKey(c)) {
          interfaces.add(c);
        }
      }
      type = type.getSuperclass();
    }
    return interfaces.toArray(new Class<?>[interfaces.size()]);
  }
 
}

  Plugin的wrap方法,根据当前的Interceptor上面的注解定义哪些接口需要拦截,然后判断当前目标对象是否有实现对应需要拦截的接口如果没有则返回目标对象本身,如果有则返回一个代理对象。而这个代理对象的InvocationHandler正是一个Plugin。所以当目标对象在执行接口方法时,如果是通过代理对象执行的,则会调用对应InvocationHandler的invoke方法,也就是Plugin的invoke方法

  invoke方法的逻辑是:如果当前执行的方法是定义好的需要拦截的方法,则把目标对象、要执行的方法以及方法参数封装成一个Invocation对象,再把封装好的Invocation作为参数,传递给当前拦截器的intercept方法。如果不需要拦截,则直接调用当前的方法。Invocation定义一个proceed方法,其逻辑就是调用当前方法,如果在intercept中需要继续调用当前方法的话可以调用invocation的procced方法。

  getSignatureMap方法解释:会拿到拦截器这个类的 @Interceptors注解,然后拿到这个注解的属性 @Signature注解集合,然后遍历这个集合,遍历的时候拿出 @Signature注解的type属性(Class类型),然后根据这个type得到带有method属性和args属性的Method。由于 @Interceptors注解的 @Signature属性是一个属性,所以最终会返回一个以type为key,value为Set<Method>的Map。

       对于实现自己的Interceptor有两个很重要的注解,一个是@Intercepts,其值是一个@Signature数组。@Intercepts用于表明当前的对象是一个Interceptor,而@Signature则表明要拦截的接口、方法以及对应的参数类型。自定义的简单Interceptor:

  MyBatis 允许在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

  1. Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  2. ParameterHandler (getParameterObject, setParameters)
  3. ResultSetHandler (handleResultSets, handleOutputParameters)
  4. StatementHandler (prepare, parameterize, batch, update, query)

  总体概括为:拦截执行器的方法、拦截参数的处理、拦截结果集的处理、拦截Sql语法构建的处理

案例:拦截器分页处理

        利用JDBC对数据库进行操作,必须要有一个对应的Statement对象,Mybatis在执行Sql语句前,会产生一个包含Sql语句的Statement对象,而且对应的Sql语句是在Statement之前产生的,所以就可以在它成Statement之前,对用来生成Statement的Sql语句进行更改。在Mybatis中Statement语句是通过RoutingStatementHandler对象的prepare方法生成的。利用拦截器实现Mybatis分页的一个思路:就是拦截StatementHandler接口的prepare方法,然后在拦截器方法中,把Sql语句改成对应的分页查询Sql语句,之后再调用StatementHandler对象的prepare方法,即调用invocation.proceed()。

  由于包括sql等其他属性在内的多个属性没有对应的方法可以直接取到,它们对外部都是封闭的,是对象的私有属性,所以就需要引入反射机制来获取或者更改对象的私有属性的值。对于分页而言,在拦截器里面常常还需要做的一个操作就是统计满足当前条件的记录一共有多少,这是通过获取到原始的Sql语句后,把它改为对应的统计语句,再利用Mybatis封装好的参数和设置参数的功能把Sql语句中的参数进行替换,之后再执行查询记录数的Sql语句进行总记录数的统计。

  分页操作封装的一个实体类Page:

package mybatis.domain;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/***
 * 分页数据的封装
 * */
public class Page<T> {
   private int  pageNo=1;     //页码
   private int pageSize=15;   //页面的记录数
   private int totalRecords;  //所有的记录数
   private int totalPages;    //总页数
   private List<T> result;    //对应的当前页记录
   private Map<String,Object> map=new HashMap<>(); //其他参数封装为map对象
}

pageInterceptpr拦截器

  @Intercepts其值是一个@Signature数组。@Intercepts用于表明当前的对象是一个Interceptor,而@Signature则表明要拦截的接口、方法以及对应的参数类型。

利用拦截器实现Mybatis分页的原理: 

       要利用JDBC对数据库进行操作就必须要有一个对应的Statement对象,Mybatis在执行Sql语句前就会产生一个包含Sql语句的Statement对象,而且对应的Sql语句 ,在Statement之前产生,所以可以在它生成Statement之前,对用来生成Statement的Sql语句进行更改。在Mybatis中Statement语句是通过RoutingStatementHandler对象的prepare方法生成的。所以利用拦截器实现Mybatis分页的一个思路就是:拦截StatementHandler接口的prepare方法,然后在拦截器方法中把Sql语句改成对应的分页查询Sql语句,之后再调用StatementHandler对象的prepare方法,即invocation.proceed()。 

      对于分页而言,在拦截器里面还需要做的一个操作就是统计满足当前条件的记录一共有多少,这是通过获取到原始的Sql语句后,把它改为对应的统计语句,再利用Mybatis封装好的参数和设 

置参数的功能把Sql语句中的参数进行替换,之后再执行查询记录数的Sql语句进行总记录数的统计。

  对于StatementHandler其实只有两个实现类,一个是RoutingStatementHandler,另一个是抽象类BaseStatementHandler,BaseStatementHandler有三个子类,分别是SimpleStatementHandlerPreparedStatementHandlerCallableStatementHandler,SimpleStatementHandler是用于处理Statement的,PreparedStatementHandler是处理PreparedStatement的,而CallableStatementHandler是处理CallableStatement的。

  Mybatis在进行Sql语句处理的时候,是建立的RoutingStatementHandler,而在RoutingStatementHandler里面拥有一个StatementHandler类型的delegate属性,RoutingStatementHandler会依据Statement的不同建立对应的BaseStatementHandler,即SimpleStatementHandler、PreparedStatementHandler或CallableStatementHandler,在RoutingStatementHandler里面所有StatementHandler接口方法的实现都是调用的delegate对应的方法。在PageInterceptor类上已经用@Signature标记该Interceptor只拦截StatementHandler接口的prepare方法,又因为Mybatis只有在建立RoutingStatementHandler的时候,是通过Interceptor的plugin方法进行包裹的,所以拦截到的目标对象肯定是RoutingStatementHandler对象。  

package mybatis.interceptor;

import mybatis.domain.Page;
import mybatis.util.ReflectUtil;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.executor.statement.RoutingStatementHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

@Intercepts({@Signature(method = "prepare",type= StatementHandler.class,args = {Connection.class})})
public class PageInterceptor implements Interceptor{

    private String databaseType;//数据库类型,不同数据库类型不同的分页方法
    private static Logger logger= LoggerFactory.getLogger(PageInterceptor.class);

    /**
     * 拦截之后调用的方法
     * **/
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        RoutingStatementHandler handler = (RoutingStatementHandler)invocation.getTarget();
        //通过反射获取RoutingStatementHandler的delegate属性
        StatementHandler delegate = (StatementHandler) ReflectUtil.getFieldValue(handler, "delegate");
        //获取statementhandler的boundSql,handler.gerBoundSql()与delete.getBoundSql()结果一样的
        //RoutingStatement的实现的所有StatementHandler接口都是调用delegate对应的方法
        BoundSql boundSql = delegate.getBoundSql();
        //拿到当前绑定sql的参数对象,就是在调用对应的mapper映射语句时所传的参数
        Object obj = boundSql.getParameterObject();
        //传入的是page对象就认定需要进行分页
        if(obj instanceof Page<?>){
            Page<?> page=(Page<?>)obj;
            //通过反射获取delegate父类的BaseStatementHandler的mappedStatement属性
            MappedStatement mappedStatement = (MappedStatement) ReflectUtil.getFieldValue(delegate, "mappedStatement");
            //拦截prepare方法参数Connection
            Connection conn=(Connection)invocation.getArgs()[0];
            //获取当前需要执行的sql,就是mapper映射中写的sql
            String sql = boundSql.getSql();
            //给当前的page参数对象设置总记录数
            setTotalRecord(page,mappedStatement,conn);
            //获取分页语句
            String pageSql = getPageSql(page, sql);
            ReflectUtil.setField(boundSql,"sql",pageSql);
        }
        return invocation.proceed();
    }

    /**
     * 根据page对象获取对应的分页查询sql语句
     * @param page 分页对象
     * @Param sql 原sql **/
    private String getPageSql(Page<?> page,String sql){
        StringBuffer sqlBuffer = new StringBuffer(sql);
        if("mysql".equalsIgnoreCase(databaseType))
            return getMysqlPageSql(page,sqlBuffer);

            return sqlBuffer.toString();
    }

    /**
     * 获取数据库的分页查询
     * @param page 分页对象
     * @param sqlBuffer 包含原sql的stringbuffer
     * **/
    private String getMysqlPageSql(Page<?> page,StringBuffer sqlBuffer){
        int offset=(page.getPageNo()-1)*page.getPageSize();
        sqlBuffer.append(" limit ").append(offset).append(",").append(page.getPageSize());
        return sqlBuffer.toString();
    }


    /**
     * 给当前的对象page设置总记录数
     * @Param page Mapper映射语句对应的参数
     * @Param mappedStatement Mapper映射语句
     * @param conn 当前的数据连接**/
    public void setTotalRecord(Page page,MappedStatement mappedStatement,Connection conn){
        //获取对应的boundSql,delegate是利用StatementHandler获取的BoundSql
        BoundSql boundSql = mappedStatement.getBoundSql(page);
        //获取在mapper映射中对应的sql
        String sql = boundSql.getSql();
        //根据查询sql获取对应的计算总记录数的sql语句
        String countSql = getCountSql(sql);
        //通过BoundSql获取对应的参数映射
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        //利用Configuration、查询记录数的sql语句、参数映射关系parameterMapping和参数page对象
        //建立查询记录数对应的BoundSql对象
        BoundSql countBoundSql = new BoundSql(mappedStatement.getConfiguration(), countSql, parameterMappings, page);
        //通过mappedStatement、参数page和boundSQL对象建立设定参数的ParameterHandler
        DefaultParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, page, countBoundSql);
        PreparedStatement pstmt=null;
        ResultSet rs=null;
        try{
            pstmt = conn.prepareStatement(countSql);
            parameterHandler.setParameters(pstmt);
            rs=pstmt.executeQuery();
            if(rs.next()){
                int totalRecord = rs.getInt(1);
                page.setTotalRecords(totalRecord);
            }
        }catch (SQLException e){
            logger.error("query exception");
        }finally {
            try{
                if(rs !=null)
                    rs.close();
                if(pstmt!=null)
                    pstmt.close();
            }catch (SQLException e){
                System.out.println("close resource exception!");
            }
        }
    }

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

    @Override
    public void setProperties(Properties properties) {
        this.databaseType=properties.getProperty("databaseType");
    }

    /**
     * 根据原sql获取对应的查询总记录数
     * **/
    private String getCountSql(String sql){
        return "select count(*) from ( "+sql+" )";
    }
}

反射工具类

package mybatis.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Field;

public class ReflectUtil {
    private static Logger logger= LoggerFactory.getLogger(ReflectUtil.class);

    public static Object getFieldValue(Object obj,String fieldName){
        Object result=null;
        Field field = getField(obj, fieldName);
        field.setAccessible(true);
        try {
            result=field.get(obj);
        } catch (IllegalAccessException e) {
            logger.info("permission denied");
        }

        return result;
    }

    private static Field getField(Object obj,String fieldName)  {
        Field field=null;
        for(Class<?> clazz=obj.getClass();clazz!=Object.class;clazz=clazz.getSuperclass()){
            try {
                field = clazz.getDeclaredField(fieldName);
            }catch (NoSuchFieldException e){
                logger.error("No Such Field");
            }
        }
        return field;
    }

    public static void setField(Object obj,String fieldName,String fieldValue){
        Field field = getField(obj, fieldName);
        if(field!=null){
            field.setAccessible(true);
            try {
                field.set(obj,fieldValue);
            } catch (IllegalAccessException e) {
                logger.info("permission denied!");
            }
        }
    }
}

注册拦截器

  注册拦截器是通过在Mybatis配置文件中,plugins元素下的plugin元素来进行的。一个plugin对应着一个拦截器,在plugin元素下面,可以指定若干个property子元素。Mybatis在注册定义的拦截器时,会先把对应拦截器下面的所有property通过Interceptor的setProperties方法注入给对应的拦截器。

<plugins>
        <plugin interceptor="mybatis.interceptor.PageInterceptor">
            <property name="databaseType" value="mysql"/>
        </plugin>
    </plugins>

  XMLConfigBuilder解析MyBatis全局配置文件的pluginElement私有方法:

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

  主要就是通过反射实例化plugin节点中的interceptor属性表示的类。然后调用全局配置类Configuration的addInterceptor方法。

public void addInterceptor(Interceptor interceptor) {
       interceptorChain.addInterceptor(interceptor);
     }

  interceptorChain是Configuration的内部属性,类型为InterceptorChain,也就是一个拦截器链,我们来看下它的定义:

public class InterceptorChain {

  private final List<Interceptor> interceptors = new ArrayList<Interceptor>();

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

  public void addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
  }

  public List<Interceptor> getInterceptors() {
    return Collections.unmodifiableList(interceptors);
  }

}

  回过头看下为何拦截器会拦截这些方法(Executor,ParameterHandler,ResultSetHandler,StatementHandler的部分方法):

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
    return parameterHandler;
}

public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
  ResultHandler resultHandler, BoundSql boundSql) {
    ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
    resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
    return resultSetHandler;
}

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

public Executor newExecutor(Transaction transaction, ExecutorType executorType, boolean autoCommit) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
      executor = new CachingExecutor(executor, autoCommit);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

  以上4个方法都是Configuration的方法。这些方法在MyBatis的一个操作(新增,删除,修改,查询)中都会被执行到,执行的先后顺序是Executor,ParameterHandler,ResultSetHandler,StatementHandler(其中ParameterHandler和ResultSetHandler的创建是在创建StatementHandler[3个可用的实现类CallableStatementHandler,PreparedStatementHandler,SimpleStatementHandler]的时候,其构造函数调用的[这3个实现类的构造函数其实都调用了父类BaseStatementHandler的构造函数])。

  这4个方法实例化了对应的对象之后,都会调用interceptorChain的pluginAll方法,InterceptorChain的pluginAll刚才已经介绍过了,就是遍历所有的拦截器,然后调用各个拦截器的plugin方法。注意:拦截器的plugin方法的返回值会直接被赋值给原先的对象

usermapper,xml中的配置

<select id="findPage" resultMap="userResult" parameterType="page">
        select * from user
    </select>

测试方法

@Test
    public void testFindPage(){
        SqlSession sqlSession=null;
        try {
            sqlSession=SqlSessionFactoryUtil.openSession();
            UserMapper sqlSessionMapper = sqlSession.getMapper(UserMapper.class);
            Page<User> page = new Page<>();
            page.setPageNo(2);
            List<User> users = sqlSessionMapper.findPage(page);
            page.setResult(users);
            System.out.println(page);
        }finally {
            sqlSession.close();
        }

 

参考:

  http://elim.iteye.com/blog/1851081