mybatis插件实现原理和手写实现


mybatis提供了一个可插拔的插件插件实现方式,只要实现 Interceptor接口,并在实现类中使用注解@Intercepts表明要增强的类和方法。这篇文章将分享mybatis插件的底层实现原理,并动手模拟写一个我们自己的Demo

mybatis插件实现原理

mybatis在openSession()方法的时候创建SqlSession的对象,再创建对象之前会进行Executor的实例化,封装在SqlSession中,最后执行sql语句的时候会调用Executor的实例对象与数据库交互。不清楚的,可以参考我这篇文章:mybatis源码阅读
创建Executor方法如下:

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);
    }
    //TODO 开启二级缓存,所有executor共享
    if (cacheEnabled) {
      //TODO 装饰者设计模式:增加executor缓存功能
      executor = new CachingExecutor(executor);
    }
    //TODO 将执行器注册到拦截器栈的每一个拦截器中,返回动态代理对象
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

如上所示,创建Executor的时候会根据执行类型创建不同的的Executor , 然后判断如果开启了缓存则使用装饰者设计模式封装成CachingExecutor对象 ,这个类也是实现了Executor接口,最后使用java的动态代理注册定义从插件,返回代理对象。整个Executor的类图如下:
在这里插入图片描述

注册插件的方法如下:

 private final List<Interceptor> interceptors = new ArrayList<>();
 
 public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }

自定义的拦截器在解析配置的是否封装到了 interceptors 里面,因为list是有顺序的,所以配置在前面的插件先被代理,而后面的后代理;再继续看看注册插件的方法:

default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }
//TODO target是需要代理的对象 ,interceptor是需要添加的拦截器
  public static Object wrap(Object target, Interceptor interceptor) {
    // TODO signatureMap : 是拦截的接口类型 和 接口中需要拦截方法的集合构成的map映射
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    //TODO 获取到被代理对象中所有需要拦截的接口
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
      //如果代理对象实现了需要拦截的接口,则将代理对象返回(代理对象持有target, interceptor, signatureMap)
      // 代理对象调用接口的中的任意方法,都会先执行 Plugin 类的invoke方法
      //proxy相当于实现了interfaces中的所有接口,所以展现出什么样的特性,是由强转决定的
       Object proxy = Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
      return proxy ;
    }
    //没有需要代理的接口,原样返回
    return target;
  }

从源码中可知,传入的Executor对象,判断如果是实现了接口则使用java的动态代理进行代理返回;如果不需要插件增加,则直接返回该对象。那如何判断是否需要注册插件增强呢?看如下getSignatureMap方法的代码:

private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    //获取到自定义插件上的 Intercepts 注解
    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
    if (interceptsAnnotation == null) {
      throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
    }
    //获取到Intercepts注解的属性值 Signature[] ,每一个 Signature就是一个需要拦截的接口类型里的一个方法
    Signature[] sigs = interceptsAnnotation.value();
    //封装需要拦截的类和 拦截类中的方法集合的映射: Excutor:[update,query]
    Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
    for (Signature sig : sigs) {
      //每一个拦截的类传建一个set集合
      Set<Method> methods = signatureMap.computeIfAbsent(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;
  }

这个方法从传入的拦截器中获取到 Intercepts注解的信息,将要拦截的接口接口中的方法封装到 Map<Class<?>, Set> 这样的一个map中。最后在创建代理对象new Plugin(target, interceptor, signatureMap)的时候将具体的执行类,要使用的拦截器和注解中要拦截的接口方法全部封装到代理对象中。封装后如下:
在这里插入图片描述
最终形成的对象嵌套如上,也可以看出,我们定义拦截器顺序的时候,最先配置的拦截器是最后才执行的。因为对象都是实现了Executor接口,所以拿到代理对象的时候才可以强转类型为Executor .

根据上面的分析,我们最终拿到的对象是一个代理类Plugin,还没有看到插件怎么对特定的接口方法生效的呢?java的动态代理调用方法会先执行代理类的invoke方法,查看代理类Plugin的invoke方法如下:

 @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      //获取方法声明的接口,从signatureMap中获取到需要拦截的类的所有方法
      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);
    }
  }

以上代码可以看出,我们到代理类实现的接口,并通过接口查询之前封装好的signatureMap有没有这个接口的拦截方法,如果有再判断拦截的方法是不是这次要执行的方法,是则执行拦截器中的代码。如下我实现的拦截器,在intercept方法中执行我的业务逻辑。


@Intercepts({@Signature(type = Executor.class,method = "update",args = {MappedStatement.class,Object.class})})
public class TestPlugin implements Interceptor {
  private String username ;

  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    System.out.println("插件拦截器增强逻辑");
    Object proceed = invocation.proceed(); //执行原来的业务逻辑
    System.out.println("插件拦截器增强逻辑");
    return proceed;
  }

  @Override
  public void setProperties(Properties properties) {
    username = properties.getProperty("username");
  }
}

在这里插入图片描述
结合之前这张图片,箭头就是整个方法调用流程,执行完插件的逻辑后,最后才调用到数据库进行SQL操作。

实现可插拔的插件功能

以上插件的实现方式完全可以作为一个工业级的实现方案使用,如果没法使用基于接口的动态代理,也可以使用cglib的动态代理实现方式。下面根据以上的逻辑,实现我们自己的插件功能。

1.定义拦截器接口

这里我基于接口使用java类库实现动态代理,插件逻辑实现这个接口即可

public interface MyInterceptor {
  //默认的设置用户定义的变量,可以不实现,不用传递变量
  default void setPropertis(Properties properties){ }
  //进行拦截的方法,需要实现接口,定义拦截的逻辑,
  Object doInterceptor(Object proxy, Method method, Object[] args)throws Throwable;
}

2.定义注解

实现拦截器后,我们需要知道拦截器要拦截哪些类型的接口和方法,以下是我的简单注解定义。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyInter {
  Class clazz();  //拦截的接口类型
  String method(); //拦截的方法名
  Class[] args(); //拦截的方法参数类型
}

3.定义一个代理类

代理类实现动态代理的功能,生成动态代理对象。分析过程:

  1. 既然是代理,那就要有被代理对象,所以要持有被代理对象的引用。
  2. 通过拦截器插件强原来的功能,那就还要持有拦截器的引用。
  3. 需要通过比较拦截器要拦截的接口是否是当前调用对象实现的接口,并且判断当前调用的方法是不是拦截器要拦截的方法才能知道是否需要进行拦截,所以还要持有需要拦截的接口类 和 拦截的接口方法引用。
    实现如下:
//代理对象需要用拦截器增加被代理的类,所以需持有被代理对象的引用和拦截器引用
public class MyPlugin implements InvocationHandler {

  //被代理的类
  private final Object target;
  //使用增强的拦截器
  private final MyInterceptor myInterceptor;
  //拦截的接口类型
//  private Map<Class, Set<Method>> signMap = new HashMap<>();
  private Class clazz;
  private Method meth;

  public MyPlugin(Object target, MyInterceptor myInterceptor ) throws Exception{
    this.target = target;
    this.myInterceptor = myInterceptor;
    //获取拦截器中的注解
    MyInter annotation =  myInterceptor.getClass().getAnnotation(MyInter.class);
    Class cla = annotation.clazz();
    //获取到注解中标记要拦截的接口和对应的方法
    this.clazz = cla ;
    this.meth = cla.getMethod(annotation.method(),annotation.args());
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    //反射获取声明方法的类,判断声明的类和调用的方法是不是拦截器注解中要拦截的方法
    Class cla = method.getDeclaringClass();
    if (meth.equals(method) && clazz.equals(cla)) {
      return myInterceptor.doInterceptor(target,method,args);
    }
    return method.invoke(target,args);
  }

  //代理所有的方法
  public static Object plugin(Object target , MyInterceptor interceptor)throws Exception{
    Object proxy = Proxy.newProxyInstance(
      target.getClass().getClassLoader(),
      target.getClass().getInterfaces(),
      new MyPlugin(target, interceptor));
      return proxy ;
  }

}

4.测试

定义一个接口

public interface Speak {

  void speak();

  void talk();
}

定义接口实现类

public class Monkey implements Speak {

  public void speak(){
    System.out.printf("Monkey speak");
  }

  public void talk(){
    System.out.printf("Monkey talk");
  }
}

编写两个拦截器,分别拦截接口中的两个方法:

//拦截Speak 接口的 speak方法
@MyInter(clazz = Speak.class,method = "speak",args = {})
public class ExampleInterceptor implements MyInterceptor {

  @Override
  public Object doInterceptor(Object proxy, Method method, Object[] args) throws Throwable {
      System.out.println("ExampleInterceptor");
      return method.invoke(proxy,args);
  }
}
//拦截Speak 接口的 talk方法
@MyInter(clazz = Speak.class,method = "talk",args = {})
public class SecondIterceptor implements  MyInterceptor{

  @Override
  public Object doInterceptor(Object proxy, Method method, Object[] args) throws Throwable {
    System.out.println("SecondIterceptor");
    return method.invoke(proxy,args);
  }
}

main方法调用测试:

public static void main(String[] args) throws Exception {
    Speak speak = new Monkey();
    MyInterceptor instance1 = (MyInterceptor)Class.forName("demo.interceptor.ExampleInterceptor").newInstance();
    MyInterceptor instance2 = (MyInterceptor)Class.forName("demo.interceptor.SecondIterceptor").newInstance();
    Speak plugin1 = (Speak)MyPlugin.plugin(speak, instance1);
    Speak plugin2 = (Speak)MyPlugin.plugin(plugin1, instance2);
    plugin2.talk();
  }

结果:
在这里插入图片描述
上面 Class.forName反射创建类,其实就是拿到配置文件中配置的类名,然后进行反射创建对象注册到拦截器的List中,然后循环注册。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值