手写MyBatis ORM框架(九)

手写MyBatis ORM框架(九)

引言

这篇文章我们来实现一下这个MyBatis的插件功能。

开始

介绍

Mybatis Plugin 的插件功能是非常重要的一个功能点,包括我们可以结合插件的扩展;分页、数据库表路由、监控日志等。而这些核心功能的扩展,都是来自于 Mybatis Plugin 提供对类的代理扩展,并在代理中调用我们自定义插件的逻辑行为。

对于插件的使用,我们会按照 Mybatis 框架提供的拦截器接口,实现自己的功能实现类,并把这个类配置到 Mybatis 的 XML 配置中

<plugins>

<plugin interceptor="com.example.LogInterceptor"> <!-- 拦截器顺序决定责任链层级 -->

<property name="threshold" value="1000"/> <!-- 可传递参数 -->

</plugin>

</plugins>

MyBatis 的插件机制是其框架扩展性的核心,允许开发者通过拦截器(Interceptor)对 Executor、StatementHandler、ParameterHandler、ResultSetHandler 四大核心组件的关键方法进行功能增强。

流程图

详细设计

拦截器定义

接口实现:所有插件需实现 Interceptor 接口,定义拦截逻辑。

public interface Interceptor {
  Object intercept(Invocation invocation) throws Throwable; // 拦截逻辑
  Object plugin(Object target);  // 生成代理对象
  void setProperties(Properties properties); // 接收配置参数
}

拦截点声明

注解标记:通过 @Intercepts 和 @Signature 指定拦截的目标类和方法。

@Intercepts({
  @Signature(
    type = Executor.class, // 拦截目标类
    method = "query",      // 拦截方法名
    args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class} // 方法参数类型
  )
})
public class LogInterceptor implements Interceptor { ... }
  • Intercepts 注解一个目的是作为标记存在,所有的插件实现都需要有这个自定义的注解标记。另外这个注解中还有另外一个注解的存在,就是方法签名注解,用于定位需要在哪个类的哪个方法下完成插件的调用。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {

    Signature[] value();

}
public @interface Signature {

    /**
     * 被拦截类
     */
    Class<?> type();

    /**
     * 被拦截类的方法
     */
    String method();

    /**
     * 被拦截类的方法的参数
     */
    Class<?>[] args();

}
  • Signature 方法签名接口,定义了被拦截类的type,也就是如我们拦截 StatementHandler 语句处理器。另外就是在这个类下需要根据方法名称和参数来确定是这个类下的哪个方法,只有这2个信息都存在,才能确定唯一类下的方法。

创建Plugin类

Plugin 通过实现 InvocationHandler 代理接口,在 invoke 方法中包装对插件的处理,当任何一个被代理的类,包括:ParameterHandler、ResultSetHandler、StatementHandler、Executor,在执行方法调用的时候,就可以调用到用户自己实现的插件了。

getSignatureMap 所完成的动作就是为了获取代理类的签名操作,返回这个类下在哪个方法下执行调用插件操作,具体的处理方式如下;

  1. 根据入参 Interceptor 的接口的实现,从实现类的注解上获取方法的签名信息。

  2. 方法签名可以是一个数组结构,也就是一个插件可以监听多个配置的类以及多个类内的方法,当这些类的方法被调用的时候,就会调用到执行的自定义插件。

  3. 而在这个方法下,把符合的监听方法返回一个列表,用于代理类中判断是否调用插件。

Map<Class<?>, Set<Method>>

  • wrap 方法是用于给 ParameterHandler、ResultSetHandler、StatementHandler、Executor 创建代理类时调用的。而这个创建的目的,就是把插件内容,包装到代理中。

  • 代理的创建是通过 Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) 实现的,而入参 InvocationHandler 的实现类,则是这个 Plugin 代理插件实现类。

  • 最终对于插件的核心调用,都会体现到 invoke 方法中。如一个被代理的类 ParameterHandler 当调用它的方法时,都会进入 invoke 中。在 invoke 方法中,通过前面方法的判断确定使用方自己实现的插件,是否在此时调用的方法上。如果是则进入插件调用,插件的实现中处理完自己的逻辑则进行 invocation.proceed(); 放行。如果不在这个方法上,则直接通过 method.invoke(target, args); 调用原本的方法即可。这样就达到了扩展插件的目的。

拦截器注册

XML 配置:在 mybatis-config.xml 中声明插件。

<plugins>
  <plugin interceptor="com.example.LogInterceptor"> <!-- 拦截器顺序决定责任链层级 -->
    <property name="threshold" value="1000"/> <!-- 可传递参数 -->
  </plugin>
</plugins>

代理链生成

责任链构建:MyBatis 初始化时,通过 InterceptorChain.pluginAll() 方法按注册顺序生成动态代理链。

// InterceptorChain 核心逻辑
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;
  }
}

插件执行流程

嵌套顺序:后注册的拦截器位于代理链外层。

Proxy2(PageInterceptor)

→ Proxy1(LogInterceptor)

→ 原始Executor对象

LICENSED UNDER CC BY-NC-SA 4.0