11.插件

本文详细介绍了如何在Mybatis中使用插件机制,特别是创建了一个自动分页插件,拦截StatementHandler的prepare方法,并演示了如何检测分页条件、获取总行数并修改SQL。关键步骤包括设置分页参数、执行子查询获取总行数以及动态调整SQL。
摘要由CSDN通过智能技术生成

1.Mybatis插件概述

插件机制是为了对MyBatis现有体系进行扩展 而提供的入口。底层通过动态代理实现。可供代理拦截的接口有四个:

  1. Executor:执行器
  2. StatementHandler:JDBC处理器
  3. ParameterHandler:参数处理器
  4. ResultSetHandler:结果集处理器

这四个接口已经涵盖从发起接口调用到SQl声明、参数处理、结果集处理的全部流程。接口中任何一个方法都可以进行拦截改变方法原有属性和行为。不过这是一个非常危险的行为,稍不注意就会破坏MyBatis核心逻辑还不自知。所以在在使用插件之前一定要非常清晰MyBatis内部机制。

2.插件的使用

创建一个插件在MyBatis当中是一件非常简单的事情 ,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。

@Intercepts({@Signature(
  type= Executor.class,
  method = "update",
  args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
    
    // 当执行目标方法时会被方法拦截
    public Object intercept(Invocation invocation) throws Throwable {
      long begin = System.currentTimeMillis();
        try {
            // 继续执行原逻辑;
            return invocation.proceed();
        } finally {
            System.out.println("执行时间:"+(System.currentTimeMillis() - begin));
        }
    }
    
    // 生成代理对象,可自定义生成代理对象,这样就无需配置@Intercepts注解。
    // 另外需要自行判断是否为拦截目标接口。
    public Object plugin(Object target) {
        // 调用通用插件代理生成机器
        return Plugin.wrap(target,this);
    }
}

在config.xml 中添加插件配置

<plugins>
  <plugin interceptor="org.mybatis.example.ExamplePlugin"/>
</plugins>

通过上述配置即可以监控 在执行过修改过程当中,所耗费的时间。

注:只有从外部类调用拦截目标时拦截才会生效,如果在内部调用代理逻辑会失效,这与JDK动态代理有关。方法内部再次调用query()方法时,实际上调用的是this.query(),并不会走到代理对象里面。

如在Executor中有两个Query 方法,第一个会调用第二个query。如果你拦截的是第二个Query 则不会成功。

image-20211024174426734

3.插件代理机制

Configuration 中有一个InterceptorChain(拦截链)保存了所有拦截器,当创建四大对象之后就会调用拦截链,对目标对象进行拦截代理。

image-20211024212837630

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
  ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
  parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
  return parameterHandler;
}

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

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

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

这里使用的时候都是用动态代理将多个插件用责任链的方式添加的,最后返回的是一个代理对象; 其责任链的添加过程如下:

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

最终动态代理生成和调用的过程都在 Plugin 类中:

public static Object wrap(Object target, Interceptor interceptor) {
  // 获取签名Map
  Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); 
  // 拦截目标 (ParameterHandler|ResultSetHandler|StatementHandler|Executor)
  Class<?> type = target.getClass();
  // 获取目标接口  
  Class<?>[] interfaces = getAllInterfaces(type, signatureMap);  
  if (interfaces.length > 0) {
    // 生成代理
    return Proxy.newProxyInstance(
        type.getClassLoader(),
        interfaces,
        new Plugin(target, interceptor, signatureMap));
  }
  return target;
}

private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    // Intercepts注解
    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();
    // interceptor拦截哪些接口及方法
    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;
}

在调用接口中的方法时:

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

4.自动分页插件

自动分页是指查询时,指定页码和大小 等参数,插件就自动进行分页查询,并返回总数量。

4.1 拦截目标

接下来要解决的问题,是插件的入口写在哪里?去拦截的目标有哪些?

  • Executor:执行器

  • StementHandler:JDBC处理器

  • ParameterHandler:参数处理器

  • ResultSetHandler:结果集处理器

参数处理器和结果集处理器显然不合适,而Executor.query() 又需要额外考虑 一、二级缓存逻辑。最后还是选定StatementHandler,并拦截其prepare 方法。

@Intercepts(@Signature(type = StatementHandler.class,
        method = "prepare", args = {Connection.class,
        Integer.class}))

4.2 分页插件原理

首先设定一个Page类,其包含total、size、index 3个属性,在Mapper接口中声明该参数即表示需要执行自动分页逻辑。

总体实现步骤包含3个:

  1. 检测是否满足分页条件
  2. 自动求出当前查询的总行数
  3. 修改原有的SQL语句 ,添加 limit offset 关键字。

4.2.1 检测是否满足分页条件

分页条件是

  • 是否为查询方法,

  • 查询参数中是否带上Page参数。

在intercept 方法中可直接获得拦截目标StatementHandler ,通过它又可以获得BoundSql 里面就包含了SQL和参数。遍历参数即可获得Page。

// 带上分页参数
StatementHandler target = (StatementHandler) invocation.getTarget();
// SQL包 sql、参数、参数映射
BoundSql boundSql = target.getBoundSql();
Object parameterObject = boundSql.getParameterObject();
Page page = null;
if (parameterObject instanceof Page) {
    page = (Page) parameterObject;
} else if (parameterObject instanceof Map) {
    page = (Page) ((Map) parameterObject).values().stream().filter(v -> v instanceof Page).findFirst().orElse(null);
}

4.2.2 查询总行数

实现逻辑是 将原查询SQL作为子查询进行包装成子查询,然后用原有参数,还是能过原来的参数处理器进行赋值。关于执行是采用JDBC 原生API实现。MyBatis执行器。

private int selectCount(Invocation invocation) throws SQLException {
    int count = 0;
    StatementHandler target = (StatementHandler) invocation.getTarget();
    // SQL包 sql、参数、参数映射
    String countSql = String.format("select count(*) from (%s) as _page", target.getBoundSql().getSql());
    // JDBC
    Connection connection = (Connection) invocation.getArgs()[0];
    PreparedStatement preparedStatement = connection.prepareStatement(countSql);
    target.getParameterHandler().setParameters(preparedStatement);
    ResultSet resultSet = preparedStatement.executeQuery();
    if (resultSet.next()) {
        count = resultSet.getInt(1);
    }
    resultSet.close();
    preparedStatement.close();

    return count;
}

4.2.3 修改原有SQL,加上分页参数

String newSql= String.format("%s limit %s offset %s", boundSql.getSql(),page.getSize(),page.getOffset());
SystemMetaObject.forObject(boundSql).setValue("sql",newSql);
 preparedStatement.close();

    return count;
}

4.2.3 修改原有SQL,加上分页参数

String newSql= String.format("%s limit %s offset %s", boundSql.getSql(),page.getSize(),page.getOffset());
SystemMetaObject.forObject(boundSql).setValue("sql",newSql);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值