Mybatis?plugin的使用及原理示例解析
前言
上次,我們說過了mybatis+springboot時的啟動與執(zhí)行流程,也介紹過mybatis的執(zhí)行器和緩存,今天,我們來看看mybatis 的另一個大功能 —— plugin
一、Mybatis Plugin 是什么
MyBatis的plugin插件是用來攔截SQL執(zhí)行的,對SQL進行增強的一種機制。
MyBatis的Plugin實現(xiàn)基于JDK動態(tài)代理機制,在MyBatis初始化過程中,可以為指定的攔截對象生成代理對象,當攔截對象執(zhí)行某個方法時,代理會先執(zhí)行插件中的邏輯,再執(zhí)行原有邏輯。插件可以在原有邏輯前后添加自己的邏輯或者完全替換原有邏輯
如果你使用過spring的話,會自然的想到spring的AOP特性,兩者都是利用代理來實現(xiàn)功能的增強
二、Mybatis Plugin 的實例
這是一個舊項目,在后期對接Oracle后,有很多sql報了錯,其原因是使用 instr() 函數時,由于參數是外部傳入的,有時候可能會傳來一個幾千長度的字符串,從而導致instr 超長報錯。因為這樣的sql還有很多,不可能一一去改,所以必須使用功能增強的方式來解決
在直接上示例之前,我們先看看官方提供的接口 Interceptor.java ,只要實現(xiàn)了該接口,就可以在指定位置發(fā)揮作用
package org.apache.ibatis.plugin; public interface Interceptor { /** * intercept方法就是要進行攔截的時候要執(zhí)行的方法 */ Object intercept(Invocation invocation) throws Throwable; default Object plugin(Object target) { return Plugin.wrap(target, this); } default void setProperties(Properties properties) { // NOP } }
當然,這個接口還需要配合另一個注解 @Intercepts 使用,我們結合案例寫一個插件看看
@Component @Intercepts({ @Signature( type = StatementHandler.class, method = "prepare", args = { Connection.class, Integer.class }) }) public class ExamplePlugin implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler statementHandler = (StatementHandler)invocation.getTarget(); BoundSql boundSql = statementHandler.getBoundSql(); String sql = boundSql.getSql(); String newSql = null; // 改造sql部分省略,主要是將 instr() 拆分成 instr() or instr() 的形式以降低每個括號內 的長度... // newsql = sql.replace(...) // 把新的sql通過反射重新設置回去 Field field = boundSql.getClass().getDeclaredField("sql"); field.setAccessible(true); field.set(boundSql , newSql); Object returnVal = invocation.proceed(); return returnVal; } }
不難看出,配上注解后,該插件的意思就是針對 StatementHandler.prepare(Connection, Integer) 方法進行增強,我們實際運行下看看:
如圖,最終走到了我們寫的插件的 intercept 方法中
需要注意的是 @Intercepts 注解內支持配置 @Signature 數組,并以逗號分割。也就是說一個攔截器其實可以攔截多個類的方法,如下
@Intercepts({ @Signature( type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class}), @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}) })
三、Mybatis Plugin 原理
1. Mybatis 支持哪些 Plugin
其實從上面,案例看出,我們對插件的設置主要是通過 @Intercepts 內的 @Signature 注解實現(xiàn)的
@Intercepts({ @Signature( type = StatementHandler.class, method = "prepare", args = { Connection.class, Integer.class }) })
其中,type 就是作用的接口,method 和 args 則能確定唯一方法(單用方法名,可能有方法重載的情況)
但是,是不是這些屬性可以隨便填呢?其實不是的,mybatis沒有做的那么自由,其更像Spring中的postProcessor機制,只在固定的幾個位置有預留點,讓你可以自定義增強,而不是開放所有位置。
這里的Pulgin只針對以下四個接口有增強預留點,它們分別是
- Statementhandler:
用于處理JDBC Statement對象的相關操作,將SQL語句中的占位符進行替換,然后使用Statement對象執(zhí)行SQL語句 - Resultsethandler:
主要負責將JDBC返回的ResultSet結果集轉化為Java對象,然后返還給調用方 - ParameterHandler:
主要用于處理Java對象與JDBC參數的映射,并將其轉化為JDBC參數。 - Executor:
更頂層的設計,能對上三種類進行調用,執(zhí)行SQL語句,并獲取執(zhí)行結果
其具體調用鏈路如下:
2. myBatis 如何加載 Plugin
即我們自己創(chuàng)建了個 Interceptor 實現(xiàn)類,也使用了 @Intercepts 注解,但這個類是如何被mybatis加載的呢?
2.1 springboot 項目
我們仍以上篇文章的springboot+mybatis為例,那么此處便又要提到spring-boot的自動配置了,我們看下 MybatisAutoConfiguration (mybatis-spring-boot-autoconfigure2.1.4版本)這個自動配置類,看其構造方法
@org.springframework.context.annotation.Configuration @ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class }) @ConditionalOnSingleCandidate(DataSource.class) @EnableConfigurationProperties(MybatisProperties.class) @AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class }) public class MybatisAutoConfiguration implements InitializingBean { // 省略部分代碼 public MybatisAutoConfiguration(MybatisProperties properties, ObjectProvider<Interceptor[]> interceptorsProvider, ObjectProvider<TypeHandler[]> typeHandlersProvider, ObjectProvider<LanguageDriver[]> languageDriversProvider, ResourceLoader resourceLoader, ObjectProvider<DatabaseIdProvider> databaseIdProvider, ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider) { this.properties = properties; this.interceptors = interceptorsProvider.getIfAvailable(); this.typeHandlers = typeHandlersProvider.getIfAvailable(); this.languageDrivers = languageDriversProvider.getIfAvailable(); this.resourceLoader = resourceLoader; this.databaseIdProvider = databaseIdProvider.getIfAvailable(); this.configurationCustomizers = configurationCustomizersProvider.getIfAvailable(); } }
其構造方法的第二個參數 :ObjectProvider<Interceptor[]>
ObjectProvider 是在spring 4.3 引入的一種注入方式,它可以檢索指定的類型。
然后通過 getIfAvailable 和 getIfUnique 從spring容器中檢索出對應對象
因為我們已經在自定義的 ExamplePlugin 上使用了@Component 的注解,所以此處使用自動注入,能獲取到我們的插件理所當然。
而后再把該值賦給 sqlSessionFactoryBean, 然后再賦給 mybatis 真正的配置類 Configuration。至此,我們的插件就被 mybatis 系統(tǒng)所成功加載了。
2.2 spring 項目
如果還沒有使用上spring-boot,沒有所謂的自動配置,那也無妨,只是需要手動額外配置一點參數也是同樣的。
如:已經在 application.properties 配置了mybatis 配置文件
mybatis.config.location: classpath:/mybatis-config.xml
然后在mybatis-config.xml 里加上如下配置
<configuration> <plugins> <plugin interceptor="com.zhanfu.spring.demo.utils.ExamplePlugin"/> </plugins> </configuration>
這樣也能達到,將指定插件放入 mybatis 框架的效果
3. Plugin 生效原理
上面我們講了,如何寫一個插件,以及插件是怎么交給 myBatis框架的,現(xiàn)在要談最重要的內容了。即myBatis 是如何利用插件的。
上文我們已經了解到了,所有的插件實例都被放入了 myBatis 的總配置類 Configuration 去管理,成為了該類的一個屬性interceptorChain ,該類詳情如下:
public class InterceptorChain { // 所有的插件都存在這個 List 中 private final List<Interceptor> interceptors = new ArrayList<>(); public Object pluginAll(Object target) { for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; } public void addInterceptor(Interceptor interceptor) { interceptors.add(interceptor); } public List<Interceptor> getInterceptors() { return Collections.unmodifiableList(interceptors); } }
所以我們只要看該類拿這些插件做了什么即可,可以看到,該類對所有新建的目標對象,都進行了 pluginAll 操作,結合上圖,我們不難看出,該方法其實就是遍歷所有插件,然后調用每個插件的 plugin() 方法 生成一個新對象,然后下一個插件拿這個新對象再 plugin() 生成一個新對象,實際上構成了一套鏈式的嵌套
那么plugin() 方法到底做了什么呢?我們來看看回頭再來看看 Interceptor 接口里,該方法的默認實現(xiàn)
// Interceptor.java default Object plugin(Object target) { return Plugin.wrap(target, this); } // Plugin.java public static Object wrap(Object target, Interceptor interceptor) { // 從插件的注解中,解析出該插件可作用的接口,以及該類下的哪些方法 Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); Class<?> type = target.getClass(); // 找到插件可作用的接口和目標類的中所有重合的接口 Class<?>[] interfaces = getAllInterfaces(type, signatureMap); // 如果有重合的接口,則生成jdk代理并返回。注意,interceptor是我們的插件對象,signatureMap是插件注釋解析到的類與方法 if (interfaces.length > 0) { return Proxy.newProxyInstance( type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); } // 如果沒有,則返回原對象 return target; }
綜上,不難看出,只有生成指定的四種實例時,才會進入上述代碼生成代理,最后返還的其實就是代理對象。需要注意的是,此時的代理是能夠代理這些接口的所有方法的,要想實現(xiàn)指定方法才使用代理,還得依靠代理的 invoke 方法內去篩選
// Plugin.java @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { // 只有注解上指定方法才能走插件對象的 intercept 方法 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); } }
以上就是 Mybatis plugin 的使用及原理解析的詳細內容,更多關于 Mybatis plugin使用原理的資料請關注腳本之家其它相關文章!
相關文章
mybatis-plus @DS實現(xiàn)動態(tài)切換數據源原理
本文主要介紹了mybatis-plus @DS實現(xiàn)動態(tài)切換數據源原理,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2022-07-07Docker?DockerFile部署java?jar項目包及Mysql和Redis的詳細過程
Dockerfile是一種用于構建Docker鏡像的文件格式,可以通過Dockerfile部署Java項目,這篇文章主要給大家介紹了關于Docker?DockerFile部署java?jar項目包及Mysql和Redis的詳細過程,需要的朋友可以參考下2023-12-12