关于java:mybatis拦截器

4次阅读

共计 7016 个字符,预计需要花费 18 分钟才能阅读完成。

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 种形式实现注册,简略不便。

运行

拦截器筹备好了,启动我的项目,轻易跑一个数据查问的办法:

能够看到拦截器曾经能够失常工作了。

下面咱们曾经实现了一个简略的 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

正文完
 0