Mybatis 插件加載原理與實(shí)戰(zhàn)

點(diǎn)擊上方「Java有貨」關(guān)注我們

+
導(dǎo)語
想知道Mybatis的插件是如何生效的就需要了解mybatis的配置,所有的信息都在Mybatis Configuration內(nèi)部,
在之前的文章中,我們也會看到或如下的代碼:
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);// 加載插件parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);return parameterHandler;}
Mybatis 配置文件概覽
MyBatis 的配置文件包含了會深深影響 MyBatis 行為的設(shè)置和屬性信息。配置文檔的頂層結(jié)構(gòu)如下:
configuration(配置)
environment(環(huán)境變量)
transactionManager(事務(wù)管理器)
dataSource(數(shù)據(jù)源)
properties(屬性)
settings(設(shè)置)
typeAliases(類型別名)
typeHandlers(類型處理器)
objectFactory(對象工廠)
plugins(插件)
environments(環(huán)境配置)
databaseIdProvider(數(shù)據(jù)庫廠商標(biāo)識)
mappers(映射器)
InterceptorChain
看了Mybatis的源碼我們發(fā)現(xiàn),加載插件其實(shí)是通過InterceptorChain進(jìn)行加載的;
public class InterceptorChain {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);}}
在InterceptorChain 內(nèi)部維護(hù)了一個集合Interceptor(Interceptor 是一個接口), 然后通過 pluginAll 進(jìn)行循環(huán)加載;
插件(plugins)
MyBatis 允許你在映射語句執(zhí)行過程中的某一點(diǎn)進(jìn)行攔截調(diào)用。默認(rèn)情況下,MyBatis 允許使用插件來攔截的方法調(diào)用包括:
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query)
這些類中方法的細(xì)節(jié)可以通過查看每個方法的簽名來發(fā)現(xiàn),或者直接查看 MyBatis 發(fā)行包中的源代碼。如果你想做的不僅僅是監(jiān)控方法的調(diào)用,那么你最好相當(dāng)了解要重寫的方法的行為。因?yàn)樵谠噲D修改或重寫已有方法的行為時,很可能會破壞 MyBatis 的核心模塊。這些都是更底層的類和方法,所以使用插件的時候要特別當(dāng)心。
通過 MyBatis 提供的強(qiáng)大機(jī)制,使用插件是非常簡單的,只需實(shí)現(xiàn) Interceptor 接口,并指定想要攔截的方法簽名即可。
如果您不確定攔截了哪些對象,可以在 Configuration 搜素 pluginAll ;
自定義Mybatis 插件
在前面我們說過,插件其本質(zhì)就是攔截器的原理 ,那么我們實(shí)現(xiàn)Interceptor 接口既可以;
({(type = org.apache.ibatis.executor.Executor.class, method = "update",args = {MappedStatement.class, Object.class}),(type = Executor.class, method = "query",args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})public class ExamplePlugin implements Interceptor {private Properties properties = new Properties();public Object intercept(Invocation invocation) throws Throwable {System.out.println("自定義攔截器已經(jīng)被執(zhí)行.....");Object returnObject = invocation.proceed();// 必要時進(jìn)行后期處理return returnObject;}public void setProperties(Properties properties) {this.properties = properties;}}// 可在在其中獲取哪些值/*** 獲取被攔截的目前類,在這里是攔截了statementHandler,所有目前類也就是它* 通過這個類我們可以拿到待執(zhí)行的sql語句,通常使用mataObject工具類來獲取* 關(guān)于這個工具類,大家可自行了解,個人認(rèn)為這個工具類很強(qiáng)大*/StatementHandler statementHandler = (StatementHandler) invocation.getTarget();MetaObject metaObject = SystemMetaObject.forObject(statementHandler);/*** 先解釋下為什么寫成delegate.boundSql就可以拿到boundSql類* 從前面也可以得知,statementHandler的默認(rèn)實(shí)現(xiàn)是routingStatementHandler。* 這個類有一個屬性statementHandler,屬性名就叫delegate,而這個屬性的默認(rèn)實(shí)現(xiàn)又是preparedStatementHandler* 后面這個類又有屬性boundSql,所以,最終形成的寫法就是delegate.boundSql。* 所以這也體現(xiàn)了MetaObject工具類的強(qiáng)大,可以通過實(shí)例傳參,就可以根據(jù)屬性名獲取對應(yīng)屬性值*/BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");// 待執(zhí)行的sql,在這里也就是預(yù)編譯后的sql,即參數(shù)位都是?號String sql = boundSql.getSql();/*** 既然拿到了預(yù)編譯后的sql,那就可以按照你自己的想法為所欲為,如分頁,按年分表等等* 分表的話,個人推薦druid的sql解析器,我認(rèn)為還是不錯的,大家可以自行了解* 最后改造完sql,別忘了把它設(shè)置回去* metaObject.setValue("delegate.boundSql.sql",sql);* invocation.proceed,即原始方法的執(zhí)行* 注意點(diǎn)就是,因?yàn)閙ybatis插件采用的是代理模式,所以如果存在多個插件,會形成多個代理* 你如果要拿到最原始的對象,還得進(jìn)一步進(jìn)行分解* 如:while(metaObject.getValue(""h) != null){* Object obj = metaObject.getValue("h");* ....* }*/return invocation.proceed();
配置插件
XML配置
<!-- mybatis-config.xml -->
<plugins>
<plugin interceptor="com.javayh.mybatis.config.ExamplePlugin">
<property name="someProperty" value="100"/>
</plugin>
</plugins>java配置類
.springframework.context.annotation.Configurationpublic class MapperConfig {//注冊插件public ExamplePlugin myPlugin() {ExamplePlugin myPlugin = new ExamplePlugin();//設(shè)置參數(shù),比如閾值等,可以在配置文件中配置,這里直接寫死便于測試Properties properties = new Properties();//這里設(shè)置慢查詢閾值為1毫秒,便于測試properties.setProperty("time", "1");myPlugin.setProperties(properties);return myPlugin;}//將插件加入到mybatis插件攔截鏈中/*@Beanpublic ConfigurationCustomizer configurationCustomizer() {return configuration -> {//插件攔截鏈采用了責(zé)任鏈模式,執(zhí)行順序和加入連接鏈的順序有關(guān)ExamplePlugin myPlugin = new ExamplePlugin();//設(shè)置參數(shù),比如閾值等,可以在配置文件中配置,這里直接寫死便于測試Properties properties = new Properties();//這里設(shè)置慢查詢閾值為1毫秒,便于測試properties.setProperty("time", "1");myPlugin.setProperties(properties);configuration.addInterceptor(myPlugin);};}*/}
上面的插件將會攔截在 Executor 實(shí)例中所有的 “update 、query” 方法調(diào)用, 這里的 Executor 是負(fù)責(zé)執(zhí)行底層映射語句的內(nèi)部對象。
提示 覆蓋配置類
除了用插件來修改 MyBatis 核心行為以外,還可以通過完全覆蓋配置類來達(dá)到目的。只需繼承配置類后覆蓋其中的某個方法,再把它傳遞到 SqlSessionFactoryBuilder.build(myConfig) 方法即可。再次重申,這可能會極大影響 MyBatis 的行為,務(wù)請慎之又慎。
