Mybatis支持四种类型的拦截器,这一点可以从Mybatis的初始化类Configuration.java中得到验证(源码体不贴出了,改天分析Mybatis初始化过程的时候详细说)。具体包括:
- ParameterHandler拦截器
- ResultSetHandler拦截器
- StatementHandler拦截器
- Executor拦截器
四种拦截器分别有各自不同的用途,当我们熟悉Mybatis的运行机制之后,理解起来就相对容易一些。
目前,如果我们对Mybatis还不是很了解的话,也没有关系,不影响我们对Mybatis的拦截器做初步的了解。
我们不需要一次性对四种类型的拦截器都了解,因为他们的工作机制及底层原理大致相同。
我们今天以Executor拦截器为切入点,了解Mybatis拦截器的实现方法、以及初步分析其实现原理。
今天的目标是:用Mybatis拦截器技术,计算每一句sql语句的执行时长,并在控制台打印出来具体的sql语句及参数。
在此过程中,我们会了解:
- 编写Mybatis拦截器。
- Mybatis拦截器注册。
- Mybatis拦截器的初始化过程。
- Mybatis拦截器是如何生效的。
准备工作
Springboot项目,并引入Mybatis,pom文件加入依赖:
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.1.3
然后配置数据库访问、建表、创建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;
}
}
要实现的目标都在上面这段代码中,一目了然。
需要解释以下几点:
- @Intercepts注解:目的是为了告诉Mybatis当前拦截器的类型(开篇说的四种类型之一)、拦截方法名以及方法参数。
- Invocation:拦截器被调用的时候组装起来的一个包装对象,包含了被代理对象(原对象)、被代理的方法、以及方法调用参数等。
- 通过Invocation.proceed()执行被代理对象的原方法,所以在该方法前、后可以添加我们自己的增强功能,比如计算sql语句执行时长就是在方法执行前、后分别获取系统时间并计算时间差即可。
- Executor有两个query方法,我们需要清楚地知道应用最终会调用Executor的哪个query方法,否则如果匹配不上的话就不会执行拦截。当然,我们也可以对多个方法执行拦截。
- invocation.getArgs()[0]获取到的是被代理方法的第一个参数,以此类推......可以获取到被代理方法的所有参数,所以在拦截器中可以有完整的被代理方法的执行现场,能做到一个拦截器理论上能做的任何事情。
好了,拦截器代码我们就完成了。
拦截器的注册
拦截器编写完成后,需要注册到Mybatis的InterceptorChain中才能生效。
我们可以看到Mybatis的拦截器又是一个chain的概念,所以我们是可以实现多个拦截器,每一个拦截器各自实现自己的目标的。
可以通过以下几种方式实现拦截器的注册:
- 在mybatis.xml文件中通过plugins标签配置
- 通过配置类,创建ConfigurationCustomizer类实现customize方法
- 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, Set> 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 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中,所以他本身应该是没有顺序的,想要控制调用顺序应该还得想其他办法。