3

Mybatis支持四种类型的拦截器,这一点可以从Mybatis的初始化类Configuration.java中得到验证(源码体不贴出了,改天分析Mybatis初始化过程的时候详细说)。具体包括:

  1. ParameterHandler拦截器
  2. ResultSetHandler拦截器
  3. StatementHandler拦截器
  4. Executor拦截器

四种拦截器分别有各自不同的用途,当我们熟悉Mybatis的运行机制之后,理解起来就相对容易一些。

目前,如果我们对Mybatis还不是很了解的话,也没有关系,不影响我们对Mybatis的拦截器做初步的了解。

我们不需要一次性对四种类型的拦截器都了解,因为他们的工作机制及底层原理大致相同。

我们今天以Executor拦截器为切入点,了解Mybatis拦截器的实现方法、以及初步分析其实现原理。

今天的目标是:用Mybatis拦截器技术,计算每一句sql语句的执行时长,并在控制台打印出来具体的sql语句及参数。

在此过程中,我们会了解:

  1. 编写Mybatis拦截器。
  2. Mybatis拦截器注册。
  3. Mybatis拦截器的初始化过程。
  4. Mybatis拦截器是如何生效的。

准备工作

Springboot项目,并引入Mybatis,pom文件加入依赖:

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
     <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.3</version>
</dependency>

然后配置数据库访问、建表、创建mapper.xml文件及mapper对象,在mapper.xml中写一个简单的获取数据的sql、使用mapper对象通过该sql语句获取数据。

今天文章的主要目标是拦截器,所以以上关于通过Mybatis获取数据库数据的代码就不贴出了。

编写拦截器

Mybatis拦截器是AOP的一个具体实现,我们前面文章分析过AOP的实现原理其实就是动态代理,java实现动态代理有两种方式:cglib和java原生(我们前面有一篇文章专门分析过两者的区别),Mybatis拦截器是通过java原生的方式实现的。

其实我们实现的拦截器在java原生动态代理的框架中属于回调对象的一部分,回调对象其实是Plugin,Plugin对象持有Interceptor,Plugin的invoke方法才是JDK动态代理中的那个回调方法、其中会调用Interceptor的intercept方法,所以Plugin的invoke方法其实又类似于一个模板方法(这部分后面会有具体分析)

所以Mybatis都已经替我们安排好了,我们的拦截器只需要实现这个intercept方法即可。

@Slf4j
@Component
@Intercepts(@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class,
        ResultHandler.class}))
public class myInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
        Object param = invocation.getArgs()[1];
        BoundSql boundSql = ms.getBoundSql(param);
        String sql=boundSql.getSql();
        sql=sql.trim().replaceAll("\\s+", " ");
        log.info("sql:"+ sql);
        log.info("param:" + param);
        long startTime=System.currentTimeMillis();
        Object result=invocation.proceed();
        long endTime=System.currentTimeMillis();
        log.info("sql statement take :"+ (endTime - startTime));
        return result;
    }
}

要实现的目标都在上面这段代码中,一目了然。

需要解释以下几点:

  1. @Intercepts注解:目的是为了告诉Mybatis当前拦截器的类型(开篇说的四种类型之一)、拦截方法名以及方法参数。
  2. Invocation:拦截器被调用的时候组装起来的一个包装对象,包含了被代理对象(原对象)、被代理的方法、以及方法调用参数等。
  3. 通过Invocation.proceed()执行被代理对象的原方法,所以在该方法前、后可以添加我们自己的增强功能,比如计算sql语句执行时长就是在方法执行前、后分别获取系统时间并计算时间差即可。
  4. Executor有两个query方法,我们需要清楚地知道应用最终会调用Executor的哪个query方法,否则如果匹配不上的话就不会执行拦截。当然,我们也可以对多个方法执行拦截。
  5. invocation.getArgs()[0]获取到的是被代理方法的第一个参数,以此类推......可以获取到被代理方法的所有参数,所以在拦截器中可以有完整的被代理方法的执行现场,能做到一个拦截器理论上能做的任何事情。

好了,拦截器代码我们就完成了。

拦截器的注册

拦截器编写完成后,需要注册到Mybatis的InterceptorChain中才能生效。

我们可以看到Mybatis的拦截器又是一个chain的概念,所以我们是可以实现多个拦截器,每一个拦截器各自实现自己的目标的。

可以通过以下几种方式实现拦截器的注册:

  1. 在mybatis.xml文件中通过plugins标签配置
  2. 通过配置类,创建ConfigurationCustomizer类实现customize方法
  3. Spring项目中将拦截器注册到Spring Ioc容器中

我们当前是基于Springboot的项目,所以上面代码中已经加了@Component注解,通过第3种方式完成注册,简单方便。

运行

拦截器准备好了,启动项目,随便跑一个数据查询的方法:

image.png

可以看到拦截器已经可以正常工作了。

上面我们已经实现了一个简单的Executor拦截器,下面我们要花点时间分析一下这个拦截器是怎么生效的。

拦截器的初始化

在尚未对Mybatis的初始化过程进行整体分析的情况下,想要彻底搞清楚拦截器的初始化过程多少有点困难,但是如果我们只看Mybatis初始化过程中与拦截器有关的部分的话,也不是不可以。

Mybatis初始化的过程中会通过SqlSessionFatoryBuilder创建SqlSessionFactory,SqlSessionFactory会持有Configuration对象。

而我们前面所说的注册Mybatis拦截器,不论以什么样的方式进行注册,其目的无非就是要让Mybatis启动、初始化的过程中,将拦截器注册到Configuration对象中。

比如我们上面所说的任何一种注册方式,最终SqlSessionFactoryBean都会将拦截器获取到plugins属性中,在buildSqlSessionFactory()方法中将拦截器注册到Configuration对象中:

if (!isEmpty(this.plugins)) {
      Stream.of(this.plugins).forEach(plugin -> {
        targetConfiguration.addInterceptor(plugin);
        LOGGER.debug(() -> "Registered plugin: '" + plugin + "'");
      });
    }
// 省略代码

    return this.sqlSessionFactoryBuilder.build(targetConfiguration);

最后调用SqlSessionFactoryBuilder的build方法创建SqlSessionFactory,我们从源码可以看到最终创建了DefaultSqlSessionFactory,并且将Configuration对象以参数的形式传递过去:

 public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }

而DefaultSqlSessionFactory会持有该Configuration对象:

 public DefaultSqlSessionFactory(Configuration configuration) {
    this.configuration = configuration;
  }

所以,Mybatis初始化的过程中会获取到我们注册的拦截器,该拦截器会注册到Configuration对象中,最终,SqlSesscionFactory对象会持有Configuration对象,从而持有该拦截器。

拦截器是如何生效的#openSession

那我们现在看一下,已经完成初始化的拦截器最终是如何生效的。

我们知道一条数据库操作语句的执行首先是要调用SqlSesscionFactory的openSession来获取sqlSession开始的。

上面我们已经看到初始化过程中创建的是DefaultSqlSessionFactory,所以我们直接看DefaultSqlSessionFactory的openSession方法。

最终会调用到openSessionFromDataSource或openSessionFromConnection,两个方法的结构差不太多,但是具体细节的区分今天就不做分析了。我们直接看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);
      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();
    }
  }

关注的重点放在final Executor executor = configuration.newExecutor(tx, execType)上,我们去看一下Configuraton的这个方法:

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

方法最后阶段获取到Excutor后,调用interceptorChain.pluginAll,该方法逐个调用拦截器的plugin方法,拦截器的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) {
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

最终通过动态代理的方式,返回该对象的一个代理对象,回调对象为持有原对象、拦截器、拦截方法签名的Plugin对象。

所以我们知道,openSession最终创建的DefaultSqlSession所持有的Executor其实是已经被拦截器处理过的代理对象。

根据我们对JDK代理的理解,最终Executor的方法被调用的时候,其实是要回调这个代理对象创建的时候的回调器的invoke方法的,也就是Plugin的invoke方法。

拦截器是如何生效的#Executor执行

上面一节分析了openSession过程中,Executor代理对象是如何被创建的。

接下来看一下具体的Executor的执行,本例拦截的是他的query方法。其实我们已经知道query方法执行的时候是要调用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);
    }
  }

获取到当前Executor对象的所有注册的拦截方法,比较当前调用的方法是否为拦截方法,是的话就调用拦截器的intercept方法......就是我们自己编写的拦截器的拦截方法。否则如果当前方法没有配置拦截的话就调用原方法。

调用拦截器的拦截方法的时候,创建了一个持有被代理对象target、拦截方法、拦截方法的调用参数...等数据的Invocation对象作为参数传进去。这也就是为什么我们在拦截器方法中能获取到这些数据的原因。

OK...还差一点,就是如果配置了多个代理器的话,调用顺序的问题。其实整体比较起来,Mybatis的源码感觉比Spring的简单了许多,拦截器注册之后在InterceptorChain也就是保存在ArrayList中,所以他本身应该是没有顺序的,想要控制调用顺序应该还得想其他办法。

上一篇 Spring Security + JWT
下一篇 Mybatis缓存机制


31 声望10 粉丝