MyBatis源码学习三:mybatis插件

一、mybatis插件的实现

1. 实现步骤

1.实现Interceptor接口,重写对应方法,主要是Intercept()和setProperties()方法
2.在子类中采用@Intercepts注解,标识要拦截的类和方法
3.在mybatis-config.xml中配置Plugins标签

以pagehelper插件为例来说明:

@Intercepts(
        {
                @Signature(type = Executor.class,method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
                @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
        }
)
public class PageInterceptor implements Interceptor {
    // ... 省略

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        try {
            Object[] args = invocation.getArgs();
            MappedStatement ms = (MappedStatement) args[0];
            Object parameter = args[1];
            RowBounds rowBounds = (RowBounds) args[2];
            ResultHandler resultHandler = (ResultHandler) args[3];
            Executor executor = (Executor) invocation.getTarget();
            CacheKey cacheKey;
            BoundSql boundSql;
            //由于逻辑关系,只会进入一次
            if (args.length == 4) {
                //4 个参数时
                boundSql = ms.getBoundSql(parameter);
                cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
            } else {
                //6 个参数时
                cacheKey = (CacheKey) args[4];
                boundSql = (BoundSql) args[5];
            }
            checkDialectExists();

            List resultList;
            //调用方法判断是否需要进行分页,如果不需要,直接返回结果
            if (!dialect.skip(ms, parameter, rowBounds)) {
                //判断是否需要进行 count 查询
                if (dialect.beforeCount(ms, parameter, rowBounds)) {
                    //查询总数
                    Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                    //处理查询总数,返回 true 时继续分页查询,false 时直接返回
                    if (!dialect.afterCount(count, parameter, rowBounds)) {
                        //当查询总数为 0 时,直接返回空的结果
                        return dialect.afterPage(new ArrayList(), parameter, rowBounds);
                    }
                }
                resultList = ExecutorUtil.pageQuery(dialect, executor,
                        ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
            } else {
                //rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页
                resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
            }
            return dialect.afterPage(resultList, parameter, rowBounds);
        } finally {
            if(dialect != null){
                dialect.afterAll();
            }
        }
    }

// ...省略
}

PageInterceptor 拦截了Executor类的query的两个重载方法。

2. mybatis中可以拦截的类

mybatis的插件支持拦截的类有四个,如下:

parameterHandler
ResultSetHandler
StatementHandler
Executor

从mybatis的源码可知,主要在Configuration类中对这几个对象实例化方法中看出,如下:

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

3. 实现原理

  1. 在解析xml配置文件的时候,将plugins标签中对应的实现类解析处理放入到interceptorChain集合中。
private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
      // 遍历 plugin
      for (XNode child : parent.getChildren()) {
        // 获取interceptor属性值
        String interceptor = child.getStringAttribute("interceptor");
        // 获取所有子节点属性值
        Properties properties = child.getChildrenAsProperties();
        // 实例化Interceptor
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
        // 将对象属性赋值
        interceptorInstance.setProperties(properties);
        // 加入到Configuration对象的拦截器链中
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }
  1. 在实例化SqlSession时,会实例化Executor,实例化Executor主要就是通过Configuration中的newExecutor方法,如下:
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      // 实例化Executor
      final Executor executor = configuration.newExecutor(tx, execType);
      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();
    }
  }
  1. newExecutor方法中会通过拦截器链的pluginAll方法,该方法会遍历所有的拦截器,然后匹配是否有拦截当前exector的拦截器类,如果有会使用动态代理返回一个executor的代理对象。
// class: InterceptorChain
public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }
// plugin方法中主要调用了Plugin对象的wrap方法
public static Object wrap(Object target, Interceptor interceptor) {
	// 获取拦截器的签名类和方法方法入到集合中,类对象为key, 方法结合为value
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    // target:当前的目标对象,比如现在是CacheExecutor, 获取她的类对象
    Class<?> type = target.getClass();
    // 通过该方法获取到target实现的接口集合,该方法会通过signatrueMap的key去匹配是否有type的接口
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
      // 通过动态代理生成一个target的代理对象
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

  1. 生成了代理对象之后,所有的方法都会通过Plugin的invoke的处理,如下:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      // 如果有方法所属的类有拦截器,统一通过拦截器的intercept处理
      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. 大致的流程图

MyBatis源码学习三:mybatis插件

MyBatis源码学习三:mybatis插件MyBatis源码学习三:mybatis插件 Mamball 发布了4 篇原创文章 · 获赞 6 · 访问量 3002 私信 关注
上一篇:Android在TextView中设置局部文字的样式(HTML和SpannableString两种方式)


下一篇:【java框架ssm-mybatis】使用mybait自定义拦截器实现分页功能