首页 > 编程语言 >【Mybatis】【插件】Mybatis源码解析-插件机制

【Mybatis】【插件】Mybatis源码解析-插件机制

时间:2023-03-09 20:45:48浏览次数:50  
标签:插件 target Object 源码 Executor Mybatis 方法 method

1  前言

这节我们来看看插件,插件是来干啥的呢?一般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者自行拓展。这样的 好处是显而易见的,一是增加了框架的灵活性。二是开发者可以结合实际需求,对框架进行 拓展,使其能够更好的工作。以 MyBatis 为例,我们可基于 MyBatis 插件机制实现分页、 分表,监控等功能。那么我们这节就来看看 Mybatis 里的插件。

2  插件类型

MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。 如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。 因为在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。 这些都是更底层的类和方法,所以使用插件的时候要特别当心。

3  插件的简单使用

我们简单来个插件的示例:

@Intercepts({@Signature(
  type= Executor.class,
  method = "update",
  args = {MappedStatement.class, Object.class})})
public class MyInterceptor implements Interceptor {

  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    System.out.println("===================my");
    return invocation.proceed();
  }
}
<plugins>
  <plugin interceptor="org.apache.ibatis.test.plugin.MyInterceptor"></plugin>
</plugins>

这样 MyBatis 在启动时可以加载插件, 并保存插件实例到相关对象(InterceptorChain,拦截器链)中。待准备工作做完后,MyBatis 处于就绪状态。我们在执行 SQL 时,需要先通过 DefaultSqlSessionFactory 创 建 SqlSession 。Executor 实例会在创建 SqlSession 的过程中被创建,Executor 实例创建完毕 后,MyBatis 会通过 JDK 动态代理为实例生成代理类。这样,插件逻辑即可在 Executor 相 关方法被调用前执行。以上就是 MyBatis 插件机制的基本原理。接下来,我们来看一下原 理背后对应的源码是怎样的。

4  源码分析

4.1  入场时机

本节,我将以 Executor 为例,分析 MyBatis 是如何为 Executor 实例植入插件逻辑的。 Executor 实例是在开启 SqlSession 时被创建的,因此,下面我们从源头进行分析。先来看 一下 SqlSession 开启的过程:

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
  Transaction tx = null;
  try {
    ...
    // 创建我们的执行器 进入这里创建执行器
    final Executor executor = configuration.newExecutor(tx, execType);
    // 创建 SqlSession 对象
    return new DefaultSqlSession(configuration, executor, autoCommit);
  } catch (Exception e) {
    closeTransaction(tx); // may have fetched a connection so lets call close()
    throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

Executor 的创建过程封装在 Configuration 中,我们跟进去:

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
  ...
  if (cacheEnabled) {
    executor = new CachingExecutor(executor);
  }
  // 插件
  executor = (Executor) interceptorChain.pluginAll(executor);
  return executor;
}

如上,newExecutor 方法在创建好 Executor 实例后,紧接着通过拦截器链 interceptorChain 为 Executor 实例植入代理逻辑。那下面我们看一下 InterceptorChain 的代码是怎样的。

public class InterceptorChain {

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

  public Object pluginAll(Object target) {
    // 遍历我们的插件拦截器
    for (Interceptor interceptor : interceptors) {
      // 调用拦截器的 plugin 方法进行插件的创建 其实就是会创建代理类
      target = interceptor.plugin(target);
    }
    return target;
  }

  // 添加插件
  public void addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
  }

  // 获取插件列表
  public List<Interceptor> getInterceptors() {
    return Collections.unmodifiableList(interceptors);
  }

}

以上是 InterceptorChain 的全部代码,比较简单。它的 pluginAll 方法会调用具体插件的 plugin 方法植入相应的插件逻辑。如果有多个插件,则会多次调用 plugin 方法,最终生成一 个层层嵌套的代理类。那我们就来看下 plugin方法是如何创建代理的。

4.2  plugin 决定是否创建代理

plugin 方法是由具体的插件类实现,不过该方法代码一般比较固定,我们看一下:

// Interceptor 默认实现
default Object plugin(Object target) {
  // 调用 wrap 方法进行包装
  return Plugin.wrap(target, this);
}
/**
 * Plugin 生成代理
 * 这个代理可能会嵌套生成,越后嵌套越先执行奥
 * @param target 目标对象
 * @param interceptor 插件拦截
 * @return 代理对象或者目标对象
 */
public static Object wrap(Object target, Interceptor interceptor) {
  // 解析插件拦截器的信息
  Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
  // 当前被拦截对象的类型
  Class<?> type = target.getClass();
  /**
   * 判断当前的被拦截对象的类型是否存在拦截
   * 比如当前的对象是 statementHandler的 而当前的拦截器是配置的 Executor -> [xxx]
   * 那么当前方法就无需这个插件
   */
  Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
  // 当发现需要插件增强的话,创建当前对象的代理类 jdk方式的
  if (interfaces.length > 0) {
    return Proxy.newProxyInstance(
        type.getClassLoader(),
        interfaces,
        // 注意这个这是我们的增强 也就是拦截会执行 Plugin 中的 invoke
        new Plugin(target, interceptor, signatureMap));
  }
  // 不需要的话 直接返回当前对象
  return target;
}

如上,plugin 方法在内部调用了 Plugin 类的 wrap 方法,用于为目标对象生成代理。Plugin 类实现了InvocationHandler接口,因此它可以作为参数传给 Proxy 的 newProxyInstance方法。

我们再来简单看下 getSignatureMap 方法就是解析我们的插件类获取信息的,我们看下:

/**
 * 我们来拿一个例子给你解析
 * @Intercepts({@Signature(
 *   type= Executor.class,
 *   method = "update",
 *   args = {MappedStatement.class, Object.class})})
 *   那么返回的就是 Executor -> [update方法]
 * @param interceptor 就是我们的插件实现类
 * @return 返回类型和方法集合的映射
 */
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());
  }
  // 获取到 @Signature 数组
  Signature[] sigs = interceptsAnnotation.value();
  /**
   * 用于保存结果 相同类的话会放进同一个 key 中
   * key 哪种插件类型  value 就是拦截的方法列表
   */
  Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
  for (Signature sig : sigs) {
    // 初始化 key 的集合
    Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
    try {
      // 根据拦截类型 type 以及参数列表获取到对应的拦截方法
      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;
}

4.3  插件执行逻辑

Plugin 实现了 InvocationHandler 接口,因此它的 invoke 方法会拦截所有的方法调用。 invoke 方法会对所拦截的方法进行检测,以决定是否执行插件逻辑。该方法的逻辑如下:

// Plugin 当代理对象执行方法的时候,会代理执行到此
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
    /**
     * 获取当前要执行的方法来源于哪个类并获取是否存在方法增强
     * 比如当前要执行的是 Executor 的 query方法 而当前拦截器的类型方法映射为 Executor -> [update]
     * 那么 query 就不在增强范围内,所以就会直接执行目标方法
     */
    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);
  }
}

invoke 方法的代码比较少,逻辑不难理解。首先,invoke 方法会检测被拦截方法是否配 置在插件的 @Signature 注解中,若是,则执行插件逻辑,否则执行被拦截方法。插件逻辑 封装在 intercept 中,该方法的参数类型为 Invocation。Invocation 主要用于存储目标类,方法 以及方法参数列表。下面简单看一下该类的定义。

public class Invocation {
  // 被代理对象
  private final Object target;
  // 当前拦截的方法
  private final Method method;
  // 方法参数
  private final Object[] args;
  public Invocation(Object target, Method method, Object[] args) {
    // 初始化
    this.target = target;
    this.method = method;
    this.args = args;
  }
  public Object getTarget() {
    return target;
  }
  public Method getMethod() {
    return method;
  }
  public Object[] getArgs() {
    return args;
  }
  public Object proceed() throws InvocationTargetException, IllegalAccessException {
    // 下一个拦截器执行或者目标方法执行
    return method.invoke(target, args);
  }
}

关于插件的机制就到这里了,他的代理记住是一个套一个的逐层执行的哈。

5  小结

这节我们大致看了一下插件的入场以及插件的实现机制,有理解不对的地方欢迎指正哈。

标签:插件,target,Object,源码,Executor,Mybatis,方法,method
From: https://www.cnblogs.com/kukuxjx/p/17201223.html

相关文章

  • 关于在SSM项目中使用mybatis-plus时控制台出现was not registered for synchronizatio
    1.出现这个问题可能会出现事务不同步的问题导致无法进行数据库的连接。可以在service层中添加@Tranational注解2.这里我解决是添加配置文件log4j的配置文件查看是数据库连......
  • MyBatis
    一、什么是MyBatisMyBatis是一款优秀的持久层框架,用于简化JDBC开发MyBatis本是Apache的一个开源项目iBatis,2010年这个项目由apachesoftwarefoundation......
  • Mybatis 快速入门
    要使用MyBatis,只需将 mybatis-x.x.x.jar 文件置于类路径(classpath)中即可。如果使用Maven来构建项目,则需将下面的依赖代码置于pom.xml文件中:<dependency><gro......
  • SpringBoot使用Mybatis-plus 报错:‘sqlSessionFactory‘ or ‘sqlSessionTemplate‘不
    查看pom.xml中mybatis-plus配置<dependency><groupId>com.baomidou</groupId><artifactId>mybatisplus-spring-boot-starter</artifactId>......
  • MyBatis简介
    什么是MyBatis?MyBatis是一款优秀的持久层框架,用于简化JDBC开发MyBatis本是Apache的一个开源项目iBatis, 2010年这个项目由apache softwarefoundation迁移到了google c......
  • React Hooks源码深度解析
    作者:京东零售郑炳懿前言ReactHooks是React16.8引入的一个新特性,它允许函数组件中使用state和其他React特性,而不必使用类组件。Hooks是一个非常重要的概念,因为它们提......
  • eslint 插件扩展设置
    {  "liveServer.settings.donotShowInfoMsg":true,  "editor.unicodeHighlight.allowedCharacters":{    " ":true  },  "editor.rende......
  • React Hooks源码深度解析
    作者:京东零售郑炳懿前言​​ReactHooks​​是​​React​​16.8引入的一个新特性,它允许函数组件中使用​​state​​和其他React特性,而不必使用类组件。​​Hooks​​......
  • 随堂笔记13-spring之aop底层源码
    动态代理:代理模式:为其他对象提供一种代理来控制对这个对象的访问,增强一个类中的某个方法,对其进行扩展调用分为俩类,一类是jdk的接口代理,需要有接口,另一种是cglib代......
  • Jenkins 使用Send files or execute commands over SSH 插件时,报ERROR: Exception whe
    1.检查Jenkins系统配置下的PublishoverSSH是否连接正确2.配置构建任务,构建步骤中,设置开启Verboseoutputinconsole(控制台中详细输出)这样方便查看具体错误信息......