MyBatis 源码分析 - 插件机制
1.簡介
一般情況下,開源框架都會提供插件或其他形式的拓展點,供開發(fā)者自行拓展。這樣的好處是顯而易見的,一是增加了框架的靈活性。二是開發(fā)者可以結(jié)合實際需求,對框架進行拓展,使其能夠更好的工作。以 MyBatis 為例,我們可基于 MyBatis 插件機制實現(xiàn)分頁、分表,監(jiān)控等功能。由于插件和業(yè)務(wù)無關(guān),業(yè)務(wù)也無法感知插件的存在。因此可以無感植入插件,在無形中增強功能。
開發(fā) MyBatis 插件需要對 MyBatis 比較深了解才行,一般來說最好能夠掌握 MyBatis 的源碼,門檻相對較高。本篇文章在分析完 MyBatis 插件機制后,會手寫一個簡單的分頁插件,以幫助大家更好的掌握 MyBatis 插件的編寫。
?2. 插件機制原理
我們在編寫插件時,除了需要讓插件類實現(xiàn) Interceptor 接口,還需要通過注解標注該插件的攔截點。所謂攔截點指的是插件所能攔截的方法,MyBatis 所允許攔截的方法如下:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
如果我們想要攔截 Executor 的 query 方法,那么可以這樣定義插件。
| 1 2 3 4 5 6 7 8 9 10 | @Intercepts({@Signature(type = Executor.class,method = "query",args ={MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}) }) public class ExamplePlugin implements Interceptor {// 省略邏輯 } |
除此之外,我們還需將插件配置到相關(guān)文件中。這樣 MyBatis 在啟動時可以加載插件,并保存插件實例到相關(guān)對象(InterceptorChain,攔截器鏈)中。待準備工作做完后,MyBatis 處于就緒狀態(tài)。我們在執(zhí)行 SQL 時,需要先通過 DefaultSqlSessionFactory 創(chuàng)建 SqlSession 。Executor 實例會在創(chuàng)建 SqlSession 的過程中被創(chuàng)建,Executor 實例創(chuàng)建完畢后,MyBatis 會通過 JDK 動態(tài)代理為實例生成代理類。這樣,插件邏輯即可在 Executor 相關(guān)方法被調(diào)用前執(zhí)行。
以上就是 MyBatis 插件機制的基本原理。接下來,我們來看一下原理背后對應(yīng)的源碼是怎樣的。
?3. 源碼分析
?3.1 植入插件邏輯
本節(jié),我將以 Executor 為例,分析 MyBatis 是如何為 Executor 實例植入插件邏輯的。Executor 實例是在開啟 SqlSession 時被創(chuàng)建的,因此,下面我們從源頭進行分析。先來看一下 SqlSession 開啟的過程。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // -☆- 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)建 Executorfinal Executor executor = configuration.newExecutor(tx, execType);return new DefaultSqlSession(configuration, executor, autoCommit);} catch (Exception e) {...} finally {...} } |
Executor 的創(chuàng)建過程封裝在 Configuration 中,我們跟進去看看看。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // -☆- 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 實例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 實例后,緊接著通過攔截器鏈 interceptorChain 為 Executor 實例植入代理邏輯。那下面我們看一下 InterceptorChain 的代碼是怎樣的。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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;}/** 添加插件實例到 interceptors 集合中 */public void addInterceptor(Interceptor interceptor) {interceptors.add(interceptor);}/** 獲取插件列表 */public List<Interceptor> getInterceptors() {return Collections.unmodifiableList(interceptors);} } |
以上是 InterceptorChain 的全部代碼,比較簡單。它的 pluginAll 方法會調(diào)用具體插件的 plugin 方法植入相應(yīng)的插件邏輯。如果有多個插件,則會多次調(diào)用 plugin 方法,最終生成一個層層嵌套的代理類。形如下面:
當(dāng) Executor 的某個方法被調(diào)用的時候,插件邏輯會先行執(zhí)行。執(zhí)行順序由外而內(nèi),比如上圖的執(zhí)行順序為?plugin3 → plugin2 → Plugin1 → Executor。
plugin 方法是由具體的插件類實現(xiàn),不過該方法代碼一般比較固定,所以下面找個示例分析一下。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | // -☆- 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();// 獲取目標類實現(xiàn)的接口Class<?>[] interfaces = getAllInterfaces(type, signatureMap);if (interfaces.length > 0) {// 通過 JDK 動態(tài)代理為目標類生成代理類return Proxy.newProxyInstance(type.getClassLoader(),interfaces,new Plugin(target, interceptor, signatureMap));}return target; } |
如上,plugin 方法在內(nèi)部調(diào)用了 Plugin 類的 wrap 方法,用于為目標對象生成代理。Plugin 類實現(xiàn)了 InvocationHandler 接口,因此它可以作為參數(shù)傳給 Proxy 的 newProxyInstance 方法。
到這里,關(guān)于插件植入的邏輯就分析完了。接下來,我們來看看插件邏輯是怎樣執(zhí)行的。
?3.2 執(zhí)行插件邏輯
Plugin 實現(xiàn)了 InvocationHandler 接口,因此它的 invoke 方法會攔截所有的方法調(diào)用。invoke 方法會對所攔截的方法進行檢測,以決定是否執(zhí)行插件邏輯。該方法的邏輯如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // -☆- 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());// 檢測方法列表是否包含被攔截的方法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 方法會檢測被攔截方法是否配置在插件的 @Signature 注解中,若是,則執(zhí)行插件邏輯,否則執(zhí)行被攔截方法。插件邏輯封裝在 intercept 中,該方法的參數(shù)類型為 Invocation。Invocation 主要用于存儲目標類,方法以及方法參數(shù)列表。下面簡單看一下該類的定義。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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í)行邏輯就分析到這,整個過程不難理解,大家簡單看看即可。
?4. 實現(xiàn)一個分頁插件
為了更好的向大家介紹 MyBatis 的插件機制,下面我將手寫一個針對 MySQL 的分頁插件。Talk is cheap. Show the code。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | @Intercepts({@Signature(type = Executor.class, // 目標類method = "query", // 目標方法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;@Overridepublic Object intercept(Invocation invocation) throws Throwable {Object[] args = invocation.getArgs();RowBounds rb = (RowBounds) args[ROW_BOUNDS_INDEX];// 無需分頁if (rb == RowBounds.DEFAULT) {return invocation.proceed();}// 將原 RowBounds 參數(shù)設(shè)為 RowBounds.DEFAULT,關(guān)閉 MyBatis 內(nèi)置的分頁機制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)建一個 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();}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {} } |
上面的分頁插件通過 RowBounds 參數(shù)獲取分頁信息,并生成相應(yīng)的 limit 語句。之后拼接 sql,并使用該 sql 作為參數(shù)創(chuàng)建 StaticSqlSource。最后通過反射替換 MappedStatement 對象中的 sqlSource 字段。以上代碼中出現(xiàn)了一些大家不太熟悉的類,比如 BoundSql,MappedStatement 以及 StaticSqlSource,這里簡單解釋一下吧。BoundSql 包含了經(jīng)過解析后的 sql 語句,以及使用者運行時傳入的參數(shù),這些參數(shù)最終會被設(shè)置到 sql 中。MappedStatement 與映射文件中的 <select>,<insert> 等節(jié)點對應(yīng),包含了節(jié)點的配置信息,比如 id,fetchSize 以及 SqlSource。StaticSqlSource 是 SqlSource 實現(xiàn)類之一,包含完全解析后的 sql 語句。所謂完全解析是指 sql 語句中不包含 ${xxx} 或 #{xxx} 等占位符,以及其他一些未解析的動態(tài)節(jié)點,比如 <if>,<where> 等。關(guān)于這些類就介紹這么多,如果大家還是不怎么理解的話,可以看看我之前寫的文章。接下里,寫點測試代碼驗證一下插件是否可以正常運行。先來看一下 Dao 接口與映射文件的定義:
| 1 2 3 | public interface StudentDao {List<Student> findByPaging(@Param("id") Integer id, RowBounds rb); } |
| 1 2 3 4 5 6 7 8 9 10 | <mapper namespace="xyz.coolblog.dao6.StudentDao"><select id="findByPaging" resultType="xyz.coolblog.model5.Student">SELECT`id`, `name`, `age`FROMstudentWHEREid > #{id}</select> </mapper> |
測試代碼如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | public class PluginTest {private SqlSessionFactory sqlSessionFactory;@Beforepublic void prepare() throws IOException {String resource = "mybatis-plugin-config.xml";InputStream inputStream = Resources.getResourceAsStream(resource);sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);inputStream.close();}@Testpublic void testPlugin() {SqlSession session = sqlSessionFactory.openSession();try {StudentDao studentDao = session.getMapper(StudentDao.class);studentDao.findByPaging(1, new RowBounds(20, 10));} finally {session.close();}} } |
上面代碼運行之后,會打印如下日志。
在上面的輸出中,SQL 語句中包含了 LIMIT 字樣,這說明插件生效了。
?5. 總結(jié)
到此,關(guān)于 MyBatis 插件機制就分析完了。總體來說,MyBatis 插件機制比較簡單。但實現(xiàn)一個插件卻較為復(fù)雜,需要對 MyBatis 比較了解才行。因此,若想寫出高效的插件,還需深入學(xué)習(xí)源碼才行。
好了,本篇文章就先到這了。感謝大家的閱讀。
?附錄:MyBatis 源碼分析系列文章列表
| 2018-07-16 | MyBatis 源碼分析系列文章導(dǎo)讀 |
| 2018-07-20 | MyBatis 源碼分析 - 配置文件解析過程 |
| 2018-07-30 | MyBatis 源碼分析 - 映射文件解析過程 |
| 2018-08-17 | MyBatis 源碼分析 - SQL 的執(zhí)行過程 |
| 2018-08-19 | MyBatis 源碼分析 - 內(nèi)置數(shù)據(jù)源 |
| 2018-08-25 | MyBatis 源碼分析 - 緩存原理 |
| 2018-08-26 | MyBatis 源碼分析 - 插件機制 |
- 本文鏈接:?https://www.tianxiaobo.com/2018/08/26/MyBatis-源碼分析-插件機制/
http://www.tianxiaobo.com/2018/08/26/MyBatis-%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90-%E6%8F%92%E4%BB%B6%E6%9C%BA%E5%88%B6/?
總結(jié)
以上是生活随笔為你收集整理的MyBatis 源码分析 - 插件机制的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MyBatis 源码分析 - 缓存原理
- 下一篇: MyBatis 源码分析系列文章合集