mybatis攔截器注冊初始化編寫示例及如何生效詳解
Mybatis支持四種類型的攔截器
這一點(diǎn)可以從Mybatis的初始化類Configuration.java中得到驗(yàn)證(源碼體不貼出了,改天分析Mybatis初始化過程的時(shí)候詳細(xì)說)。具體包括:
- ParameterHandler攔截器
- ResultSetHandler攔截器
- StatementHandler攔截器
- Executor攔截器
四種攔截器分別有各自不同的用途,當(dāng)我們熟悉Mybatis的運(yùn)行機(jī)制之后,理解起來就相對容易一些。
目前,如果我們對Mybatis還不是很了解的話,也沒有關(guān)系,不影響我們對Mybatis的攔截器做初步的了解。
我們不需要一次性對四種類型的攔截器都了解,因?yàn)樗麄兊墓ぷ鳈C(jī)制及底層原理大致相同。
我們今天以Executor攔截器為切入點(diǎn),了解Mybatis攔截器的實(shí)現(xiàn)方法、以及初步分析其實(shí)現(xiàn)原理。
今天的目標(biāo)是:用Mybatis攔截器技術(shù),計(jì)算每一句sql語句的執(zhí)行時(shí)長,并在控制臺(tái)打印出來具體的sql語句及參數(shù)。
在此過程中,我們會(huì)了解:
- 編寫Mybatis攔截器。
- Mybatis攔截器注冊。
- Mybatis攔截器的初始化過程。
- 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ù)庫訪問、建表、創(chuàng)建mapper.xml文件及mapper對象,在mapper.xml中寫一個(gè)簡單的獲取數(shù)據(jù)的sql、使用mapper對象通過該sql語句獲取數(shù)據(jù)。
今天文章的主要目標(biāo)是攔截器,所以以上關(guān)于通過Mybatis獲取數(shù)據(jù)庫數(shù)據(jù)的代碼就不貼出了。
編寫攔截器
Mybatis攔截器是AOP的一個(gè)具體實(shí)現(xiàn),我們前面文章分析過AOP的實(shí)現(xiàn)原理其實(shí)就是動(dòng)態(tài)代理,java實(shí)現(xiàn)動(dòng)態(tài)代理有兩種方式:cglib和java原生(我們前面有一篇文章專門分析過兩者的區(qū)別),Mybatis攔截器是通過java原生的方式實(shí)現(xiàn)的。
其實(shí)我們實(shí)現(xiàn)的攔截器在java原生動(dòng)態(tài)代理的框架中屬于回調(diào)對象的一部分,回調(diào)對象其實(shí)是Plugin,Plugin對象持有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)前攔截器的類型(開篇說的四種類型之一)、攔截方法名以及方法參數(shù)。
- Invocation:攔截器被調(diào)用的時(shí)候組裝起來的一個(gè)包裝對象,包含了被代理對象(原對象)、被代理的方法、以及方法調(diào)用參數(shù)等。
- 通過Invocation.proceed()執(zhí)行被代理對象的原方法,所以在該方法前、后可以添加我們自己的增強(qiáng)功能,比如計(jì)算sql語句執(zhí)行時(shí)長就是在方法執(zhí)行前、后分別獲取系統(tǒng)時(shí)間并計(jì)算時(shí)間差即可。
- Executor有兩個(gè)query方法,我們需要清楚地知道應(yīng)用最終會(huì)調(diào)用Executor的哪個(gè)query方法,否則如果匹配不上的話就不會(huì)執(zhí)行攔截。當(dāng)然,我們也可以對多個(gè)方法執(zhí)行攔截。
- invocation.getArgs()[0]獲取到的是被代理方法的第一個(gè)參數(shù),以此類推......可以獲取到被代理方法的所有參數(shù),所以在攔截器中可以有完整的被代理方法的執(zhí)行現(xiàn)場,能做到一個(gè)攔截器理論上能做的任何事情。
好了,攔截器代碼我們就完成了。
攔截器的注冊
攔截器編寫完成后,需要注冊到Mybatis的InterceptorChain中才能生效。
我們可以看到Mybatis的攔截器又是一個(gè)chain的概念,所以我們是可以實(shí)現(xiàn)多個(gè)攔截器,每一個(gè)攔截器各自實(shí)現(xiàn)自己的目標(biāo)的。
可以通過以下幾種方式實(shí)現(xiàn)攔截器的注冊:
- 在mybatis.xml文件中通過plugins標(biāo)簽配置
- 通過配置類,創(chuàng)建ConfigurationCustomizer類實(shí)現(xiàn)customize方法
- Spring項(xiàng)目中將攔截器注冊到Spring Ioc容器中
我們當(dāng)前是基于Springboot的項(xiàng)目,所以上面代碼中已經(jīng)加了@Component注解,通過第3種方式完成注冊,簡單方便。
運(yùn)行
攔截器準(zhǔn)備好了,啟動(dòng)項(xiàng)目,隨便跑一個(gè)數(shù)據(jù)查詢的方法:
可以看到攔截器已經(jīng)可以正常工作了。
上面我們已經(jīng)實(shí)現(xiàn)了一個(gè)簡單的Executor攔截器,下面我們要花點(diǎn)時(shí)間分析一下這個(gè)攔截器是怎么生效的。
攔截器的初始化
在尚未對Mybatis的初始化過程進(jìn)行整體分析的情況下,想要徹底搞清楚攔截器的初始化過程多少有點(diǎn)困難,但是如果我們只看Mybatis初始化過程中與攔截器有關(guān)的部分的話,也不是不可以。
Mybatis初始化的過程中會(huì)通過SqlSessionFatoryBuilder創(chuàng)建SqlSessionFactory,SqlSessionFactory會(huì)持有Configuration對象。
而我們前面所說的注冊Mybatis攔截器,不論以什么樣的方式進(jìn)行注冊,其目的無非就是要讓Mybatis啟動(dòng)、初始化的過程中,將攔截器注冊到Configuration對象中。
比如我們上面所說的任何一種注冊方式,最終SqlSessionFactoryBean都會(huì)將攔截器獲取到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);
最后調(diào)用SqlSessionFactoryBuilder的build方法創(chuàng)建SqlSessionFactory,我們從源碼可以看到最終創(chuàng)建了DefaultSqlSessionFactory,并且將Configuration對象以參數(shù)的形式傳遞過去:
public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config); }
而DefaultSqlSessionFactory會(huì)持有該Configuration對象:
public DefaultSqlSessionFactory(Configuration configuration) { this.configuration = configuration; }
所以,Mybatis初始化的過程中會(huì)獲取到我們注冊的攔截器,該攔截器會(huì)注冊到Configuration對象中,最終,SqlSesscionFactory對象會(huì)持有Configuration對象,從而持有該攔截器。
攔截器是如何生效的#openSession
那我們現(xiàn)在看一下,已經(jīng)完成初始化的攔截器最終是如何生效的。
我們知道一條數(shù)據(jù)庫操作語句的執(zhí)行首先是要調(diào)用SqlSesscionFactory的openSession來獲取sqlSession開始的。
上面我們已經(jīng)看到初始化過程中創(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; }
最終通過動(dòng)態(tài)代理的方式,返回該對象的一個(gè)代理對象,回調(diào)對象為持有原對象、攔截器、攔截方法簽名的Plugin對象。
所以我們知道,openSession最終創(chuàng)建的DefaultSqlSession所持有的Executor其實(shí)是已經(jīng)被攔截器處理過的代理對象。
根據(jù)我們對JDK代理的理解,最終Executor的方法被調(diào)用的時(shí)候,其實(shí)是要回調(diào)這個(gè)代理對象創(chuàng)建的時(shí)候的回調(diào)器的invoke方法的,也就是Plugin的invoke方法。
攔截器是如何生效的#Executor執(zhí)行
上面一節(jié)分析了openSession過程中,Executor代理對象是如何被創(chuàng)建的。
接下來看一下具體的Executor的執(zhí)行,本例攔截的是他的query方法。其實(shí)我們已經(jīng)知道query方法執(zhí)行的時(shí)候是要調(diào)用Plugin的invoke方法的。
代碼其實(shí)比較簡單:
@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對象的所有注冊的攔截方法,比較當(dāng)前調(diào)用的方法是否為攔截方法,是的話就調(diào)用攔截器的intercept方法......就是我們自己編寫的攔截器的攔截方法。否則如果當(dāng)前方法沒有配置攔截的話就調(diào)用原方法。
調(diào)用攔截器的攔截方法的時(shí)候,創(chuàng)建了一個(gè)持有被代理對象target、攔截方法、攔截方法的調(diào)用參數(shù)...等數(shù)據(jù)的Invocation對象作為參數(shù)傳進(jìn)去。這也就是為什么我們在攔截器方法中能獲取到這些數(shù)據(jù)的原因。
OK...還差一點(diǎn),就是如果配置了多個(gè)代理器的話,調(diào)用順序的問題。其實(shí)整體比較起來,Mybatis的源碼感覺比Spring的簡單了許多,攔截器注冊之后在InterceptorChain也就是保存在ArrayList中,所以他本身應(yīng)該是沒有順序的,想要控制調(diào)用順序應(yīng)該還得想其他辦法。
以上就是mybatis攔截器注冊初始化編寫示例及如何生效詳解的詳細(xì)內(nèi)容,更多關(guān)于mybatis攔截器的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
實(shí)例解析Java關(guān)于static的作用
只要是有學(xué)過Java的都一定知道static,也一定能多多少少說出一些作用和注意事項(xiàng)。文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04Java?HashMap中除了死循環(huán)之外的那些問題
這篇文章主要介紹了Java?HashMap中除了死循環(huán)之外的那些問題,這些問題大致可以分為兩類,程序問題和業(yè)務(wù)問題,下面文章我們一個(gè)一個(gè)來看,需要的小伙伴可以參考一下2022-05-05詳解Spring AOP自定義可重復(fù)注解沒有生效問題
本文主要介紹了Spring AOP自定義可重復(fù)注解沒有生效問題,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-08-08Java中StringBuilder字符串類型的操作方法及API整理
Java中的StringBuffer類繼承于AbstractStringBuilder,用來創(chuàng)建非線程安全的字符串類型對象,下面即是對Java中StringBuilder字符串類型的操作方法及API整理2016-05-05java控制臺(tái)實(shí)現(xiàn)學(xué)生信息管理系統(tǒng)(集合版)
這篇文章主要為大家詳細(xì)介紹了java控制臺(tái)實(shí)現(xiàn)學(xué)生信息管理系統(tǒng)的集合版,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-04-04Jmeter生成UUID作為唯一標(biāo)識(shí)符過程圖解
這篇文章主要介紹了Jmeter生成UUID作為唯一標(biāo)識(shí)符過程圖解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-08-08