MyBatis插件实现原理

1. 插件能够拦截的对象和方法

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

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

2. 插件的工作流程

解析注册---->代理----->执行时拦截

  • 解析注册是在解析mybtais-config.xml配置文件时,解析plugin标签时完成。把所有的插件都放到interceptorChain中进行保存。这是一个ArrayList。

  • 代理是在创建上面四个对象时执行。具体的代理逻辑,下面分析。

  • 执行时拦截,拦截器会拦截配置的对象的方法。例如我有一个拦截器拦截Executor的query方法,当Executor执行query方法时,会先走拦截器的逻辑。思想上有些类似于Spring的AOP。

3. 一次自定义插件使用的分析

3.1 插件配置

        
            
            
        

代码实现

实现Interceptor接口,实现接口的方法,在intercept方法中实现自己的插件功能。

@Intercepts({@Signature(type = Executor.class,method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class MyPageInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("自定义插件开始执行...");
        Executor executor = (Executor)invocation.getTarget();
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0]; // MappedStatement
        BoundSql boundSql = ms.getBoundSql(args[1]); // Object parameter
        ResultHandler resultHandler = (ResultHandler) args[3];
        String countSql = boundSql.getSql();
        System.out.println("获取到SQL语句:"+countSql);
        countSql = countSql + " limit 5";

        SqlSource sqlSource = new StaticSqlSource(ms.getConfiguration(),countSql,boundSql.getParameterMappings());
        Field field = MappedStatement.class.getDeclaredField("sqlSource");
        field.setAccessible(true);
        field.set(ms,sqlSource);
        
        try{
            return invocation.proceed();
        }finally {
            System.out.println("自定义插件执行结束!!!");
        }
    }

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

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

3.2 插件的代理

上面的例子中,我们的插件要拦截Executor的query方法。

Mybatis在运行的时候是怎么进行拦截的呢?

  • 首先,openSession()的时候会创建一个executor执行器,最后会对这个执行器进行插件植入操作。executor = (Executor) interceptorChain.pluginAll(executor);

  • 然后,把executor作为target进行代理,当配置多个拦截器的时候,会进行多层代理。代理流程如下:

InterceptorChain.java
// 对executor即进行代理
public Object pluginAll(Object target) {
  // 第一步 interceptor是配置的插件
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }
MyPageInterceptor.java
// 第二步,到具体的实现类中调用plugin方法
@Override
public Object plugin(Object target) {
    return Plugin.wrap(target, this);
}

不管有没有配置拦截器,可以被拦截的四种对象在创建的过程中都会有拦截器的包装操作,但是到底要不要进行包装呢,由if (interfaces.length > 0) {这句代码进行控制。

Plugin.java
// 第三步,使用jdk的动态代理
public static Object wrap(Object target, Interceptor interceptor) {
  // 获取拦截器要拦截的接口的签名,只要拦截器配置就会有值
  Map, Set> signatureMap = getSignatureMap(interceptor);
  // 获取目标类的类型
  Class type = target.getClass();
  /** 
   * 判断目标类是不是本次要被代理的接口。因为mybatis的拦截器可以拦截Executor、ParameterHandler、
   * ResultSetHandler、StatementHandler四种对象。这四个对象的创建过程中都会有拦截器逻辑的判断,都          * 走这个代码从interceptor中判断出这个拦截器可以拦截哪些对象,从target中知道这个对象是什么,从而可以     * 判断本次要不要进行代理操作。
   **/
  Class[] interfaces = getAllInterfaces(type, signatureMap);
  if (interfaces.length > 0) {
    return Proxy.newProxyInstance(
        type.getClassLoader(),
        interfaces,
        new Plugin(target, interceptor, signatureMap));
  }
  return target;
}

到此为止,被拦截的对象的代理工作就完成了。

3.4 插件的执行

3.4.1 JDK动态代理

JDK动态代理后方法的执行顺序:

// jdk动态代理的一个例子,重写的invoke()方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    before();
    Object res = method.invoke(person,args);
    return res;
}
  1. 代理对象调用方法。
  2. 执行h的invoke()方法。
  3. 进行前置增强处理。
  4. method.invoke真正执行要到用的方法。
3.4.2 Mybatis中动态代理的调用

我们已经知道,Mybatis是使用JDK的动态代理,去动态的代理真正要执行的对象的方法。从而实现增强的功能。因此,当要执行的对象进行方法调用的时候,会先执行实现了InvocationHandler类的invoke方法。即:

Plugin类中的invoke方法。

// plugin是触发管理类,当被代理的对象的方法执行的时候,先执行这个invoke方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
    Set 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);
  }

此时,当执行executor的query方法时,就会调用到MyPageInterceptor类中的intercept方法。

            try{
            return invocation.proceed(); // 这句话需要关注下
        }finally {
            System.out.println("自定义插件执行结束!!!");
        }

当我们执行完自己实现的插件的逻辑之后,还需要执行真正要执行的方法。什么时候去执行呢?

我们知道jdk动态代理中是使用method.invoke(target, args);去执行真正的方法的调用。而invocation.proceed();做的就是这个事情。

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

到这里,拦截器就执行完了。

4. 关键的类

InterceptorChain: 用于保存拦截的对象,使用责任链模式的思想,把所有的拦截器都添加到这个链中。

Interceptor接口: Mybtais框架给提供的接口,所有自己要实现的拦截器都要实现这个接口。相当于给定义了一个规范。

Plugin类:Mybtais框架给提供的类,实现了InvocationHandler接口,在进行invoke方法调用的时候,它的对象可以作为h参数。

Invocation类:Mybtais框架给提供的一个封装的类。把invoke方法的三个参数封装了起来,应该是为了更加方便的调用吧。

5. 总结一下调用流程

  1. executor执行query方法。
  2. 发现自己被代理了,然后去执行invoke方法。
  3. 此时就来到了Pluigin的invoke方法中,继续去执行interceptor的intercept()方法。
  4. 自定义逻辑执行完之后执行一下invocation.proceed(),就完成了method.invoke()的调用。

最后借用一下青山老师的图:


调用过程.png

你可能感兴趣的:(MyBatis插件实现原理)