Mybatis拦截器源码深度解析

Posted ryanlikecode

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Mybatis拦截器源码深度解析相关的知识,希望对你有一定的参考价值。

目录:

Mybatis拦截器 可以帮助我们在执行sql语句过程中增加插件以实现一些通用的逻辑,比如对查询sql分页、数据权限处理等。
允许使用插件拦截的方法调用包括:

- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)

各方法的具体作用可以通过 Mybatis之sqlSession调用链分析 进行了解。

方法调用时加载拦截器链的总体时序图如下:
技术分享图片

一. 建立拦截器链

1. 创建对象

建立拦截器对象,对Executor接口的实现类上的update方法调用进行拦截:

// ExamplePlugin.java
@Intercepts({@Signature(
  type= Executor.class,
  method = "update",
  args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
  public Object intercept(Invocation invocation) throws Throwable {
    return invocation.proceed();
  }
  public Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }
  public void setProperties(Properties properties) {
  }
}

Mybaits中通过注解Intercepts来设定当前拦截器是否对被拦截的请求进行处理,例子中的注解指定对Executor的所有update方法进行拦截处理。

  • Intercepts
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
  Signature[] value();
}
  • Signature
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
  Class<?> type();

  String method();

  Class<?>[] args();
}

对注解的解析在Plugin类中。

2. 建立配置文件

可以通过XMl和注解两种方式进行配置,通过解析配置建立拦截器链。

  • xml配置:
<!-- mybatis-config.xml -->
<plugins>
  <plugin interceptor="org.mybatis.example.ExamplePlugin">
    <property name="someProperty" value="100"/>
  </plugin>
</plugins>
  • 注解配置:
@Bean
    public PaginationInterceptor paginationInterceptor() {
        return new PaginationInterceptor();
    }

3. 加载拦截器链

以XML配置好拦截器好,从XML解析源码中,可以看到通过xml标签属性 interceptor 把拦截器加入 Configuration的 InterceptorChain中,

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

二. 方法调用解析

之前我们配置了一个对Executor的update方法进行拦截的插件,那么看下具体的执行过程。在调用Configuration生成执行对象时,通过拦截器链对对象进行包装

  public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    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);
    }
    // 调用拦截器链对executor进行包装,因为使用了JDK的动态代理,所以返回对象必须为接口 
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

1. 对请求对象进行拦截器包装

通过InterceptorChain的pluginAll方法,对interceptors集合循环,依次对target(也就是上面传入的executor对象)进行代理:

public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      // target通过拦截器链,循环迭代代理
      target = interceptor.plugin(target);
    }
    return target;
  }

自定义的拦截器需要实现Interceptor实现的plugin方法,该方法用来给target增加代理,推荐直接调用Plugin.wrap(target, this)方法(这里将每一个代理对象的创建放在Plugin类中的静态方法,但是每新增一个插件都需要写这个方法)

  @Override
  public Object plugin(Object target) {
    // 注意第二个参数 为this,通过回调自己,将参数传递给Plugin对象,plugin代理对象执行时,如果符合条件,将回调target的intercept,参见本文2小节
    return Plugin.wrap(target, this);
  }

Plugin实现了InvocationHandler接口,其类图如下:

技术分享图片

Plugin的wrap方法用来创建target的代理对象:

// 参数interceptor为 ExamplePlugin类对象 
public static Object wrap(Object target, Interceptor interceptor) {
    // 1. 获取注解中的配置,class对应Signature中的type属性,Set<Method>对应Signature中的method属性
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    // 2. 获取被代理对象的class类对象,这里为Executor的接口实现类
    Class<?> type = target.getClass();
    // 3. 获取符合对象target接口的拦截器(这里为Executor.class)
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    // 如果注解包含目标对象接口,对Plugin对象进行代理,返回的Plugin代理对象,调用目标方法时会进行Pluing的invoke()方法
    if (interfaces.length > 0) {
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          // 代理对象传入参数包括 目标类、该拦截器对象、注解配置解析Map
          new Plugin(target, interceptor, signatureMap));
    }
    // 如果该插件注解中TYPE值不包含目标对象接口,则不处理,直接返回目标对象
    return target;
  }

其中getAllInterFaces方法用来判断判断注解中是否包含目标对象接口:

private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
    Set<Class<?>> interfaces = new HashSet<>();
    while (type != null) {
      // 如果传入Executor的实现类,那么这里为Executor接口
      for (Class<?> c : type.getInterfaces()) {
        // 1. 判断注解中是否包含目标对象接口  
        if (signatureMap.containsKey(c)) {
          interfaces.add(c);
        }
      }
      // 2. 如果继承父接口,继续循环判断
      type = type.getSuperclass();
    }
    // 返回该插件注解与被代理对象匹配的所有的接口类对象数组 
    return interfaces.toArray(new Class<?>[interfaces.size()]);
  }

2. 执行调用

当调用被代理对象Execute的所有方法,都会进入Plugin的invoke方法:

  // 代理类只用来做流程判断,不增加具体的业务逻辑,业务逻辑统一在实现Invocation接口的插件类中增加
  // 对该方法进行递归调用(包括所有已加载进拦截器链中的拦截器,首先判断当前拦截器中的注解条件是否满足,满足的话执行当前拦截器,否则调用target的方法,进入下一个拦截器)
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      // 判断Signature参数method是否包含当前调用方法,如果包含,进入拦截器intercept方法;否则,跳过该拦截器继续运行
      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);
    }
  }

自定义拦截器中intercept实现如下:

public Object intercept(Invocation invocation) throws Throwable {

    // TODO 增加自己的业务需求

    return invocation.proceed();
  }

Invocation相当于责任链中的请求类,其封装反射参数,包含target(target通过InterceptorChain的pluginAll循环代理,因此可以是拦截器的代理类,当经过最后一个拦截器时,为实际调用对象)、method、args,该类主要用来减少intercept方法调用时传入的参数数量

技术分享图片

其中proceed方法如下:

public Object proceed() throws InvocationTargetException, IllegalAccessException {
    // 通过反射执行target的方法
    // 如果target依旧为Plugin代理类,则继续进行代理类Plugin中的invoke方法中
    return method.invoke(target, args);
  }

三. 小结

Mybatis拦截器主要实现以下几个点:

  • 通过注解判断被拦截的请求是否符合当前的拦截器。
  • 支持横向扩展,可以自定义拦截器并加入到拦截器链中。
  • 支持请求在拦截器链中依次传递(Invocation类)。

    其结合责任链模式使请求和处理解耦,但是每一次请求都要通过责任链上的所有拦截器,也就是一次调用需要所有拦截器进行判断,因此也有一些局限性。

相关文章:

一文读懂JDBC
mybaits动态代理之最小demo实现
Mybatis之sqlSession调用链分析

了解更多请关注微信公众号:

技术分享图片












以上是关于Mybatis拦截器源码深度解析的主要内容,如果未能解决你的问题,请参考以下文章

Mybatis源码解析MyBatis插件原理

怒肝一夜 | Mybatis源码深度解析

mybatis源码配置文件解析之四:解析plugins标签

《MyBatis3源码深度解析》图书简介

《MyBatis3源码深度解析》图书简介

《MyBatis3源码深度解析》图书简介