MyBatis框架原理4:插件

插件的定義和作用

首先引用MyBatis文檔對插件(plugins)的定義:

MyBatis 允許你在已映射語句執行過程中的某一點進行攔截調用。默認情況下,MyBatis 允許使用插件來攔截的方法調用包括:

Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)

ParameterHandler (getParameterObject, setParameters)

ResultSetHandler (handleResultSets, handleOutputParameters)

StatementHandler (prepare, parameterize, batch, update, query)

這些類中方法的細節可以通過查看每個方法的簽名來發現,或者直接查看 MyBatis 發行包中的源代碼。 如果你想做的不僅僅是監控方法的調用,那么你最好相當了解要重寫的方法的行為。 因為如果在試圖修改或重寫已有方法的行為的時候,你很可能在破壞 MyBatis 的核心模塊。 這些都是更低層的類和方法,所以使用插件的時候要特別當心。

Mybatis插件所攔截的4個對象正是在之前的MyBatis框架原理2:SqlSession運行過程文章中介紹的4個實現核心功能的接口。那么插件攔截這4個接口能做什么呢?根據之前文章對4個接口的介紹,可以猜測到:

Executor是SqlSession整個執行過程的總指揮,同時還對緩存進行操作,通過插件可以使用自定義的緩存,比如mybatis-enhanced-cache插件。

StatementHandler負責SQL的編譯和執行,通過插件可以改寫SQL語句。

ParameterHandler負責SQL的參數設置,通過插件可以改變參數設置。

ResultSetHandler負責結果集映射和存儲過程輸出參數的組裝,通過插件可以對結果集映射規則改寫。

插件的原理

在理解插件原理之前,得先搞清楚以下三個概念:

動態代理

代理模式是一種給真實對象提供一個代理對象,并由代理對象控制對真實對象的引用的一種設計模式,動態代理是在程序運行時動態生成代理類的模式,JDK動態代理對象是由java提供的一個Proxy類和InvocationHandler接口以及一個真實對象的接口生成的。通常InvocationHandler的實現類持有一個真實對象字段和定義一個invoke方法,通過Proxy類的newProxyInstance方法就可以生成這個真實對象的代理對象,通過代理對象調度方法實際就是調用InvocationHandler實現類的invoke方法,在invoke方法中可以通過反射實現調用真實對象的方法。

攔截器(Interceptor)

動態代理對象可以對真實對象方法引用,是因為InvocationHandler實現類持有了一個真實對象的字段,通過反射就可以實現這個功能。如果InvocationHandler實現類再持有一個Interceptor接口的實現類,Interceptor接口定義了一個入參為真實對象的intercept方法,Interceptor接口的實現類通過重寫intercept方法可以對真實對象的方法引用或者實現增強功能等等,也就是當我們再次使用這個動態代理對象調度方法時,可以根據需求對真實對象的方法做出改變。

從這個Interceptor接口實現類的功能上來看,可以叫做真實對象方法的攔截器。于是我們再想一下,如果前面講到MyBatis的4個核心功能接口的實現類(比如PreparedStatementHandler)是一個真實對象,我們通過JDK動態代理技術生成一個代理對象,并且生成代理類所需的InvocationHandler實現類同時還持有了一個Interceptor接口實現類,通過使用代理對象調度方法,我們就可以根據需求對PreparedStatementHandler的功能進行增強。

實際上MyBatis確實提供了這樣一個Interceptor接口和intercept方法,也提供了這樣的一個InvocationHandler接口的實現類,類名叫Plugin,它們都位于MyBatis的org.apache.ibatis.plugin包下。等等,那么MyBatis的插件不就是攔截器嗎?攔截器的原理都講完了,等下還怎么講什么插件原理?


責任鏈模式

我們通過JDK動態代理技術生成一個代理對象,代理的真實對象是個StatementHandler,并且持有StatementHandler的攔截器(插件)。如果我們把這個代理對象視為一個target對象,再利用動態代理生成一個代理類,并且持有對這個target對象的攔截器(插件),如果再把新生成的代理視為一個新的target類,同樣持有對新target類的攔截器(插件),那么我們就得到了一個像是被包裹了三層攔截器(插件)的StatementHandler的代理對象:

當MyBatis每一次SqlSession會話需要引用到StatementHandler的方法時,如過符合上圖中攔截器3的攔截邏輯,則按攔截器3的定義的方法執行;如果不符合攔截邏輯,則將執行責任交給攔截器2處理,以此類推,這樣的模式叫做責任鏈模式。MyBatis全局配置文件里可以配置多個插件,多個插件的運行就是按照這樣的責任鏈模式執行的。

通過對以上三點的理解,我們已經對MyBatis插件原理已經有了初步認識,下面就通過源碼看看MyBatis插件是如何運行起來的。

插件的運行過程

插件的接口

MyBatis提供了一個Interceptor接口,插件必須實現這個接口,接口定義了3個方法如下:

publicinterfaceInterceptor{// 執行插件實現的方法,Invocation對象持有真實對象,可通過反射調用真實對象的方法Objectintercept(Invocation invocation)throwsThrowable;// 設置插件攔截的對象target,通常調用Pulgin類的wrap方法生成一個代理類Objectplugin(Object target);// 根據配置文件初始化插件voidsetProperties(Properties properties);}

插件的初始化

在MyBatis初始化時XMLConfigBuilderder的pluginElement方法對插件配置文件解析:

privatevoidpluginElement(XNode parent)throwsException{if(parent !=null) {for(XNode child : parent.getChildren()) {? ? ? String interceptor = child.getStringAttribute("interceptor");? ? ? Properties properties = child.getChildrenAsProperties();// 通過反射生成插件的實例Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();// 調用插件配置參數interceptorInstance.setProperties(properties);// 將插件實例保存到Configuration對象中configuration.addInterceptor(interceptorInstance);? ? }? }}

Configuration對象最終將解析出的插件配置保存在持有InterceptorChain對象中,InterceptorChain對象又是通過一個ArrayList來保存所有插件,可見在MyBatis初始化的時候插件配置就已經加載好了,運行時就會根據插件編寫的規則執行攔截邏輯。

publicclassInterceptorChain{// 通過集合來保存插件privatefinalList interceptors =newArrayList();// 通過責任鏈模式調用插件plugin方法生成代理對象publicObjectpluginAll(Object target){for(Interceptor interceptor : interceptors) {? ? ? ? ? ? target = interceptor.plugin(target);? ? ? ? ? }returntarget;? }//? Configuration對象調用的添加插件的方法publicvoidaddInterceptor(Interceptor interceptor){? ? ? interceptors.add(interceptor);? }publicListgetInterceptors(){returnCollections.unmodifiableList(interceptors);? }}

插件的運行

如果我們需要攔截MyBatis的Executor接口,Configuration在初始化Executor時就會通過責任鏈模式將初始化的Executor作為真實對象,調用InterceptorChain的pluginAll放法生成代理對象:

publicExecutornewExecutor(Transaction transaction, ExecutorType executorType){executorType = executorType ==null? defaultExecutorType : executorType;executorType = executorType ==null? ExecutorType.SIMPLE : executorType;Executor executor;// 根據配置文件生成相應Executorif(ExecutorType.BATCH == executorType) {? executor =newBatchExecutor(this, transaction);}elseif(ExecutorType.REUSE == executorType) {? executor =newReuseExecutor(this, transaction);}else{? executor =newSimpleExecutor(this, transaction);}if(cacheEnabled) {? executor =newCachingExecutor(executor);}// 調用InterceptorChain的pluginAll放法生成代理對象executor = (Executor) interceptorChain.pluginAll(executor);returnexecutor;}

InterceptorChain的 pluginAll方法調用插件的plugin方法,plugin方法可以調用MyBatis提供的工具類Plugin類來生成代理對象,Plugin類實現了InvocationHandler,在Plugin類中定義invoke方法來實現攔截邏輯和執行插件方法:

publicclassPluginimplementsInvocationHandler{// target為需要攔截的真實對象 privatefinalObject target;// interceptor為插件privatefinalInterceptor interceptor;privatefinalMap, Set> signatureMap;privatePlugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap){this.target = target;this.interceptor = interceptor;this.signatureMap = signatureMap;}publicstaticObjectwrap(Object target, Interceptor interceptor){? Map, Set> signatureMap = getSignatureMap(interceptor);? Class type = target.getClass();? Class[] interfaces = getAllInterfaces(type, signatureMap);// 動態代理生成代理對象并返回if(interfaces.length >0) {returnProxy.newProxyInstance(? ? ? ? type.getClassLoader(),? ? ? ? interfaces,newPlugin(target, interceptor, signatureMap));? }returntarget;}@OverridepublicObjectinvoke(Object proxy, Method method, Object[] args)throwsThrowable{try{? ? Set methods = signatureMap.get(method.getDeclaringClass());// 根據攔截邏輯執行插件的方法if(methods !=null&& methods.contains(method)) {returninterceptor.intercept(newInvocation(target, method, args));? ? }// 直接調用真是對象的方法returnmethod.invoke(target, args);? }catch(Exception e) {throwExceptionUtil.unwrapThrowable(e);? }}...

插件的intercept方法參數為Invocation對象,Invocation對象持有真實對象和一個proceed方法,proceed方法通過反射調用真實對象的方法。于是多個插件生成的責任鏈模式的代理對象,就可以通過一層一層執行proceed方法來調用真實對象的方法。

插件的開發

自己編寫插件必須繼承MyBatis的Interceptor接口

publicinterfaceInterceptor{// 執行插件實現的方法,Invocation對象持有真實對象,可通過反射調用真實對象的方法Objectintercept(Invocation invocation)throwsThrowable;// 設置插件攔截的對象target,通常調用Pulgin類的wrap方法生成一個代理類Objectplugin(Object target);// 根據配置文件初始化插件voidsetProperties(Properties properties);? }

使用@Intercepts和@Signature注解

@Intercepts({@Signature(type = StatementHandler.class, method ="prepare", args = {Connection.class ,Integet.class})})publicclassMyPluginimplementsInterceptor{...}

用@Intercepts注解申明是一個插件,@Signature注解申明攔截的對象,方法和參數。上面的寫法表明了攔截了StatementHandler對象的prepare方法,參數是一個Connection對象和一個Integet。

編寫攔截方法

MyBatis提供了一個Invocation工具類,通常我們將需要攔截的真實對象,方法及參數封裝在里面作為一個參數傳給插件的intercept方法,在插件intercept方法里可以編寫攔截邏輯和執行攔截方法,方法參數invocation可以通過反射調用被代理對象的方法:

@Intercepts({@Signature(type = StatementHandler.class, method ="prepare", args = {Connection.class ,Integet.class})})publicclassMyPluginimplementsInterceptor{@overridepublicObjectintercept(Invocation invocation)throwsThrowable{// do something ...// 調用被代理對象的方法invocation.proceed();// do something ...@override調用Plugin工具類生成代理對象publicObjectplugin(Object target){returnPlugin.wrap(target,this);? ? ? }? ? ? ...? }

生成代理對象

MyBatis還提供了一個Plugin工具類,其中wrap方法用于生成代理類,invoke方法驗證攔截類型和方法,并選擇是否按攔截器的方法,代碼如下:

publicclassPluginimplementsInvocationHandler{privatefinalObject target;// 真實對象privatefinalInterceptor interceptor;// 攔截器(插件)privatefinalMap, Set> signatureMap;// Map保存簽名的類型,方法和參數信息privatePlugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap){this.target = target;this.interceptor = interceptor;this.signatureMap = signatureMap;}publicstaticObjectwrap(Object target, Interceptor interceptor){// getSignatureMap方法通過反射獲取插件里@Intercepts和@Signature注解聲明的攔截類型,方法和參數信息Map, Set> signatureMap = getSignatureMap(interceptor);? Class type = target.getClass();? 從signatureMap中獲取攔截對象的類型? Class[] interfaces = getAllInterfaces(type, signatureMap);// 生成代理對象,如果target的類型不是插件里注解聲明的類型則直接返回target不作攔截。if(interfaces.length >0) {returnProxy.newProxyInstance(? ? ? ? type.getClassLoader(),? ? ? ? interfaces,newPlugin(target, interceptor, signatureMap));? }returntarget;}@OverridepublicObjectinvoke(Object proxy, Method method, Object[] args)throwsThrowable{try{// 驗證代理對象調用的方法是否為插件里申明攔截的方法Set methods = signatureMap.get(method.getDeclaringClass());if(methods !=null&& methods.contains(method)) {// 如果是聲明攔截的方法,則調用插件的intercept方法執行攔截處理returninterceptor.intercept(newInvocation(target, method, args));? ? }// 如果不是聲明攔截的方法,則直接調用真實對象的方法returnmethod.invoke(target, args);? }catch(Exception e) {throwExceptionUtil.unwrapThrowable(e);? }}...

總結

MyBatis插件運行依靠Java動態代理技術實現,雖然原理很簡單,但是編寫插件涉及到修改MyBatis框架底層的接口,需要十分謹慎,做為初學者,最好使用現成的插件。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容