欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Mybatis(八) - Mybatis插件原理

程序员文章站 2022-03-26 20:23:18
Mybatis插件原理本篇文章是通过看视频学习总结的内容, 如有错误的地方请谅解,并联系博主及时修改,谢谢您的阅读.回顾上一篇文章:《Mybatis手写分页插件》前言: 根据上一篇博客《mybatis手写分页插件》已经参与过一次手写插件的过程,那么在本篇文章中,主要讲解 mybatis 是如何实现插件对四大对象的不同方法进行拦截的......

Mybatis插件原理

本篇文章是通过看视频学习总结的内容, 如有错误的地方请谅解,并联系博主及时修改,谢谢您的阅读.

前言: 根据上一篇博客《mybatis手写分页插件》已经参与过一次手写插件的过程,那么在本篇文章中,主要讲解 mybatis 是如何注册插件并且实现对四大对象的不同方法进行拦截的.

在之前书写 《Mybatis(二) - Mybatis是如何创建出SqlSessionFactory的》 一文中,简单讲述了 mybatis 加载 mybatis-config.xml 并将内容初始化到 Configuration 对象中,因为我们配置的插件是在 xml文件中,所以最终我们自己的 Interceptor 一定是存在于 Configurator 对象中的,那么继续回顾一下解析xml拿到所有的Interceptor 的过程

  • 1 单元测试,进入 build(inputStream) 方法
@Before
public void prepare() throws IOException {
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
}
  • 2 找到 XMLConfigBuilder#parse() 进入
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
  	  // 进入这里的 parse()
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        inputStream.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }
  • 3 这里就能直观的看到 直接解析 root(/configuration) 节点,然后进入方法 parseConfiguration()
public Configuration parse() {
  if (parsed) {
    throw new BuilderException("Each XMLConfigBuilder can only be used once.");
  }
  parsed = true;
  // 这里直接就拿到 root 节点,然后进入方法parseConfiguration() 中
  parseConfiguration(parser.evalNode("/configuration"));
  return configuration;
}
  • 4 找到解析 plugins 节点的解析方法,并且进入方法
private void parseConfiguration(XNode root) {
  try {
    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);
    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);
  }
}
  • 5 关键的地方来了,这里就是将 plugins 节点下的所有 interceptor 解析出来放入到 Configuration 对象中 interceptorChain 容器中
private void pluginElement(XNode parent) throws Exception {
  if (parent != null) {
  	// 循环 plugins 父节点,拿到每个子节点
    for (XNode child : parent.getChildren()) {
      // 拿到 xml 中自己配置的 intercptor 的类全路径
      String interceptor = child.getStringAttribute("interceptor");
      // 读取到 pugin 节点的子节点,并且包装成 Properties 对象
      Properties properties = child.getChildrenAsProperties();
      // 这里就是最关键的地方,是将我们配置在 xml上 interceptor属性通过反射,使用构造器创建一个实例
      Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
      interceptorInstance.setProperties(properties);
      // 最终调用 configuration 对象中的 addInterceptor,将实例化完成的 interceptor 放入到 interceptorChain容器中
      configuration.addInterceptor(interceptorInstance);
    }
  }
}

// 放入容器
public void addInterceptor(Interceptor interceptor) {
  interceptorChain.addInterceptor(interceptor);
}
  • 6 我很好奇interceptorChain 到底是个什么数据结构,我相信看的人也比较好奇,那么随我一同进去看看吧
/**
 * @author Clinton Begin
 */
public class InterceptorChain implements Serializable {

  private final List<Interceptor> interceptors = new ArrayList<>();

  // 这个方法很重要,后续在初始化 Exector 的时候,就会调用该方法
  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);
  }
}
  • 7 6 小结中的 interceptor.plugin(target) 不是很明白,那不如继续进入看看 mybatis 是怎么写的.
public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

  // 调用这里的这个 default 的方法,我们实现的不就是这个 Interceptor 接口中这三个方法吗???
  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

  default void setProperties(Properties properties) {
    // NOP
  }
}
  • 8 既然自定义 mybatis 插件就是实现的这个三个方法,那么又回过头看看自己自定义的 Interceptorplugin 方法是怎么写的
@Override
public Object plugin(Object target) {
    return Plugin.wrap(target, this);
}

这里为什么要这么写? 如果不写这个方法,回出现什么情况?
那么在自定义的 interceptor 中删除这些方法,又会出现什么情况,我已经删了,并且跑了一次,没任何异常情况,插件也能被使用。为什么呢?

其实啊, 接口中的 default 修饰的方法,是可以不用被实现的,只要某一个地方是调用了 Interceptor 接口中的 plugin 方法, 那么其实都能被执行的,因为在自定义 interceptor 类中 plugin 方法和 Interceptor接口中 pugin 方法逻辑是一样的,都是调用了 Plugin.wrap()

  • 9 既然知道了这个方法的重写是可选的,那么应该先知道这个方法到底是怎么被调用的,这个方法内部又做了什么事情,我选择进入方法去看看。
public static Object wrap(Object target, Interceptor interceptor) {
  // 这里是解析@Interceptors 注解,拿到被拦截的类和被拦截的方法
  // 结合自己的拦截器,这里的内容就是 {Executor.class, [query()] }
  Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
  // 拿到被拦截的目标类  Executor.class
  Class<?> type = target.getClass();
  // 拿到被拦截的类的所有实现的接口
  Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
  if (interfaces.length > 0) {
    // 通过动态代理,创建被拦截的类的实例
    return Proxy.newProxyInstance(
        type.getClassLoader(),
        interfaces,
        // 这里的 invocationHandler 是 plugin 对象,只要一旦执行了 Executor#query方法,就会去调用plugin对象中的 invocke() 方法。
        new Plugin(target, interceptor, signatureMap));
  }
  return target;
}

/**
 * 这个方法其实就是将拦截的类和方法做映射
 * 这里面有个容易被忽略的内容就是 sig.args(),这个参数是自定义Interceptor类上的注解中,传入的参数,   
 * 其目的是为了在自定义Intecptor 中能使用到这些参数。这些参数也是有规则的,不能随意的传入。
 */
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 = 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;
}

wrap() 方法其实的作用,就是将所有的被拦截到的类(@Interceptor注解去拦截的类,在这里的是Executor类,和被拦截的方法 query),并且为这个Executor 生成一个动态代理的类,只要方法执行到为被代理的Executorquery方法,那么就来执行代理类InvocationHandler 类的 invock(),接下来再看看 Plugin 类中 invocke() 干了什么事情.

  • 10 Plugin#invoke() 方法干了什么事情
@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)) {
      // 很明显,直接去调用实现类的 intercept 方法,并且传入参数 Invocation 对象,这不就是自定义拦截器实现Interceptor接口的intercept 方法吗???
      return interceptor.intercept(new Invocation(target, method, args));
    }
    return method.invoke(target, args);
  } catch (Exception e) {
    throw ExceptionUtil.unwrapThrowable(e);
  }
}

恍然大悟,原来插件也可以这么玩儿

总结
  • mybatis 插件的工作原理其实就是根据动态代理,代理一个被拦截的类,然后在使用到被代理的拦截的方法的时候,会调用动态代理中 InvocationHandlerinvoke 方法,invoke方法再调用实现了Interceptor接口的 intercept 方法,即可完成插件拦截的效果
  • 本篇博客是博主通过学习,总结的笔记,如有错误的地方,请联系博主及时修改

本文地址:https://blog.csdn.net/qq_38800175/article/details/108719239

相关标签: mybatis plugin