mybatis拦截器 - 969251639/study GitHub Wiki

mybatis的拦截器可以用来实现很多功能,比如最常见的分页拦截等,很方便的去扩展mybatis
mybatis执行sql时最终会获取一个statement的handler

  @Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    ...
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
    ...
  }

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

首先看下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);//将执行完结果传递给下一个,所以需要将执行结果赋给target
    }
    return target;
  }

  public void addInterceptor(Interceptor interceptor) {//添加连接器到链中
    interceptors.add(interceptor);
  }
  
  public List<Interceptor> getInterceptors() {//获取所有拦截器
    return Collections.unmodifiableList(interceptors);
  }
}

很明显的可以看到用了一个List来保存所有的Interceptor,然后调用pluginAll可以让整个拦截器链逐个执行,返回最后一个拦截器的执行结果

接下来看拦截器的使用

  1. 在mybatis的配置中配置plugin节点
<configuration>
    ...
    <plugins>
        <plugin interceptor="xxx">
            <property name="key" value="value" />
            ...
        </plugin>
        ...
    </plugins>
</configuration>

这样mybatis解析时便可以解析到该节点,并将其加入到拦截器链中

  private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {//获取所有plugins下的节点
        String interceptor = child.getStringAttribute("interceptor");//获取plugin节点的interceptor属性(该属性为插件实现类的全路径)
        Properties properties = child.getChildrenAsProperties();//获取配置属性
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();//反射创建插件实例对象
        interceptorInstance.setProperties(properties);//为创建好的对象设置属性
        configuration.addInterceptor(interceptorInstance);//加入到拦截器链中
      }
    }
  }
  1. 然后需要在实现类中加入Intercepts注解,并实现Interceptor接口,重写intercept, plugin, setProperties三个方法
    @Intercepts({ @Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}), @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}), @Signature(type = StatementHandler.class, method = "batch", args = {Statement.class}) }) public class LogIntercceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { ... } @Override public Object plugin(Object target) { if (target instanceof StatementHandler) { return Plugin.wrap(target, this); } else { return target; } }

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

其中plugin是整个链路的核心,因为它是串起整个拦截器链的实现

  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {//挨个执行拦截器
      target = interceptor.plugin(target);//将执行完结果传递给下一个,所以需要将执行结果赋给target
    }
    return target;
  }

而且实现一般都是调用下面的代码

Plugin.wrap(target, this);

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

在上面的例子中第一个参数是target是StatementHandler(mybatis可以拦截三种handler,后面讲),第二个参数是拦截器类本身

  • 获取拦截器类上面的注解,也就是Signature标签,上面登记着要拦截的方法,参数,handler类型等信息
  • 获取目标对象(StatementHandler)的Class
  • 动态代理出目标对象的代理类
public class Plugin implements InvocationHandler {
  @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));//如果是需要拦截的方法则调用拦截器类的intercept方法
      }
      return method.invoke(target, args);//如果不需要拦截的方法,直接调用目标类的方法
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }
}

所以,mybatis拦截器所能拦截的方法就是在拦截器类的注解上声明

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
  Signature[] value();
}

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
  Class<?> type();//handler类型

  String method();//拦截方法

  Class<?>[] args();//参数
}

再回过头看这段代码

    @Override
    public Object plugin(Object target) {//其实写return Plugin.wrap(target, this);也一样
	if (target instanceof StatementHandler) {//因为签名中代理的handler是StatementHandler
	    return Plugin.wrap(target, this);
	} else {
	    return target;
	}
    }

会发现,如果目标类是StatementHandler,那么就返回一个动态代理类,代理StatementHandler对象,然后循环到下一个拦截器类,还是执行上面那段代码,也会代理之前返回的那个代理类,比如有三个拦截器,那么会嵌套三层代理

然后当执行query方法时,会从最外面的那一层开始调用invoke方法,判断是否需要拦截该方法,需要则进入拦截器的intercept方法,否则进入下一层拦截器,当进入intercept方法后最后也需要调用invocation.proceed();方法继续进入下一层拦截器,最后到最终的目标类,调用目标方法

    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);

    //返回代理对象
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        ...
        return invocation.proceed();
    }

    public Object proceed() throws InvocationTargetException, IllegalAccessException {
        return method.invoke(target, args);//调用下一个拦截器,最后一个的话则是目标类方法
    }

前面提过mubatis可以拦截三种handler


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

分别是StatementHandler,ResultSetHandler,ParameterHandler,对应sql执行拦截,执行结果拦截,参数拦截,另外还有一个可以拦截的点是Executor执行器

  public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    ...
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

所以注解中的签名@Signature的type为下面的四种
StatementHandler.class ResultSetHandler.class ParameterHandler.class Executor.class

确定了type之后就可以配置method属性对type对象里面的方法进行拦截了

//拦截StatementHandler接口中的query(Statement, ResultHandler)方法
@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class})

public interface StatementHandler {
  ...
  <E> List<E> query(Statement statement, ResultHandler resultHandler)
      throws SQLException;
  ...
}
⚠️ **GitHub.com Fallback** ⚠️