插件機(jī)制
一般情況下,開源框架都會(huì)提供插件或其他形式的拓展點(diǎn),供開發(fā)者自行拓展。這樣的
好處是顯而易見的,一是增加了框架的靈活性。二是開發(fā)者可以結(jié)合實(shí)際需求,對(duì)框架進(jìn)行
拓展,使其能夠更好的工作。以 MyBatis 為例,我們可基于 MyBatis 插件機(jī)制實(shí)現(xiàn)分頁(yè)、
分表,監(jiān)控等功能。由于插件和業(yè)務(wù)無關(guān),業(yè)務(wù)也無法感知插件的存在。因此可以無感植入
插件,在無形中增強(qiáng)功能。
開發(fā) MyBatis 插件需要對(duì) MyBatis 比較深了解才行,一般來說最好能夠掌握 MyBatis
的源碼,門檻相對(duì)較高。本篇文章在分析完 MyBatis 插件機(jī)制后,會(huì)手寫一個(gè)簡(jiǎn)單的分頁(yè)
插件,以幫助大家更好的掌握 MyBatis 插件的編寫。
1、插件機(jī)制原理
我們?cè)诰帉懖寮r(shí),除了需要讓插件類實(shí)現(xiàn) Interceptor 接口外,還需要通過注解標(biāo)注
該插件的攔截點(diǎn)。所謂攔截點(diǎn)指的是插件所能攔截的方法,MyBatis 所允許攔截的方法如
下:
- Executor: update, query, flushStatements, commit, rollback,
getTransaction, close, isClosed - ParameterHandler: getParameterObject, setParameters
- ResultSetHandler: handleResultSets, handleOutputParameters
- StatementHandler: prepare, parameterize, batch, update, query
如果我們想要攔截 Executor 的 query 方法,那么可以這樣定義插件。
@Intercepts({
@Signature(
type = Executor.class,
method = "query",
args ={MappedStatement.class, Object.class, RowBounds.class,
ResultHandler.class}
)
})
public class ExamplePlugin implements Interceptor {
// 省略邏輯
}
除此之外,我們還需將插件配置到相關(guān)文件中。這樣 MyBatis 在啟動(dòng)時(shí)可以加載插件,
并保存插件實(shí)例到相關(guān)對(duì)象(InterceptorChain,攔截器鏈)中。待準(zhǔn)備工作做完后,MyBatis
處于就緒狀態(tài)。我們?cè)趫?zhí)行 SQL 時(shí),需要先通過 DefaultSqlSessionFactory 創(chuàng) 建
SqlSession 。Executor 實(shí)例會(huì)在創(chuàng)建 SqlSession 的過程中被創(chuàng)建,Executor 實(shí)例創(chuàng)建完畢
后,MyBatis 會(huì)通過 JDK 動(dòng)態(tài)代理為實(shí)例生成代理類。這樣,插件邏輯即可在 Executor 相
關(guān)方法被調(diào)用前執(zhí)行。以上就是 MyBatis 插件機(jī)制的基本原理。接下來,我們來看一下原
理背后對(duì)應(yīng)的源碼是怎樣的。
1.1 植?插件邏輯
本節(jié),我將以 Executor 為例,分析 MyBatis 是如何為 Executor 實(shí)例植入插件邏輯的。
Executor 實(shí)例是在開啟 SqlSession 時(shí)被創(chuàng)建的,因此,下面我們從源頭進(jìn)行分析。先來看
一下 SqlSession 開啟的過程。
// -☆- DefaultSqlSessionFactory
public SqlSession openSession() {
return openSessionFromDataSource(
configuration.getDefaultExecutorType(), null, false);
}
private SqlSession openSessionFromDataSource(ExecutorType execType,
TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
// 省略部分邏輯
// 創(chuàng)建 Executor
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
}
catch (Exception e) {...}
finally {...}
}
Executor 的創(chuàng)建過程封裝在 Configuration 中,我們跟進(jìn)去看看看。
// -☆- Configuration
public Executor newExecutor(Transaction transaction,
ExecutorType executorType) {
executorType = executorType == null ?
defaultExecutorType : executorType;
executorType = executorType == null ?
ExecutorType.SIMPLE : executorType;
Executor executor;
// 根據(jù) executorType 創(chuàng)建相應(yīng)的 Executor 實(shí)例
if (ExecutorType.BATCH == executorType) {...}
else if (ExecutorType.REUSE == executorType) {...}
else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// 植入插件
executor = (Executor) interceptorChain.pluginAll(executor);
return executor; }
如上,newExecutor 方法在創(chuàng)建好 Executor 實(shí)例后,緊接著通過攔截器鏈 interceptorChain
為 Executor 實(shí)例植入代理邏輯。那下面我們看一下 InterceptorChain 的代碼是怎樣的。
public class InterceptorChain {
private final List<Interceptor> interceptors =
new ArrayList<Interceptor>();
public Object pluginAll(Object target) {
// 遍歷攔截器集合
for (Interceptor interceptor : interceptors) {
// 調(diào)用攔截器的 plugin 方法植入相應(yīng)的插件邏輯
target = interceptor.plugin(target);
}
return target;
}
/** 添加插件實(shí)例到 interceptors 集合中 */
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
/** 獲取插件列表 */
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
以上是 InterceptorChain 的全部代碼,比較簡(jiǎn)單。它的 pluginAll 方法會(huì)調(diào)用具體插件的
plugin 方法植入相應(yīng)的插件邏輯。如果有多個(gè)插件,則會(huì)多次調(diào)用 plugin 方法,最終生成一
個(gè)層層嵌套的代理類。形如下面:

當(dāng) Executor 的某個(gè)方法被調(diào)用的時(shí)候,插件邏輯會(huì)先行執(zhí)行。執(zhí)行順序由外而內(nèi),比如
上圖的執(zhí)行順序?yàn)?plugin3 → plugin2 → Plugin1 → Executor。
plugin 方法是由具體的插件類實(shí)現(xiàn),不過該方法代碼一般比較固定,所以下面找個(gè)示例
分析一下。
// -☆- ExamplePlugin
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
// -☆- Plugin
public static Object wrap(Object target, Interceptor interceptor) {
// 獲取插件類 @Signature 注解內(nèi)容,并生成相應(yīng)的映射結(jié)構(gòu)。形如下面:
// {
// Executor.class : [query, update, commit],
// ParameterHandler.class : [getParameterObject, setParameters]
// }
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
// 獲取目標(biāo)類實(shí)現(xiàn)的接口
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
// 通過 JDK 動(dòng)態(tài)代理為目標(biāo)類生成代理類
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
如上,plugin 方法在內(nèi)部調(diào)用了 Plugin 類的 wrap 方法,用于為目標(biāo)對(duì)象生成代理。Plugin
類實(shí)現(xiàn)了InvocationHandler接口,因此它可以作為參數(shù)傳給Proxy的newProxyInstance方法。
到這里,關(guān)于插件植入的邏輯就分析完了。接下來,我們來看看插件邏輯是怎樣執(zhí)行
的。
1.2 執(zhí)?插件邏輯
Plugin 實(shí)現(xiàn)了 InvocationHandler 接口,因此它的 invoke 方法會(huì)攔截所有的方法調(diào)用。
invoke 方法會(huì)對(duì)所攔截的方法進(jìn)行檢測(cè),以決定是否執(zhí)行插件邏輯。該方法的邏輯如下:
// -☆- Plugin
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
try {
// 獲取被攔截方法列表,比如:signatureMap.get(Executor.class),
// 可能返回 [query, update, commit]
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
// 檢測(cè)方法列表是否包含被攔截的方法
if (methods != null && methods.contains(method)) {
// 執(zhí)行插件邏輯
return interceptor.intercept(
new Invocation(target, method, args));
}
// 執(zhí)行被攔截的方法
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
} }
invoke 方法的代碼比較少,邏輯不難理解。首先,invoke 方法會(huì)檢測(cè)被攔截方法是否配
置在插件的 @Signature 注解中,若是,則執(zhí)行插件邏輯,否則執(zhí)行被攔截方法。插件邏輯
封裝在 intercept 中,該方法的參數(shù)類型為 Invocation。Invocation 主要用于存儲(chǔ)目標(biāo)類,方法
以及方法參數(shù)列表。下面簡(jiǎn)單看一下該類的定義。
public class Invocation {
private final Object target;
private final Method method;
private final Object[] args;
public Invocation(Object target, Method method, Object[] args) {
this.target = target;
this.method = method;
this.args = args;
}
public Object proceed()
throws InvocationTargetException, IllegalAccessException {
// 調(diào)用被攔截的方法
return method.invoke(target, args);
} }
關(guān)于插件的執(zhí)行邏輯就分析到這,整個(gè)過程不難理解,大家簡(jiǎn)單看看即可。
2、實(shí)現(xiàn)?個(gè)分頁(yè)插件
為了更好的向大家介紹 MyBatis 的插件機(jī)制,本節(jié)將實(shí)現(xiàn)一個(gè) MySQL 數(shù)據(jù)庫(kù)分頁(yè)插
件。相關(guān)代碼如下:
@Intercepts({
@Signature(
type = Executor.class, // 目標(biāo)類
method = "query", // 目標(biāo)方法
args ={MappedStatement.class,
Object.class, RowBounds.class, ResultHandler.class}
)
})
public class MySqlPagingPlugin implements Interceptor {
private static final Integer MAPPED_STATEMENT_INDEX = 0;
private static final Integer PARAMETER_INDEX = 1;
private static final Integer ROW_BOUNDS_INDEX = 2;
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
RowBounds rb = (RowBounds) args[ROW_BOUNDS_INDEX];
// 無需分頁(yè)
if (rb == RowBounds.DEFAULT) {
return invocation.proceed();
}
// 將原 RowBounds 參數(shù)設(shè)為 RowBounds.DEFAULT,關(guān)閉 MyBatis 內(nèi)置的分頁(yè)機(jī)制
args[ROW_BOUNDS_INDEX] = RowBounds.DEFAULT;
MappedStatement ms = (MappedStatement) args[MAPPED_STATEMENT_INDEX];
BoundSql boundSql = ms.getBoundSql(args[PARAMETER_INDEX]);
// 獲取 SQL 語句,拼接 limit 語句
String sql = boundSql.getSql();
String limit = String.format(
"LIMIT %d,%d", rb.getOffset(), rb.getLimit());
sql = sql + " " + limit;
// 創(chuàng)建一個(gè) StaticSqlSource,并將拼接好的 sql 傳入
SqlSource sqlSource = new StaticSqlSource(
ms.getConfiguration(), sql, boundSql.getParameterMappings());
// 通過反射獲取并設(shè)置 MappedStatement 的 sqlSource 字段
Field field = MappedStatement.class.getDeclaredField("sqlSource");
field.setAccessible(true);
field.set(ms, sqlSource);
// 執(zhí)行被攔截方法
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
} }
上面的分頁(yè)插件通過 RowBounds 參數(shù)獲取分頁(yè)信息,并生成相應(yīng)的 limit 語句。之后拼
接 sql,并使用該 sql 作為參數(shù)創(chuàng)建 StaticSqlSource。最后通過反射替換 MappedStatement 對(duì)
象中的 sqlSource 字段。下面,寫點(diǎn)測(cè)試代碼驗(yàn)證一下插件是否可以正常運(yùn)行。先來看一下
Dao 接口與映射文件的定義:
public interface StudentDao {
List<Student> findByPaging(@Param("id") Integer id, RowBounds rb);
}
<mapper namespace="xyz.coolblog.chapter7.dao.StudentDao">
<select id="findByPaging"
resultType="xyz.coolblog.chapter7.model.Student">
SELECT
`id`, `name`, `age`
FROM
student
WHERE
id > #{id}
</select>
</mapper>
測(cè)試代碼如下:
public class PluginTest {
private SqlSessionFactory sqlSessionFactory;
@Before
public void prepare() throws IOException {
String resource = "chapter7/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory=new SqlSessionFactoryBuilder().build(inputStream);
inputStream.close();
}
@Test
public void testPlugin() {
SqlSession session = sqlSessionFactory.openSession();
try {
StudentDao studentDao = session.getMapper(StudentDao.class);
studentDao.findByPaging(1, new RowBounds(20, 10));
} finally {
session.close();
}
} }
上面代碼運(yùn)行之后,會(huì)打印如下日志。

在上面的輸出中,SQL 語句中包含了 LIMIT 字樣,這說明插件生效了。
3 本章?結(jié)
到此,關(guān)于 MyBatis 插件機(jī)制就分析完了??傮w來說,MyBatis 插件機(jī)制比較簡(jiǎn)單。
但實(shí)現(xiàn)一個(gè)插件卻較為復(fù)雜,需要對(duì) MyBatis 比較了解才行。因此,若想寫出高效的插
件,還需深入學(xué)習(xí)源碼才行。