Mybatis源码 - 详解插件机制 自定义插件 初始化流程 拦截原理

前言

Mybatis的插件,是Mybatis提供的一种拓展机制,它可以针对Mybatis中的四大组件进行功能增强,这四大组件分别是:Executor(执行器)StatementHandler(语句处理器)ParameterHandler(参数处理器)ResultSetHandler(结果集处理器)

Mybatis插件基于JDK动态代理,应用了责任链模式,实现了对源代码的零侵入增强。同时,还支持开发者自定义插件,实现自己想要的功能。

今天就来分享一下Mybatis中的插件机制,包括自定义插件实现、插件机制源码解析(初始化流程、调用拦截机制)等,希望可以帮到大家,谢谢~

自定义插件

俗话说,先会走才能跑,学习框架也是同理。一个功能,得先会用,再去学习其源码及优秀设计思想。

这里就先来实现一个自定义插件,具体功能就是打印执行的sql语句,这个小案例旨在展示自定义插件的使用方法。

先介绍一下需要用到的API:两个注解 + 一个接口

@Intercepts

这个注解,是用来标在自定义插件类上面的,功能是用来定义当前插件需要增强的所有组件的方法。该注解有一个属性,是一个Signature数组,这个数组中的每一个元素,就是一个可以被拦截的组件方法。

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

@Signature

这个注解,就是用来具体的定义一个可以被增强的组件的方法,里面有三个属性:

  • Class<?> type():用来指定当前要增强哪个组件,例如语句处理器:StatementHandler.class
  • String method():用来指定要增强组件中的哪个方法,例如:prepare()方法
  • Class<?>[] args():要增强方法的参数类型,因为存在重载方法,所以要指定参数类型,才能确定唯一方法。
java 复制代码
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
  Class<?> type();
  String method();
  Class<?>[] args();
}

Interceptor接口

这个接口是插件实现的根本,每个插件都要实现这个接口,重写里面的方法。

java 复制代码
public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

  default void setProperties(Properties properties) {
    // NOP
  }
}

这个接口有三个方法:

  • intercept:插件拦截组件之后,需要执行的增强逻辑。入参invocation为目标对象。
  • plugin:生成目标对象(待增强组件)的代理对象。
  • setProperties:在插件初始化时,设置一些属性值。通过调用Properties的get或setProperty方法,设置或获取值。

plugin()方法中,又调用了Plugin.wrap(target, this)来生产代理对象,至于为什么这样就可以生产代理对象,我们在后面的源码解析中进行说明,这里大家就知道,这个方法可以生产代理对象即可。

案例

现在我想指定一个自定义插件MyPlugin,在执行sql语句时将sql语句进行打印。想实现这个自定义插件,需要三步

  1. 创建一个自定义插件类,实现Interceptor接口,重写里面的方法,添加自己的增强逻辑。
  2. 自定义插件类上面标注 @Intercepts、@Signature 注解,来指定当前插件要增强哪些组件的哪些方法。
  3. 将自定义插件进行配置,例如在xml核心配置文件中配置\<plugins>标签。

下面这个案例,就是去增强了StatementHandler组件的prepare()方法,在执行的时候,打印当前封装好的sql语句,大家可以做个参考。

java 复制代码
@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class MyPlugin implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler handler = (StatementHandler) invocation.getTarget();
        String sql = handler.getBoundSql().getSql();
        System.out.println("【sql】:" + sql);
        return invocation.proceed();
    }
    
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
    
    @Override
    public void setProperties(Properties properties) {
        System.out.println("插件配置的初始化参数:" + properties);
    }
}
xml 复制代码
<configuration>

    <!--注册插件-->
    <plugins>
        <plugin interceptor="com.sxb.interceptor.MyPlugin"></plugin>
    </plugins>
    
    <!--省略其它配置项...-->
</configuration>

源码解析

核心配置文件解析

Mybatis中对于配置文件的解析,是通过SqlSessionFactoryBuilder类的build方法来完成的,过程中解析了配置文件,创建了SqlSessionFactory工厂对象。解析过程中,经历了XMLConfigBuilder解析核心配置文件、XMLMapperBuilder解析映射配置文件等等。

所以对于核心配置文件中<plugins>标签的解析,肯定是在build方法中完成的。在build方法中,创建了XMLConfigBuilder对象,调用它的parse()方法来解析配置文件,过程中,对于configuration根标签下的每一个子标签都进行了解析,例如<mappers><plugins><environments>等等。

下面是XMLConfigBuilder中的一个重要方法parseConfiguration(),对于每一个子标签都进行了解析。

java 复制代码
  private void parseConfiguration(XNode root) {
    try {
      // issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

重点看一下第9行pluginElement(),这个方法就是对<plugins>标签进行了解析,看一下源码:

java 复制代码
  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).getDeclaredConstructor().newInstance();
        interceptorInstance.setProperties(properties);
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }
  • 第2-5行:方法一开始进行了判空,然后遍历,取出每一个<plugin>标签的属性interceptor的值(这里是配置的自定义插件的全路径),接下来获取了 <plugin> 标签内部的<property>子标签,封装为了Properties对象。
  • 第6行:通过反射的方式,拿到当前插件的构造器对象,然后调用newInstance方法创建了实例。
  • 第7行:调用了插件实例的setProperties方法,将上一步取出的 Properties 对象进行了传入。这里实际就是调用了实现Interceptor接口时重写的方法中。这里我们可以明白:Properties对象是在这里进行了封装,值就是获取的<plugin>内部的所有<property>子标签内容
  • 第8行:调用全局配置对象configurationaddInterceptor方法,将当前插件,添加到了插件拦截器链中。看一下这个方法的源码:
java 复制代码
public class Configuration {
    //省略其它属性和方法...
    protected final InterceptorChain interceptorChain = new InterceptorChain();
    public void addInterceptor(Interceptor interceptor) {
        interceptorChain.addInterceptor(interceptor);
    }
}

可以看到,插件的添加操作,被继续委派给了Configuration类中的一个成员变量interceptorChain,顾名思义就是拦截链,这里调用了拦截链对象的addInterceptor方法。看一下 InterceptorChain 这个类的结构:

java 复制代码
public class InterceptorChain {
  private final List<Interceptor> interceptors = new ArrayList<>();

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

}
  • 首先可以看到,在这个类中,维护了一个List集合interceptors,泛型是Interceptor,不难理解,这个集合中就维护了所有的插件。
  • addInterceptor方法和getInterceptors方法,就是对这个插件集合的操作:添加插件和获取插件。
  • pluginAll 方法比较重要,我们来单独介绍一下

pluginAll方法

我们先来看一下,这个方法是在什么位置调用的

这个方法的调用位置,总共有四处,都在全局配置类Configuration中:

java 复制代码
public class Configuration {
    //省略其它属性和方法...
    protected final InterceptorChain interceptorChain = new InterceptorChain();
    
    //参数解析器构建方法
    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) {
        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 = (Executor) interceptorChain.pluginAll(executor);
        return executor;
    }
}

从上面的代码我们可以看出:这四个方法,不就是文章开头提到的 可以被插件增强的四大组件 的构建方法吗?

在构建的过程中,每个组件都会调用拦截链interceptorChainpluginAll方法,传入当前构建好的对象,这里获取的返回值,如果配置了插件,返回的就是代理对象,反之,就是原对象。为什么这么说呢?还是得看一下pluginAll方法的逻辑

java 复制代码
  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }

方法的逻辑比较简单:就是去遍历了拦截链,然后,调用了每一个插件的plugin方法,还记得这个方法的逻辑吗?这个方法是default修饰的,在接口中就将逻辑定义好的方法。

java 复制代码
public interface Interceptor {
  Object intercept(Invocation invocation) throws Throwable;
  
  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }
  
  default void setProperties(Properties properties) {
  }
}

可以看到,plugin方法中,调用了Plugin这个类的静态方法wrap,传入的参数中,target就是上一步调用时,传入的四大组件的实例,还传入了this,就是当前自定义插件的实例。那为什么调用了这个方法,就能获取到组件实例的代理对象呢?这得看一下Plugin这个类的结构:

java 复制代码
public class Plugin implements InvocationHandler {

  private final Object target;
  private final Interceptor interceptor;
  private final 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;
  }

  @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));
      }
      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);
    // issue #251
    if (interceptsAnnotation == null) {
      throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
    }
    Signature[] sigs = interceptsAnnotation.value();
    Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
    for (Signature sig : sigs) {
      Set<Method> methods = MapUtil.computeIfAbsent(signatureMap, sig.type(), k -> new HashSet<>());
      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<>();
    while (type != null) {
      for (Class<?> c : type.getInterfaces()) {
        if (signatureMap.containsKey(c)) {
          interfaces.add(c);
        }
      }
      type = type.getSuperclass();
    }
    return interfaces.toArray(new Class<?>[0]);
  }

}
  • 看到这里,就很明朗了,因为Plugin这个类,实现了InvocationHandler接口,这是一个函数式接口,是搭配JDK动态代理来使用的,重写了接口中的invoke方法后,使用代理对象调用目标方法时,就会首先调用到invoke方法,实现横向的逻辑增强。可以用一张图来加强一下理解:

    类中也的确重写了invoke方法,方法中,有一行比较重要的代码:interceptor.intercept(new Invocation(target, method, args)),这里就是去调用了插件的intercept方法,也就是 自定义的拦截逻辑。

    看到这里,首先就可以明白:Plugin类,实现了InvocationHandler接口,重写了invoke方法,那么在创建了四大组件的代理对象之后,代理对象调用目标方法,都会转到invoke方法中,而这个方法中,又调用了插件对象的intercept方法,就是通过这样的方式,完成了插件拦截逻辑。

  • wrap方法中的逻辑也很简单,就是获取到当前插件可以增强的方法之后,调用了Proxy对象的newProxyInstance方法,创建了代理对象,调用时传入的InvocationHandler实例,就是去创建了一个Plugin对象。

    • 这个方法中,还进行了@Intercepts @Signature注解的扫描封装,调用了getSignatureMap方法,这个方法中获取了插件类上面标记的注解,获取了配置的属性。最后将所有可以增强的方法,封装成了一个Set<Method>集合,最后在调用intercept增强逻辑之前,进行了判断,当前方法是否存在于set集合,是 则调用增强逻辑,否 则不进行增强。

原来,我们所说的插件可以增强四大组件,原因就在于:这四大组件的构建过程中,都去尝试获取了代理对象。

到这里,整个解析流程就结束了。感谢你的阅读,如果有不对的地方,欢迎在评论区指正!谢谢~

相关推荐
一 乐18 分钟前
办公系统|基于springboot + vueOA办公管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·spring
Tony Bai21 分钟前
Go 1.26 新特性前瞻:从 Green Tea GC 到语法糖 new(expr),性能与体验的双重进化
开发语言·后端·golang
资源站shanxueit或com32 分钟前
Python入门教程:从零到实战的保姆级指南(避坑大全) 原创
后端
越千年34 分钟前
工作中常用到的二进制运算
后端·go
转转技术团队38 分钟前
转转大数据与AI——数据治理安全打标实践
大数据·人工智能·后端
利刃大大42 分钟前
【SpringBoot】SpringMVC && 请求注解详解 && 响应注解详解 && Lombok
java·spring boot·后端
梨子同志1 小时前
Java 介绍与开发环境安装
后端
她说..1 小时前
Spring AOP场景4——事务管理(源码分析)
java·数据库·spring boot·后端·sql·spring·springboot
用户4099322502121 小时前
Vue3动态样式控制:ref、reactive、watch与computed的应用场景与区别是什么?
后端·ai编程·trae