浅析Mybatis拦截器

一、背景

最近针对项目中出现的慢sql,我们使用自定义Mybatis拦截器,结合配置中心动态配置慢sql阈值,来监控慢sql并报警,提前发现风险点。借着这个契机,浅析下Mybatis拦截器原理,个人理解,不足之处请指正。

二、Mybatis拦截器

Mybatis使用plugin来拦截方法调用,所以MyBatis plugin也称为:Mybatis拦截器。Mybatis采用责任链模式,通过代理组织多个plugin,对Executor、ParameterHandler、StatementHandler、ResultSetHandler中的方法进行拦截。

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

本文使用mybatis版本为3.4.5。

2.1 使用Mybatis拦截器

2.1.1 自定义Plugin

Signature定义具体要拦截的方法信息,type为拦截的对象,method为拦截对象的方法名,考虑到重载的方法,args为拦截对象的方法参数。

@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}),
        @Signature(
                type = Executor.class,
                method = "update",
                args = {MappedStatement.class, Object.class})
})
public class ExamplePlugin implements Interceptor {

    /**
     * 具体拦截逻辑
     * @param invocation
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // implement pre-processing if needed
        Object result = invocation.proceed();
        // implement post-processing if needed
        return result;
    }

    /**
     * 生成代理对象
     * @param target
     * @return
     */
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    /**
     * 注册插件时xml配置的property转化,设置一些自定义属性
     * @param properties
     */
    @Override
    public void setProperties(Properties properties) {

    }
}

2.1.2 注册Plugin

在mybatis-config.xml中注册插件。

<plugins>
  <plugin interceptor="xx.ExamplePlugin">
    <property name="xxxProperty" value="xxx"/>
  </plugin>
</plugins>

2.1.3 绑定SqlSessionFactory

<bean id="xxxSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="xxxDataSource"/>
        <property name="configLocation" value="classpath:xxx/mybatis-config.xml"/>
</bean>

2.2 源码分析

在Spring容器对SqlSessionFactory初始化时,会解析mybatis-config.xml,注册Interceptor。

/** 
 * org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration
 * 解析mybatis-config.xml
 */
private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
}
/** 
 * org.apache.ibatis.builder.xml.XMLConfigBuilder#pluginElement
 * 解析mybatis-config.xml中的plugins元素
 */
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).newInstance();
        interceptorInstance.setProperties(properties);
        configuration.addInterceptor(interceptorInstance);
      }
    }
}
/**
 * org.apache.ibatis.session.Configuration#addInterceptor
 * 将Interceptor添加到interceptorChain中
 */
public void addInterceptor(Interceptor interceptor) {
    interceptorChain.addInterceptor(interceptor);
}

2.2.1 生成代理流程

这里以ExamplePlugin为例,对Executor阶段的方法进行代理,跟踪一下流程。

①步

/**
 * org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSessionFromDataSource
 */
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();
    }
}

 

第②步

/**
 * org.apache.ibatis.session.Configuration#newExecutor(org.apache.ibatis.transaction.Transaction, org.apache.ibatis.session.ExecutorType)
 */
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor 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);
    }
    //处理注册的plugin
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

第③步

/**
 * org.apache.ibatis.plugin.InterceptorChain#pluginAll
 * 如果同一个拦截的方法存在多个拦截器,会按照声明的顺序,循环进行代理。
 */
public Object pluginAll(Object target) {
    //循环处理每个注册的plugin,构成一条代理链。
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
}

前面说了会对Executor、ParameterHandler、StatementHandler、ResultSetHandler中的方法进行拦截,从这一步的引用可以论证,这4个阶段都会通过plugin生成代理对象。

第④步

/**
 * ExamplePlugin#plugin
 * 调用自定义拦截器的plugin方法
 * @param target
 * @return
 */
 @Override
 public Object plugin(Object target) {
      return Plugin.wrap(target, this);
 }

第⑤⑥⑦步

/**
 * org.apache.ibatis.plugin.Plugin#wrap
 */
public static Object wrap(Object target, Interceptor interceptor) {
    //反射获取拦截方法信息
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    //原始对象类信息
    Class<?> type = target.getClass();
    //获取可以生成代理的接口数组
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    //判断是否需要生成代理
    if (interfaces.length > 0) {
      //生成代理对象,Plugin对象为该代理对象的InvocationHandler
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
}
/**
 * org.apache.ibatis.plugin.Plugin#getSignatureMap
 */
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    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();
    //key是拦截的类,value是拦截的方法集合
    Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
    for (Signature sig : sigs) {
      Set<Method> methods = signatureMap.get(sig.type());
      if (methods == null) {
        methods = new HashSet<Method>();
        signatureMap.put(sig.type(), methods);
      }
      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;
}

/**
 * org.apache.ibatis.plugin.Plugin#getAllInterfaces
 */
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
    Set<Class<?>> interfaces = new HashSet<Class<?>>();
    while (type != null) {
      //遍历原始对象类的接口信息
      for (Class<?> c : type.getInterfaces()) {
        //判断拦截配置是否存在的匹配的类型
        if (signatureMap.containsKey(c)) {
          interfaces.add(c);
        }
      }
      //以父类进行匹配,一直匹配到java.lang.Object的父类,为null结束循环
      type = type.getSuperclass();
    }
    return interfaces.toArray(new Class<?>[interfaces.size()]);
}

2.2.2 拦截流程

这里以一个单条查询sql跟踪一下拦截流程。

 

第①步
/**
 * org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object, org.apache.ibatis.session.RowBounds)
 */
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      //此时的executor是代理后的executor
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
}
第②步
/**
 * org.apache.ibatis.plugin.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)) {
        //调用自定义plugin实现的intercept方法
        return interceptor.intercept(new Invocation(target, method, args));
      }
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
}
第③步
/**
 * ExamplePlugin#intercept
 * 具体拦截逻辑
 * 如果同一个拦截的方法存在多个拦截器,由于生成层层代理时是顺序生成,拦截时正好相反。
 * @param invocation
 * @return
 * @throws Throwable
 */
@Override
public Object intercept(Invocation invocation) throws Throwable {
     // implement pre-processing if needed
     //调用代理对象的目标类
     Object result = invocation.proceed();
     // implement post-processing if needed
     return result;
}

2.3 应用场景

1.慢sql监控(拦截Executor对象的query、update方法)
2.分库分表场景主键ID替换(拦截StatementHandler对象的prepare方法)
3.分页查询(拦截Executor对象的query方法)

2.4 优化

在生成代理流程时序图中的第⑥步会进行反射判断是否需要生成代理,这一步可以前置到第④步进行判断,减少反射。

/**
 * 生成代理对象
 * @param target
 * @return
 */
@Override
public Object plugin(Object target) {
    //提前判断是否要生成代理对象
    if (target != null && target instanceof Executor) {
         return Plugin.wrap(target, this);
    }
    return target;
}

三、总结思考

通过自定义plugin,自己也对mybatis拦截器的实现机制有了清晰的认识,希望这篇文章也能帮助到读者。

四、参考文档

https://mybatis.org/mybatis-3/configuration.html#plugins

posted on 2024-06-17 17:13  zhengbiyu  阅读(314)  评论(0编辑  收藏  举报