Mybatis源码分析--Plugin(插件原理)

Mybatis源码分析--Plugin(插件原理)

在开始之前,我们需要先了解,Mybatis插件是什么,能干什么,如何使用,最后我们再一起学习它的实现原理。

是什么?

Mybatis插件本质上来说就是一个拦截器,那它拦截了什么呢?[Myabtis官网](https://mybatis.org/mybatis-3/configuration.html#plugins)告诉我们会拦截以下四种对象。

支持拦截的对象及方法

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)

  • ParameterHandler (getParameterObject, setParameters)

  • ResultSetHandler (handleResultSets, handleOutputParameters)

  • StatementHandler (prepare, parameterize, batch, update, query)

既然是对这四种对象的拦截,那我们就需要先了解这四个对象在Mybatis中扮演的什么角色提供了什么功能。对这些对象的拦截无非就是做一下增强来满足我们自己的需求。就好比平时你都是顺这一条道去学校,突然某天一个人把你拦住问路(插件的角色),问完路,你还得接着去学校(原始逻辑)

下面这张图展示了Mybatis完整的执行流程以及各个组件之间的依赖关系。

Mybatis执行流程

下面简单对这四大对象提供的功能进行介绍,如想深入了解内部原理,笔者也贴出了对应的博客地址有需要的可以深入学习,一起探讨。

拦截对象提供功能笔者相关博客
ExecutorMybatis的执行器,用于执行增删改查操作,提供了对应APIMybatis源码解析-Execotor
StatementHandler是对JDBC的Statement处理逻辑封装,默认分别持有DefaultParamterHandler和DefaultResultSetHandlerMybatis源码解析-StatementHandler
ParameterHandler用与PrepareStatement和CallbackStatement的参数设置Mybatis源码解析-ParamterHandler
ResultSetHandler用于对结果集(ResultSet)的解析Mybatis源码解析-ResultSetHandler

能干什么?

  • 分页功能:mybatis的分页默认是基于内存分页的(查出所有,再截取),数据量大的情况下效率较低,不过使用mybatis插件可以改变该行为,只需要拦截StatementHandler类的prepare方法,改变要执行的SQL语句为分页语句即可
  • 性能监控:对于SQL语句执行的性能监控,可以通过拦截Executor类的update, query等方法,用日志记录每个方法执行的时间

如何使用

在使用之前,我们先来看看Mybatis提供的插件相关的类,过一遍它们分别提供了哪些功能,最后我们自己定义一个拦截器

用于定义插件的类

前面已经知道Mybatis插件是对Mybatis中四大对象中的方法进行拦截,那拦截器拦截哪个类的哪个方法如何知道,就由下面这个注解提供拦截信息

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {  
  Signature[] value();
}

由于一个拦截器可以同时拦截多个对象的多个方法,所以就使用了`Signture`数组,该注解定义了拦截的完整信息

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
  // 拦截的类
  Class<?> type();
  // 拦截的方法
  String method();
  // 拦截方法的参数    
  Class<?>[] args();
}

已经知道了该拦截哪些对象的哪些方法,拦截后要干什么就需要实现`Intercetor#intercept`方法,在这个方法里面实现拦截后的处理逻辑

public interface Interceptor {
  /**
   * 真正方法被拦截执行的逻辑
   *
   * @param invocation 主要目的是将多个参数进行封装
   */
  Object intercept(Invocation invocation) throws Throwable;
  // 生成目标对象的代理对象
  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }
  // 可以拦截器设置一些属性
  default void setProperties(Properties properties) {
    // NOP
  }
}

自定义拦截器

已经知道了Mybatis插件相关类提供的功能,下面我们就通过它们自定义一个拦截器

@Intercepts({
  @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class MyInterceptor implements Interceptor {
  private String desc;
  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    Object target = invocation.getTarget();
    System.out.println("被拦截的类" + target.getClass().getName());
    Method method = invocation.getMethod();
    System.out.println("被拦截的方法:" + method);
    Object[] args = invocation.getArgs();
    System.out.println("被拦截方法的参数" + args);
    System.out.println("设置的成员属性" + desc);
    return method.invoke(target, args);
  }
​
  @Override
  public Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }
​
  @Override
  public void setProperties(Properties properties) {
    desc = properties.getProperty("desc");
  }
}

Mybatis插件实现原理

插件配置信息的加载

上面我们已经定义好了一个拦截器,那我们怎么告诉Mybatis呢?Mybatis所有的配置都定义在XXx.xml配置文件中

<plugins>
    <plugin interceptor="com.keminapera.mybatis.plugin.MyInterceptor">
        <property name="desc" value="设置的参数"/>
    </plugin>
</plugins>

对应的解析代码如下(XMLConfigBuilder#pluginElement):

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);
      }
    }
  }

主要做了一下工作:

  1. 遍历解析plugins标签下每个plugin标签

  2. 根据解析的类信息创建Interceptor对象

  3. 调用setProperties方法设置属性

  4. 将拦截器添加到Configuration类的IntercrptorChain拦截器链中

对应时序图如下:

代理对象的生成

Executor代理对象(Configuration#newExecutor)

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 = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

ParameterHandler代理对象(Configuration#newParameterHandler)

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
    // 生成ParameterHandler代理对象逻辑 
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
    return parameterHandler;
  }

ResultSetHandler代理对象(Configuration#newResultSetHandler)

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 = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
    return resultSetHandler;
  }

StatementHandler代理对象(Configuration#newStatementHandler)

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 = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
  }

通过查看源码会发现,所有代理对象的生成都是通过InterceptorChain#pluginAll方法来创建的,进一步查看pluginAll方法

public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
        target = interceptor.plugin(target);
    }
    return target;
}

InterceptorChain#pluginAll内部通过遍历Interceptor#plugin方法来创建代理对象,并将生成的代理对象又赋值给target,如果存在多个拦截器的话,生成的代理对象会被另一个代理对象所代理,从而形成一个代理链,执行的时候,依次执行所有拦截器的拦截逻辑代码,我们再跟进去

default Object plugin(Object target) {
  return Plugin.wrap(target, this);
}

Interceptor#plugin方法最终将目标对象和当前的拦截器交给Plugin.wrap方法来创建代理对象。该方法是默认方法,是Mybatis框架提供的一个典型plugin方法的实现。让我们看看在Plugin#wrap方法中是如何实现代理对象的

public static Object wrap(Object target, Interceptor interceptor) {
    // 1.解析该拦截器所拦截的所有接口及对应拦截接口的方法
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    // 2.获取目标对象实现的所有被拦截的接口
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    // 3.目标对象有实现被拦截的接口,生成代理对象并返回
    if (interfaces.length > 0) {
        // 通过JDK动态代理的方式实现
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    // 目标对象没有实现被拦截的接口,直接返回原对象
    return target;
  }

最终我们看到其实是通过JDK提供的Proxy.newProxyInstance方法来生成代理对象

以上代理对象生成过程的时序图如下

拦截逻辑的执行

通过上面的分析,我们知道Mybatis框架中执行Executor、ParameterHandler、ResultSetHandler和StatementHandler中的方法时真正执行的是代理对象对应的方法。而且该代理对象是通过JDK动态代理生成的,所以执行方法时实际上是调用InvocationHandler#invoke方法(Plugin类实现InvocationHandler接口),下面是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)) {
        return interceptor.intercept(new Invocation(target, method, args));
      }
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值