送命題:講一講Mybatis插件的原理及如何實現?
持續原創輸出,點擊上方藍字關注我吧
目錄
-
前言 -
環境配置 -
什麼是插件? -
如何自定義插件? -
舉個栗子 -
用到哪些註解? -
如何注入Mybatis? -
測試
-
-
插件原理分析 -
如何生成代理對象? -
如何執行? -
總結
-
-
分頁插件的原理分析 -
總結
前言
-
Mybatis的分頁插件相信大家都使用過,那麼可知道其中的實現原理?分頁插件就是利用的Mybatis中的插件機制實現的,在 Executor
的query
執行前後進行分頁處理。 -
此篇文章就來介紹以下Mybatis的插件機制以及在底層是如何實現的。
環境配置
-
本篇文章講的一切內容都是基於 Mybatis3.5
和SpringBoot-2.3.3.RELEASE
。
什麼是插件?
-
插件是Mybatis中的最重要的功能之一,能夠對特定組件的特定方法進行增強。 -
MyBatis 允許你在映射語句執行過程中的某一點進行攔截調用。默認情況下,MyBatis 允許使用插件來攔截的方法調用包括: -
「Executor」: update
,query
,flushStatements
,commit
,rollback
,getTransaction
,close
,isClosed
-
「ParameterHandler」: getParameterObject
,setParameters
-
「ResultSetHandler」: handleResultSets
,handleOutputParameters
-
「StatementHandler」: prepare
,parameterize
,batch
,update
,query
-
如何自定義插件?
-
插件的實現其實很簡單,只需要實現Mybatis提供的 Interceptor
這個介面即可,源碼如下:
public interface Interceptor {
//攔截的方法
Object intercept(Invocation invocation) throws Throwable;
//返回攔截器的代理對象
Object plugin(Object target);
//設置一些屬性
void setProperties(Properties properties);
}
舉個栗子
-
有這樣一個需求:需要在Mybatis執行的時候篡改 selectByUserId
的參數值。 -
「分析」:修改SQL的入參,應該在哪個組件的哪個方法上攔截篡改呢?研究過源碼的估計都很清楚的知道, ParameterHandler
中的setParameters()
方法就是對參數進行處理的。因此肯定是攔截這個方法是最合適。 -
自定義的插件如下:
/**
* @Intercepts 註解標記這是一個攔截器,其中可以指定多個@Signature
* @Signature 指定該攔截器攔截的是四大對象中的哪個方法
* type:攔截器的四大對象的類型
* method:攔截器的方法,方法名
* args:入參的類型,可以是多個,根據方法的參數指定,以此來區分方法的重載
*/
@Intercepts(
{
@Signature(type = ParameterHandler.class,method ="setParameters",args = {PreparedStatement.class})
}
)
public class ParameterInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("攔截器執行:"+invocation.getTarget());
//目標對象
Object target = invocation.getTarget();
//獲取目標對象中所有屬性的值,因為ParameterHandler使用的是DefaultParameterHandler,因此裡面的所有的屬性都封裝在其中
MetaObject metaObject = SystemMetaObject.forObject(target);
//使用xxx.xxx.xx的方式可以層層獲取屬性值,這裡獲取的是mappedStatement中的id值
String value = (String) metaObject.getValue("mappedStatement.id");
//如果是指定的查詢方法
if ("cn.cb.demo.dao.UserMapper.selectByUserId".equals(value)){
//設置參數的值是admin_1,即是設置id=admin_1,因為這裡只有一個參數,可以這麼設置,如果有多個需要需要循環
metaObject.setValue("parameterObject", "admin_1");
}
//執行目標方法
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
//如果沒有特殊訂製,直接使用Plugin這個工具類返回一個代理對象即可
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
-
intercept
方法:最終會攔截的方法,最重要的一個方法。 -
plugin
方法:返回一個代理對象,如果沒有特殊要求,直接使用Mybatis的工具類Plugin
返回即可。 -
setProperties
:設置一些屬性,不重要。
用到哪些註解?
-
自定義插件需要用到兩個註解,分別是 @Intercepts
和@Signature
。 -
@Intercepts
:標註在實現類上,表示這個類是一個插件的實現類。 -
@Signature
:作為@Intercepts
的屬性,表示需要增強Mybatis的某些
組件中的某些
方法(可以指定多個)。常用的屬性如下:-
Class<?> type()
:指定哪個組件(Executor
、ParameterHandler
、ResultSetHandler
、StatementHandler
) -
String method()
:指定增強組件中的哪個方法,直接寫方法名稱。 -
Class<?>[] args()
:方法中的參數,必須一一對應,可以寫多個;這個屬性非常重用,區分重載方法。
-
如何注入Mybatis?
-
上面已經將插件定義好了,那麼如何注入到Mybatis中使其生效呢?
-
「前提」:由於本篇文章的環境是
SpringBoot+Mybatis
,因此講一講如何在SpringBoot中將插件注入到Mybatis中。 -
在Mybatis的自動配置類
MybatisAutoConfiguration
中,注入SqlSessionFactory
的時候,有如下一段程式碼:
-
上圖中的
this.interceptors
是什麼,從何而來,其實就是從容器中的獲取的Interceptor[]
,如下一段程式碼:
-
從上圖我們知道,這插件最終還是從IOC容器中獲取的
Interceptor[]
這個Bean
,因此我們只需要在配置類中注入這個Bean
即可,如下程式碼:
/**
* @Configuration:這個註解標註該類是一個配置類
*/
@Configuration
public class MybatisConfig{
/**
* @Bean : 該註解用於向容器中注入一個Bean
* 注入Interceptor[]這個Bean
* @return
*/
@Bean
public Interceptor[] interceptors(){
//創建ParameterInterceptor這個插件
ParameterInterceptor parameterInterceptor = new ParameterInterceptor();
//放入數組返回
return new Interceptor[]{parameterInterceptor};
}
}
測試
-
此時自定義的插件已經注入了Mybatis中了,現在測試看看能不能成功執行呢?測試程式碼如下:
@Test
void contextLoads() {
//傳入的是1222
UserInfo userInfo = userMapper.selectByUserId("1222");
System.out.println(userInfo);
}
-
測試程式碼傳入的是 1222
,由於插件改變了入參,因此查詢出來的應該是admin_1
這個人。
插件原理分析
-
插件的原理其實很簡單,就是在創建組件的時候生成代理對象( Plugin
),執行組件方法的時候攔截即可。下面就來詳細介紹一下插件在Mybatis底層是如何工作的? -
Mybatis的四大組件都是在Mybatis的配置類 Configuration
中創建的,具體的方法如下:
//創建Executor
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);
}
//調用pluginAll方法,生成代理對象
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
//創建ParameterHandler
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
//調用pluginAll方法,生成代理對象
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
//創建ResultSetHandler
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
ResultHandler resultHandler, BoundSql boundSql) {
ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
//調用pluginAll方法,生成代理對象
resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
return resultSetHandler;
}
//創建StatementHandler
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
//調用pluginAll方法,生成代理對象
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
-
從上面的源碼可以知道,創建四大組件的方法中都會執行 pluginAll()
這個方法來生成一個代理對象。具體如何生成的,下面詳解。
如何生成代理對象?
-
創建四大組件過程中都執行了 pluginAll()
這個方法,此方法源碼如下:
public Object pluginAll(Object target) {
//循環遍歷插件
for (Interceptor interceptor : interceptors) {
//調用插件的plugin()方法
target = interceptor.plugin(target);
}
//返回
return target;
}
-
pluginAll()
方法很簡單,直接循環調用插件的plugin()
方法,但是我們調用的是Plugin.wrap(target, this)
這行程式碼,因此要看一下wrap()
這個方法的源碼,如下:
public static Object wrap(Object target, Interceptor interceptor) {
//獲取註解的@signature的定義
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;
}
-
Plugin.wrap()
這個方法的邏輯很簡單,判斷這個插件是否是攔截對應的組件,如果攔截了,生成代理對象(Plugin
)返回,沒有攔截直接返回,上面例子中生成的代理對象如下圖:
如何執行?
-
上面講了Mybatis啟動的時候如何根據插件生成代理對象的( Plugin
)。現在就來看看這個代理對象是如何執行的? -
既然是動態代理,肯定會執行的 invoke()
這個方法,Plugin
類中的invoke()
源碼如下:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//獲取@signature標註的方法
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
//如果這個方法被攔截了
if (methods != null && methods.contains(method)) {
//直接執行插件的intercept()這個方法
return interceptor.intercept(new Invocation(target, method, args));
}
//沒有被攔截,執行原方法
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
-
邏輯很簡單,這個方法被攔截了就執行插件的 intercept()
方法,沒有被攔截,則執行原方法。 -
還是以上面自定義的插件來看看執行的流程: -
setParameters()
這個方法在PreparedStatementHandler
中被調用,如下圖:
-
執行 invoke()
方法,發現setParameters()
這個方法被攔截了,因此直接執行的是intercept()
方法。
-
總結
-
Mybatis中插件的原理其實很簡單,分為以下幾步: -
在項目啟動的時候判斷組件是否有被攔截,如果沒有直接返回原對象。 -
如果有被攔截,返回動態代理的對象( Plugin
)。 -
執行到的組件的中的方法時,如果不是代理對象,直接執行原方法 -
如果是代理對象,執行 Plugin
的invoke()
方法。
-
分頁插件的原理分析
-
此處安利一款經常用的分頁插件 pagehelper
,Maven依賴如下:
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.6</version>
</dependency>
-
分頁插件很顯然也是根據Mybatis的插件來訂製的,來看看插件 PageInterceptor
的源碼如下:
@Intercepts(
{
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
}
)
public class PageInterceptor implements Interceptor {}
-
既然是分頁功能,肯定是在 query()
的時候攔截,因此肯定是在Executor
這個組件中。 -
分頁插件的原理其實很簡單,不再一一分析源碼了,根據的自己定義的分頁數據重新賦值 RowBounds
來達到分頁的目的,當然其中涉及到資料庫方言等等內容,不是本章重點,有興趣可以看一下GitHub上的文檔。
總結
-
對於業務開發的程式設計師來說,插件的這個功能很少用到,但是不用就不應該了解嗎?做人要有追求,哈哈。 -
歡迎關注作者的微信公眾號 碼猿技術專欄
,作者為你們精心準備了springCloud最新精彩影片教程
、精選500本電子書
、架構師免費影片教程
等等免費資源,讓我們一起進階,一起成長。