mybatis攔截器注冊(cè)初始化編寫示例及如何生效詳解
Mybatis支持四種類型的攔截器
這一點(diǎn)可以從Mybatis的初始化類Configuration.java中得到驗(yàn)證(源碼體不貼出了,改天分析Mybatis初始化過(guò)程的時(shí)候詳細(xì)說(shuō))。具體包括:
- ParameterHandler攔截器
- ResultSetHandler攔截器
- StatementHandler攔截器
- Executor攔截器
四種攔截器分別有各自不同的用途,當(dāng)我們熟悉Mybatis的運(yùn)行機(jī)制之后,理解起來(lái)就相對(duì)容易一些。
目前,如果我們對(duì)Mybatis還不是很了解的話,也沒有關(guān)系,不影響我們對(duì)Mybatis的攔截器做初步的了解。
我們不需要一次性對(duì)四種類型的攔截器都了解,因?yàn)樗麄兊墓ぷ鳈C(jī)制及底層原理大致相同。
我們今天以Executor攔截器為切入點(diǎn),了解Mybatis攔截器的實(shí)現(xiàn)方法、以及初步分析其實(shí)現(xiàn)原理。
今天的目標(biāo)是:用Mybatis攔截器技術(shù),計(jì)算每一句sql語(yǔ)句的執(zhí)行時(shí)長(zhǎng),并在控制臺(tái)打印出來(lái)具體的sql語(yǔ)句及參數(shù)。
在此過(guò)程中,我們會(huì)了解:
- 編寫Mybatis攔截器。
- Mybatis攔截器注冊(cè)。
- Mybatis攔截器的初始化過(guò)程。
- Mybatis攔截器是如何生效的。
準(zhǔn)備工作
Springboot項(xiàng)目,并引入Mybatis,pom文件加入依賴:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>然后配置數(shù)據(jù)庫(kù)訪問(wèn)、建表、創(chuàng)建mapper.xml文件及mapper對(duì)象,在mapper.xml中寫一個(gè)簡(jiǎn)單的獲取數(shù)據(jù)的sql、使用mapper對(duì)象通過(guò)該sql語(yǔ)句獲取數(shù)據(jù)。
今天文章的主要目標(biāo)是攔截器,所以以上關(guān)于通過(guò)Mybatis獲取數(shù)據(jù)庫(kù)數(shù)據(jù)的代碼就不貼出了。
編寫攔截器
Mybatis攔截器是AOP的一個(gè)具體實(shí)現(xiàn),我們前面文章分析過(guò)AOP的實(shí)現(xiàn)原理其實(shí)就是動(dòng)態(tài)代理,java實(shí)現(xiàn)動(dòng)態(tài)代理有兩種方式:cglib和java原生(我們前面有一篇文章專門分析過(guò)兩者的區(qū)別),Mybatis攔截器是通過(guò)java原生的方式實(shí)現(xiàn)的。
其實(shí)我們實(shí)現(xiàn)的攔截器在java原生動(dòng)態(tài)代理的框架中屬于回調(diào)對(duì)象的一部分,回調(diào)對(duì)象其實(shí)是Plugin,Plugin對(duì)象持有Interceptor,Plugin的invoke方法才是JDK動(dòng)態(tài)代理中的那個(gè)回調(diào)方法、其中會(huì)調(diào)用Interceptor的intercept方法,所以Plugin的invoke方法其實(shí)又類似于一個(gè)模板方法(這部分后面會(huì)有具體分析)。
所以Mybatis都已經(jīng)替我們安排好了,我們的攔截器只需要實(shí)現(xiàn)這個(gè)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;
}
}要實(shí)現(xiàn)的目標(biāo)都在上面這段代碼中,一目了然。
需要解釋以下幾點(diǎn):
- @Intercepts注解:目的是為了告訴Mybatis當(dāng)前攔截器的類型(開篇說(shuō)的四種類型之一)、攔截方法名以及方法參數(shù)。
- Invocation:攔截器被調(diào)用的時(shí)候組裝起來(lái)的一個(gè)包裝對(duì)象,包含了被代理對(duì)象(原對(duì)象)、被代理的方法、以及方法調(diào)用參數(shù)等。
- 通過(guò)Invocation.proceed()執(zhí)行被代理對(duì)象的原方法,所以在該方法前、后可以添加我們自己的增強(qiáng)功能,比如計(jì)算sql語(yǔ)句執(zhí)行時(shí)長(zhǎng)就是在方法執(zhí)行前、后分別獲取系統(tǒng)時(shí)間并計(jì)算時(shí)間差即可。
- Executor有兩個(gè)query方法,我們需要清楚地知道應(yīng)用最終會(huì)調(diào)用Executor的哪個(gè)query方法,否則如果匹配不上的話就不會(huì)執(zhí)行攔截。當(dāng)然,我們也可以對(duì)多個(gè)方法執(zhí)行攔截。
- invocation.getArgs()[0]獲取到的是被代理方法的第一個(gè)參數(shù),以此類推......可以獲取到被代理方法的所有參數(shù),所以在攔截器中可以有完整的被代理方法的執(zhí)行現(xiàn)場(chǎng),能做到一個(gè)攔截器理論上能做的任何事情。
好了,攔截器代碼我們就完成了。
攔截器的注冊(cè)
攔截器編寫完成后,需要注冊(cè)到Mybatis的InterceptorChain中才能生效。
我們可以看到Mybatis的攔截器又是一個(gè)chain的概念,所以我們是可以實(shí)現(xiàn)多個(gè)攔截器,每一個(gè)攔截器各自實(shí)現(xiàn)自己的目標(biāo)的。
可以通過(guò)以下幾種方式實(shí)現(xiàn)攔截器的注冊(cè):
- 在mybatis.xml文件中通過(guò)plugins標(biāo)簽配置
- 通過(guò)配置類,創(chuàng)建ConfigurationCustomizer類實(shí)現(xiàn)customize方法
- Spring項(xiàng)目中將攔截器注冊(cè)到Spring Ioc容器中
我們當(dāng)前是基于Springboot的項(xiàng)目,所以上面代碼中已經(jīng)加了@Component注解,通過(guò)第3種方式完成注冊(cè),簡(jiǎn)單方便。
運(yùn)行
攔截器準(zhǔn)備好了,啟動(dòng)項(xiàng)目,隨便跑一個(gè)數(shù)據(jù)查詢的方法:

可以看到攔截器已經(jīng)可以正常工作了。
上面我們已經(jīng)實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的Executor攔截器,下面我們要花點(diǎn)時(shí)間分析一下這個(gè)攔截器是怎么生效的。
攔截器的初始化
在尚未對(duì)Mybatis的初始化過(guò)程進(jìn)行整體分析的情況下,想要徹底搞清楚攔截器的初始化過(guò)程多少有點(diǎn)困難,但是如果我們只看Mybatis初始化過(guò)程中與攔截器有關(guān)的部分的話,也不是不可以。
Mybatis初始化的過(guò)程中會(huì)通過(guò)SqlSessionFatoryBuilder創(chuàng)建SqlSessionFactory,SqlSessionFactory會(huì)持有Configuration對(duì)象。
而我們前面所說(shuō)的注冊(cè)Mybatis攔截器,不論以什么樣的方式進(jìn)行注冊(cè),其目的無(wú)非就是要讓Mybatis啟動(dòng)、初始化的過(guò)程中,將攔截器注冊(cè)到Configuration對(duì)象中。
比如我們上面所說(shuō)的任何一種注冊(cè)方式,最終SqlSessionFactoryBean都會(huì)將攔截器獲取到plugins屬性中,在buildSqlSessionFactory()方法中將攔截器注冊(cè)到Configuration對(duì)象中:
if (!isEmpty(this.plugins)) {
Stream.of(this.plugins).forEach(plugin -> {
targetConfiguration.addInterceptor(plugin);
LOGGER.debug(() -> "Registered plugin: '" + plugin + "'");
});
}
// 省略代碼
return this.sqlSessionFactoryBuilder.build(targetConfiguration);最后調(diào)用SqlSessionFactoryBuilder的build方法創(chuàng)建SqlSessionFactory,我們從源碼可以看到最終創(chuàng)建了DefaultSqlSessionFactory,并且將Configuration對(duì)象以參數(shù)的形式傳遞過(guò)去:
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}而DefaultSqlSessionFactory會(huì)持有該Configuration對(duì)象:
public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}所以,Mybatis初始化的過(guò)程中會(huì)獲取到我們注冊(cè)的攔截器,該攔截器會(huì)注冊(cè)到Configuration對(duì)象中,最終,SqlSesscionFactory對(duì)象會(huì)持有Configuration對(duì)象,從而持有該攔截器。
攔截器是如何生效的#openSession
那我們現(xiàn)在看一下,已經(jīng)完成初始化的攔截器最終是如何生效的。
我們知道一條數(shù)據(jù)庫(kù)操作語(yǔ)句的執(zhí)行首先是要調(diào)用SqlSesscionFactory的openSession來(lái)獲取sqlSession開始的。
上面我們已經(jīng)看到初始化過(guò)程中創(chuàng)建的是DefaultSqlSessionFactory,所以我們直接看DefaultSqlSessionFactory的openSession方法。
最終會(huì)調(diào)用到openSessionFromDataSource或openSessionFromConnection,兩個(gè)方法的結(jié)構(gòu)差不太多,但是具體細(xì)節(jié)的區(qū)分今天就不做分析了。我們直接看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();
}
}關(guān)注的重點(diǎn)放在final Executor executor = configuration.newExecutor(tx, execType)上,我們?nèi)タ匆幌翪onfiguraton的這個(gè)方法:
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后,調(diào)用interceptorChain.pluginAll,該方法逐個(gè)調(diào)用攔截器的plugin方法,攔截器的plugin方法調(diào)用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;
}最終通過(guò)動(dòng)態(tài)代理的方式,返回該對(duì)象的一個(gè)代理對(duì)象,回調(diào)對(duì)象為持有原對(duì)象、攔截器、攔截方法簽名的Plugin對(duì)象。
所以我們知道,openSession最終創(chuàng)建的DefaultSqlSession所持有的Executor其實(shí)是已經(jīng)被攔截器處理過(guò)的代理對(duì)象。
根據(jù)我們對(duì)JDK代理的理解,最終Executor的方法被調(diào)用的時(shí)候,其實(shí)是要回調(diào)這個(gè)代理對(duì)象創(chuàng)建的時(shí)候的回調(diào)器的invoke方法的,也就是Plugin的invoke方法。
攔截器是如何生效的#Executor執(zhí)行
上面一節(jié)分析了openSession過(guò)程中,Executor代理對(duì)象是如何被創(chuàng)建的。
接下來(lái)看一下具體的Executor的執(zhí)行,本例攔截的是他的query方法。其實(shí)我們已經(jīng)知道query方法執(zhí)行的時(shí)候是要調(diào)用Plugin的invoke方法的。
代碼其實(shí)比較簡(jiǎn)單:
@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);
}
}獲取到當(dāng)前Executor對(duì)象的所有注冊(cè)的攔截方法,比較當(dāng)前調(diào)用的方法是否為攔截方法,是的話就調(diào)用攔截器的intercept方法......就是我們自己編寫的攔截器的攔截方法。否則如果當(dāng)前方法沒有配置攔截的話就調(diào)用原方法。
調(diào)用攔截器的攔截方法的時(shí)候,創(chuàng)建了一個(gè)持有被代理對(duì)象target、攔截方法、攔截方法的調(diào)用參數(shù)...等數(shù)據(jù)的Invocation對(duì)象作為參數(shù)傳進(jìn)去。這也就是為什么我們?cè)跀r截器方法中能獲取到這些數(shù)據(jù)的原因。
OK...還差一點(diǎn),就是如果配置了多個(gè)代理器的話,調(diào)用順序的問(wèn)題。其實(shí)整體比較起來(lái),Mybatis的源碼感覺比Spring的簡(jiǎn)單了許多,攔截器注冊(cè)之后在InterceptorChain也就是保存在ArrayList中,所以他本身應(yīng)該是沒有順序的,想要控制調(diào)用順序應(yīng)該還得想其他辦法。
以上就是mybatis攔截器注冊(cè)初始化編寫示例及如何生效詳解的詳細(xì)內(nèi)容,更多關(guān)于mybatis攔截器的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
實(shí)例解析Java關(guān)于static的作用
只要是有學(xué)過(guò)Java的都一定知道static,也一定能多多少少說(shuō)出一些作用和注意事項(xiàng)。文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04
Java?HashMap中除了死循環(huán)之外的那些問(wèn)題
這篇文章主要介紹了Java?HashMap中除了死循環(huán)之外的那些問(wèn)題,這些問(wèn)題大致可以分為兩類,程序問(wèn)題和業(yè)務(wù)問(wèn)題,下面文章我們一個(gè)一個(gè)來(lái)看,需要的小伙伴可以參考一下2022-05-05
詳解Spring AOP自定義可重復(fù)注解沒有生效問(wèn)題
本文主要介紹了Spring AOP自定義可重復(fù)注解沒有生效問(wèn)題,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-08-08
Java中StringBuilder字符串類型的操作方法及API整理
Java中的StringBuffer類繼承于AbstractStringBuilder,用來(lái)創(chuàng)建非線程安全的字符串類型對(duì)象,下面即是對(duì)Java中StringBuilder字符串類型的操作方法及API整理2016-05-05
java控制臺(tái)實(shí)現(xiàn)學(xué)生信息管理系統(tǒng)(集合版)
這篇文章主要為大家詳細(xì)介紹了java控制臺(tái)實(shí)現(xiàn)學(xué)生信息管理系統(tǒng)的集合版,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-04-04
Jmeter生成UUID作為唯一標(biāo)識(shí)符過(guò)程圖解
這篇文章主要介紹了Jmeter生成UUID作為唯一標(biāo)識(shí)符過(guò)程圖解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-08-08

