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集合,是 则调用增强逻辑,否 则不进行增强。

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

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

相关推荐
kfaino19 分钟前
码农的AI翻身(六)你好,我叫 Parameter
后端·aigc
掘金者阿豪22 分钟前
把业务数据变成共享仪表盘:Metabase可视化与远程访问实践
前端·后端
猪猪拆迁队1 小时前
虚拟工厂仿真引擎的架构设计:让一条产线可编程、可观测、可干预
后端·ai编程
字节跳动数据库2 小时前
文章分享——相似函数处理方法
人工智能·后端·程序员
云技纵横2 小时前
@Transactional 失效的 7 种场景:第 5 种最难排查
后端
用户6757049885022 小时前
你知道 Go 结构体和结构体指针调用的区别吗?一文带你彻底搞懂!
后端·go
程序员cxuan2 小时前
读懂 Claude Code 架构分析系列,第一篇,开始!
人工智能·后端·架构
用户6757049885022 小时前
面试官问“装饰器模式”,这样回答薪资多要 3000!
后端
tntxia3 小时前
Geo Scene域名修改引起的一些问题
后端
用户298698530143 小时前
Java 实现 Word 文档加密与权限解除
java·后端