本篇博客將主要講解 mybatis 插件的主要流程,其中主要包括動態代理和責任鏈的使用;
一、mybatis 攔截器主體結構
在編寫 mybatis 插件的時候,首先要實現 Interceptor 接口,然后在 mybatis-conf.xml 中添加插件,
<configuration>
<plugins>
<plugin interceptor="***.interceptor1"/>
<plugin interceptor="***.interceptor2"/>
</plugins>
</configuration>
這里需要注意的是,添加的插件是有順序的,因為在解析的時候是依次放入 ArrayList 里面,而調用的時候其順序為:2 > 1 > target > 1 > 2;(插件的順序可能會影響執行的流程)更加細致的講解可以參考 QueryInterceptor 規范 ;
然后當插件初始化完成之后,添加插件的流程如下:

首先要注意的是,mybatis 插件的攔截目標有四個,Executor、StatementHandler、ParameterHandler、ResultSetHandler:
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
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);
resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
return resultSetHandler;
}
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);
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
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;
}
這里使用的時候都是用動態代理將多個插件用責任鏈的方式添加的,最后返回的是一個代理對象; 其責任鏈的添加過程如下:
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
最終動態代理生成和調用的過程都在 Plugin 類中:
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); // 獲取簽名Map
Class<?> type = target.getClass(); // 攔截目標 (ParameterHandler|ResultSetHandler|StatementHandler|Executor)
Class<?>[] interfaces = getAllInterfaces(type, signatureMap); // 獲取目標接口
if (interfaces.length > 0) {
return Proxy.newProxyInstance( // 生成代理
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
這里所說的簽名是指在編寫插件的時候,指定的目標接口和方法,例如:
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class ExamplePlugin implements Interceptor {
public Object intercept(Invocation invocation) throws Throwable {
...
}
}
這里就指定了攔截 Executor 的具有相應方法的 update、query 方法;注解的代碼很簡單,大家可以自行查看;然后通過 getSignatureMap 方法反射取出對應的 Method 對象,在通過 getAllInterfaces 方法判斷,目標對象是否有對應的方法,有就生成代理對象,沒有就直接反對目標對象;
在調用的時候:
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);
}
}
二、PageHelper 攔截器分析
mybatis 插件我們平時使用最多的就是分頁插件了,這里以 PageHelper 為例,其使用方法可以查看相應的文檔 如何使用分頁插件,因為官方文檔講解的很詳細了,我這里就簡單補充分頁插件需要做哪幾件事情;
使用:
PageHelper.startPage(1, 2);
List<User> list = userMapper1.getAll();
PageHelper 還有很多中使用方式,這是最常用的一種,他其實就是在 ThreadLocal 中設置了 Page 對象,能取到就代表需要分頁,在分頁完成后在移除,這樣就不會導致其他方法分頁;(PageHelper 使用的其他方法,也是圍繞 Page 對象的設置進行的)
protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
Page<E> page = new Page<E>(pageNum, pageSize, count);
page.setReasonable(reasonable);
page.setPageSizeZero(pageSizeZero);
//當已經執行過orderBy的時候
Page<E> oldPage = getLocalPage();
if (oldPage != null && oldPage.isOrderByOnly()) {
page.setOrderBy(oldPage.getOrderBy());
}
setLocalPage(page);
return page;
}
主要實現:
@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 {
@Override
public Object intercept(Invocation invocation) throws Throwable {
try {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
RowBounds rowBounds = (RowBounds) args[2];
ResultHandler resultHandler = (ResultHandler) args[3];
Executor executor = (Executor) invocation.getTarget();
CacheKey cacheKey;
BoundSql boundSql;
//由於邏輯關系,只會進入一次
if (args.length == 4) {
//4 個參數時
boundSql = ms.getBoundSql(parameter);
cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
} else {
//6 個參數時
cacheKey = (CacheKey) args[4];
boundSql = (BoundSql) args[5];
}
checkDialectExists();
List resultList;
//調用方法判斷是否需要進行分頁,如果不需要,直接返回結果
if (!dialect.skip(ms, parameter, rowBounds)) {
//判斷是否需要進行 count 查詢
if (dialect.beforeCount(ms, parameter, rowBounds)) {
//查詢總數
Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql);
//處理查詢總數,返回 true 時繼續分頁查詢,false 時直接返回
if (!dialect.afterCount(count, parameter, rowBounds)) {
//當查詢總數為 0 時,直接返回空的結果
return dialect.afterPage(new ArrayList(), parameter, rowBounds);
}
}
resultList = ExecutorUtil.pageQuery(dialect, executor,
ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
} else {
//rowBounds用參數值,不使用分頁插件處理時,仍然支持默認的內存分頁
resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
}
return dialect.afterPage(resultList, parameter, rowBounds);
} finally {
if(dialect != null){
dialect.afterAll();
}
}
}
}
- 首先可以看到攔截的是 Executor 的兩個 query 方法(這里的兩個方法具體攔截到哪一個受插件順序影響,最終影響到 cacheKey 和 boundSql 的初始化);
- 然后使用 checkDialectExists 判斷是否支持對應的數據庫;
- 在分頁之前需要查詢總數,這里會生成相應的 sql 語句以及對應的 MappedStatement 對象,並緩存;
- 然后拼接分頁查詢語句,並生成相應的 MappedStatement 對象,同時緩存;
- 最后查詢,查詢完成后使用 dialect.afterPage 移除 Page對象