Mybatis插件原理与开发

插件原理与开发

Mybatis总体执行流程 一文中简单的介绍了插件的初始化过程,本文将从源码的角度介绍一下mybatis的插件原理与简单开发实战。

插件原理

插件的注册和管理是通过InterceptorChain进行的,在创建Executor、StatementHandler、ParameterHandler、ResultSetHandler对象时,会执行InterceptorChain的pluginAll方法

  public Object pluginAll(Object target) {
    // 遍历所有的插件
    for (Interceptor interceptor : interceptors) {
      // 执行插件的plugin方法,返回代理对象
      target = interceptor.plugin(target);
    }
    return target;
  }

拦截的原理,正是此时返回的代理对象,当调用目标方法时,执行的就是拦截器的intercept方法,从而实现拦截功能。

      // 执行插件的plugin方法,返回代理对象
      target = interceptor.plugin(target);

来到Interceptor接口的plugin方法:

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

这是一个默认方法,一般不会重写它的逻辑。看其实现Plugin#wrap:

  public static Object wrap(Object target, Interceptor interceptor) {
    // 拿到拦截器的@Intercepts注解信息:key是要拦截的接口,value是要拦截的接口方法集合
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    // 这里的target,就是拦截的对象(Executor、StatementHandler、ParameterHandler、ResultSetHandler对象)
    Class<?> type = target.getClass();
    // 返回包含在signatureMap中的接口
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    // 存在被拦截的接口,返回一个代理对象
    if (interfaces.length > 0) {
      // 利用jdk动态代理生成代理对象:关注Plugin(实现了InvocationHandler接口)的invoke方法
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    // 接口没有被拦截,返回原始对象
    return target;
  }

可以看到,如果接口被拦截了,就会利用JDK动态代理生成代理对象,由于Plugin实现了InvocationHandler接口,所以其invoke方法会被执行:

  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方法,并将目标对象、方法、参数传入
        return interceptor.intercept(new Invocation(target, method, args));
      }
      // 否则直接执行原始方法
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }

插件开发

自定义插件需要:

  1. 实现Interceptor接口,重写intercept方法

  2. 使用@Intercepts和@Signature注解表明需要拦截哪些类的哪些方法

  3. 在配置文件中,添加插件配置

mybatis官网中,对此也有所描述:mybatis – MyBatis 3 | Configuration

根据官网描述,mybatis插件可以拦截的方法如下:

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

以下是我写的一个记录SQL及其耗时的拦截器,加深对拦截器的理解:

/**
 * @Author: qiuxinfa
 * @CreateTime: 2023-12-07  22:15
 * @Description: 自定义拦截器:打印SQL、统计SQL执行时间
 */
@Intercepts({
        @Signature(type = StatementHandler.class,method = "batch",args = {Statement.class}),
        @Signature(type = StatementHandler.class,method = "update",args = {Statement.class}),
        @Signature(type = StatementHandler.class,method = "query",args = {Statement.class, ResultHandler.class}),
})
public class SqlLogPlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取执行的SQL
        String sql;
        Statement statement=(Statement) invocation.getArgs()[0];
        if(Proxy.isProxyClass(statement.getClass())){
            MetaObject metaObject= SystemMetaObject.forObject(statement);
            Object h = metaObject.getValue("h");
            if(h instanceof StatementLogger){
                RoutingStatementHandler rsh=(RoutingStatementHandler) invocation.getTarget();
                sql = rsh.getBoundSql().getSql();
            }else {
                PreparedStatementLogger psl=(PreparedStatementLogger) h;
                sql = psl.getPreparedStatement().toString();
            }
        }else{
            sql = statement.toString();
        }
        // 记录开始时间
        long start = System.currentTimeMillis();
        // 执行目标方法
        Object result = invocation.proceed();
        // 记录结束时间
        long end = System.currentTimeMillis();
        System.err.println("执行SQL ===> ");
        System.err.println(sql);
        System.err.println("统计SQL耗时 = " + (end - start) + "毫秒");
        System.err.println("返回结果 =======> " + result);
        return result;
    }
}

配置文件添加插件:

    <!-- 配置插件    -->
    <plugins>
        <plugin interceptor="com.qxf.plugin.SqlLogPlugin"></plugin>
    </plugins>

配置之后,会打印执行的SQL语句及其耗时。

  • 8
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
MyBatis 是一个持久层框架,它提供了丰富的 SQL 映射配置和执行功能,但是在处理大量数据时,如果没有分页机制,就会带来性能问题。 MyBatis 分页插件就是为了解决这个问题而被开发出来的,它可以通过拦截 Executor 中的 query 方法,实现 SQL 语句的自动分页,并将分页后的结果返回给调用方。 下面是 MyBatis 分页插件原理: 1. 定义分页插件 首先,我们需要定义一个分页插件,这个插件需要实现 Interceptor 接口,并且重写其 intercept 方法,该方法会在 Executor 中的 query 方法被调用时被触发。 2. 拦截 query 方法 在 intercept 方法中,我们需要拦截 Executor 中的 query 方法,并获取其中的参数和 SQL 语句。 3. 自动分页 在获取到参数和 SQL 语句后,我们需要对 SQL 语句进行处理,将其转化为分页 SQL 语句,这里需要根据不同的数据库类型来进行处理。 4. 执行 SQL 语句 处理完分页 SQL 语句后,我们需要调用 Executor 中的 query 方法来执行 SQL 语句,并获取分页后的结果。 5. 将结果返回给调用方 最后,我们需要将分页后的结果返回给调用方,这里需要注意一点,即在调用方使用分页插件时,需要在 SQL 语句中加入类似 “limit 0,10” 这样的分页参数,这样才能正确的分页。 总的来说,MyBatis 分页插件原理就是通过拦截 Executor 中的 query 方法,将 SQL 语句转化为分页 SQL 语句,并执行该语句,最后将分页后的结果返回给调用方。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值