1. 程式人生 > >精盡MyBatis原始碼分析 - 外掛機制

精盡MyBatis原始碼分析 - 外掛機制

> 該系列文件是本人在學習 Mybatis 的原始碼過程中總結下來的,可能對讀者不太友好,請結合我的原始碼註釋([Mybatis原始碼分析 GitHub 地址](https://github.com/liu844869663/mybatis-3)、[Mybatis-Spring 原始碼分析 GitHub 地址](https://github.com/liu844869663/spring)、[Spring-Boot-Starter 原始碼分析 GitHub 地址](https://github.com/liu844869663/spring-boot-starter))進行閱讀 > > MyBatis 版本:3.5.2 > > MyBatis-Spring 版本:2.0.3 > > MyBatis-Spring-Boot-Starter 版本:2.1.4 ## 外掛機制 開源框架一般都會提供外掛或其他形式的擴充套件點,供開發者自行擴充套件,增加框架的靈活性 當然,MyBatis 也提供了外掛機制,基於它開發者可以進行擴充套件,對 MyBatis 的功能進行增強,例如實現分頁、SQL分析、監控等功能,本文會對 MyBatis 外掛機制的原理以及如何實現一個自定義的外掛來進行講述 我們在編寫外掛時,除了需要讓外掛類實現 `org.apache.ibatis.plugin.Interceptor` 介面,還需要通過註解標註該外掛的攔截點,也就是外掛需要增強的方法,MyBatis 只提供下面這些類中定義的方法能夠被增強: - Executor:執行器 - ParameterHandler:引數處理器 - ResultSetHandler:結果集處理器 - StatementHandler:Statement 處理器 ### 植入外掛邏輯 在[**《MyBatis的SQL執行過程》**](https://www.cnblogs.com/lifullmoon/p/14015111.html)一系列文件中,有講到在建立Executor、ParameterHandler、ResultSetHandler和StatementHandler物件時,會呼叫`InterceptorChain`的`pluginAll`方法,遍歷所有的外掛,呼叫`Interceptor`外掛的`plugin`方法植入相應的外掛邏輯,所以在 MyBatis 中只有上面的四個物件中的方法可以被增強 程式碼如下: ```java // Configuration.java public Executor newExecutor(Transaction transaction, ExecutorType executorType) { // <1> 獲得執行器型別 executorType = executorType == null ? defaultExecutorType : executorType; executorType = executorType == null ? ExecutorType.SIMPLE : executorType; // <2> 建立對應實現的 Executor 物件 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); } // <3> 如果開啟快取,建立 CachingExecutor 物件,進行包裝 if (cacheEnabled) { executor = new CachingExecutor(executor); } // <4> 應用外掛 executor = (Executor) interceptorChain.pluginAll(executor); return executor; } public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { // 建立 ParameterHandler 物件 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) { // 建立 DefaultResultSetHandler 物件 ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds); // 應用外掛 resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler); return resultSetHandler; } public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql); // 將 Configuration 全域性配置中的所有外掛應用在 StatementHandler 上面 statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); return statementHandler; } ``` ### 分頁外掛示例 我們先來看一個簡單的外掛示例,程式碼如下: ```java @Intercepts({ @Signature( type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class} ) }) public class ExamplePlugin implements Interceptor { // Executor的查詢方法: // public List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) @Override public Object intercept(Invocation invocation) throws Throwable { Object[] args = invocation.getArgs(); RowBounds rowBounds = (RowBounds) args[2]; if (rowBounds == RowBounds.DEFAULT) { // 無需分頁 return invocation.proceed(); } /* * 將query方法的 RowBounds 入參設定為空物件 * 也就是關閉 MyBatis 內部實現的分頁(邏輯分頁,在拿到查詢結果後再進行分頁的,而不是物理分頁) */ args[2] = RowBounds.DEFAULT; MappedStatement mappedStatement = (MappedStatement) args[0]; BoundSql boundSql = mappedStatement.getBoundSql(args[1]); // 獲取 SQL 語句,拼接 limit 語句 String sql = boundSql.getSql(); String limit = String.format("LIMIT %d,%d", rowBounds.getOffset(), rowBounds.getLimit()); sql = sql + " " + limit; // 建立一個 StaticSqlSource 物件 SqlSource sqlSource = new StaticSqlSource(mappedStatement.getConfiguration(), sql, boundSql.getParameterMappings()); // 通過反射獲取並設定 MappedStatement 的 sqlSource 欄位 Field field = MappedStatement.class.getDeclaredField("sqlSource"); field.setAccessible(true); field.set(mappedStatement, sqlSource); // 執行被攔截方法 return invocation.proceed(); } @Override public Object plugin(Object target) { // default impl return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { // default nop } } ``` 在上面的分頁外掛中,`@Intercepts`和`@Signature`兩個註解指定了增強的方法是`Executor.query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler)`,也就是我們使用到的 Executor 執行資料庫查詢操作的方法 在實現的 `intercept` 方法中,通過 `RowBounds` 引數獲取分頁資訊,並生成相應的 SQL(拼接了 limit) ,並使用該 SQL 作為引數重新建立一個 `StaticSqlSource` 物件,最後通過反射替換 `MappedStatement` 物件中的 `sqlSource` 欄位,這樣就實現了一個簡單的分頁外掛 上面只是一個簡單的示例,實際場景中慎用 ### Interceptor `org.apache.ibatis.plugin.Interceptor`:攔截器介面,程式碼如下: ```java public interface Interceptor { /** * 攔截方法 * * @param invocation 呼叫資訊 * @return 呼叫結果 * @throws Throwable 若發生異常 */ Object intercept(Invocation invocation) throws Throwable; /** * 應用外掛。如應用成功,則會建立目標物件的代理物件 * * @param target 目標物件 * @return 應用的結果物件,可以是代理物件,也可以是 target 物件,也可以是任意物件。具體的,看程式碼實現 */ default Object plugin(Object target) { return Plugin.wrap(target, this); } /** * 設定攔截器屬性 * * @param properties 屬性 */ default void setProperties(Properties properties) { // NOP } } ``` - intercept方法:攔截方法,外掛的增強邏輯 - plugin方法:應用外掛,往目標物件中植入相應的外掛邏輯,如果應用成功則返回一個代理物件(JDK動態代理),否則返回原始物件,預設呼叫`Plugin`的`wrap`方法 - setProperties方法:設定攔截器屬性 ### Invocation `org.apache.ibatis.plugin.Invocation`:被攔截的物件資訊,程式碼如下: ```java 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; } // 省略 getter setter 方法 } ``` ### Plugin `org.apache.ibatis.plugin.Plugin`:實現InvocationHandler介面,用於對攔截的物件進行,一方面提供建立動態代理物件的方法,另一方面實現對指定類的指定方法的攔截處理,MyBatis外掛機制的核心類 #### 構造方法 ```java public class Plugin implements InvocationHandler { /** * 目標物件 */ private final Object target; /** * 攔截器 */ private final Interceptor interceptor; /** * 攔截的方法對映 * * KEY:類 * VALUE:方法集合 */ private final Map, Set> signatureMap; private Plugin(Object target, Interceptor interceptor, Map, Set> signatureMap) { this.target = target; this.interceptor = interceptor; this.signatureMap = signatureMap; } } ``` #### wrap方法 `wrap(Object target, Interceptor interceptor)`方法,建立目標類的代理物件,方法如下: ```java public static Object wrap(Object target, Interceptor interceptor) { // <1> 獲得攔截器中需要攔截的類的方法集合 Map, Set> signatureMap = getSignatureMap(interceptor); // <2> 獲得目標物件的 Class 物件 Class type = target.getClass(); // <3> 獲得目標物件所有需要被攔截的 Class 物件(父類或者介面) Class[] interfaces = getAllInterfaces(type, signatureMap); // <4> 若存在需要被攔截的,則為目標物件的建立一個動態代理物件(JDK 動態代理),代理類為 Plugin 物件 if (interfaces.length > 0) { // 因為 Plugin 實現了 InvocationHandler 介面,所以可以作為 JDK 動態代理的呼叫處理器 return Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); } // <5> 如果沒有,則返回原始的目標物件 return target; } ``` 1. 呼叫`getSignatureMap`方法,獲得攔截器中需要攔截的類的方法集合,有就是通過`@Intercepts`和`@Signature`兩個註解指定的增強的方法 2. 獲得目標物件的 Class 物件(父類或者介面) 3. 獲得目標物件所有需要被攔截的 Class 物件 4. 如果需要被攔截,則為目標物件的建立一個動態代理物件(JDK 動態代理),代理類為 `Plugin` 物件,並返回該動態代理物件 5. 否則返回原始的目標物件 #### getSignatureMap方法 `getSignatureMap(Interceptor interceptor)`方法,獲取外掛需要增強的方法,方法如下: ```java private static Map, Set> getSignatureMap(Interceptor interceptor) { // 獲取 @Intercepts 註解 Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class); // issue #251 if (interceptsAnnotation == null) { throw new PluginException( "No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName()); } // 獲取 @Intercepts 註解中的 @Signature 註解 Signature[] sigs = interceptsAnnotation.value(); Map, Set> signatureMap = new HashMap<>(); for (Signature sig : sigs) { // 為 @Signature 註解中定義類名建立一個方法陣列 Set methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>()); try { // 獲取 @Signature 註解中定義的方法物件 Method method = sig.type().getMethod(sig.method(), sig.args()); methods.add(method); } catch (NoSuchMethodException e) { throw new PluginException( "Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e); } } return signatureMap; } ``` - 通過該外掛上面的`@Intercepts`和`@Signature`註解,獲取到所有需要被攔截的物件中的需要增強的方法 #### getAllInterfaces方法 `getAllInterfaces(Class type, Map, Set> signatureMap)`方法,判斷目標物件是否需要被外掛應用,方法如下: ```java private static Class[] getAllInterfaces(Class type, Map, Set> signatureMap) { // 介面的集合 Set> interfaces = new HashSet<>(); // 迴圈遞迴 type 類,機器父類 while (type != null) { // 遍歷介面集合,若在 signatureMap 中,則新增到 interfaces 中 for (Class c : type.getInterfaces()) { if (signatureMap.containsKey(c)) { interfaces.add(c); } } // 獲得父類 type = type.getSuperclass(); } // 建立介面的陣列 return interfaces.toArray(new Class[interfaces.size()]); } ``` - 入參`signatureMap`就是`getSignatureMap`方法返回的該外掛需要增強的方法 - 返回存在於`signatureMap`集合中所有目標物件的父類或者介面 #### invoke方法 `invoke(Object proxy, Method method, Object[] args)`方法,動態代理物件的攔截方法,方法如下: ```java @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { // 獲得目標方法所在的類需要被攔截的方法 Set 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); } } ``` 1. 獲得目標方法所在的類需要被攔截的方法 2. 如果被攔截的方法包含當前方法,則將當前方法封裝成`Invocation`物件,呼叫`Interceptor`外掛的`intercept`方法,執行外掛邏輯 3. 否則執行原有方法 這樣一來,當你呼叫了目標物件的對應方法時,則會進入該外掛的`intercept`方法,執行外掛邏輯,擴充套件功能 ### InterceptorChain `org.apache.ibatis.plugin.InterceptorChain`:攔截器鏈,用於將所有的攔截器按順序將外掛邏輯植入目標物件,程式碼如下: ```java public class InterceptorChain { private final List interceptors = new ArrayList<>(); public Object pluginAll(Object target) { // 遍歷攔截器集合 for (Interceptor interceptor : interceptors) { // 呼叫攔截器的 plugin 方法植入相應的外掛邏輯 target = interceptor.plugin(target); } return target; } public void addInterceptor(Interceptor interceptor) { interceptors.add(interceptor); } public List getInterceptors() { return Collections.unmodifiableList(interceptors); } } ``` 配置MyBatis外掛都會儲存在`interceptors`集合中,可以回顧到**《初始化(一)之載入mybatis-config.xml》**的**XMLConfigBuilder**小節的`pluginElement`方法,會將解析到的依次全部新增到`Configuration`的`InterceptorChain`物件中,程式碼如下: ```java private void pluginElement(XNode parent) throws Exception { if (parent != null) { // 遍歷
標籤 for (XNode child : parent.getChildren()) { String interceptor = child.getStringAttribute("interceptor"); Properties properties = child.getChildrenAsProperties(); // <1> 建立 Interceptor 物件,並設定屬性 Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance(); interceptorInstance.setProperties(properties); // <2> 新增到 configuration 中 configuration.addInterceptor(interceptorInstance); } } } ``` ### 總結 本文分析了 MyBatis 中外掛機制,總體來說比較簡單的,想要實現一個外掛,需要實現 `Interceptor` 介面,並通過`@Intercepts`和`@Signature`兩個註解指定該外掛的攔截點(支援對Executor、ParameterHandler、ResultSetHandler 和 StatementHandler 四個物件中的方法進行增強),在實現的`intercept`方法中進行邏輯處理 在 MyBatis 初始化的時候,會掃描外掛,將其新增到`InterceptorChain`中 然後 MyBatis 在 SQL 執行過程中,建立上面四個物件的時候,會將建立的物件交由`InterceptorChain`去處理,遍歷所有的外掛,通過外掛的`plugin`方法為其建立一個動態代理物件並返回,代理類是`Plugin`物件 在`Plugin`物件中的`invoke`方法中,將請求交由外掛的`intercept`方法去處理 雖然 MyBatis 的外掛機制比較簡單,但是想要實現一個完善且高效的外掛卻比較複雜,可以參考[PageHelper分頁外掛](https://github.com/pagehelper/Mybatis-PageHelper) 到這裡,相信大家對 MyBatis 的外掛機制有了一定的瞭解,感謝大家的閱讀!!!:smile::smile::smile: > 參考文章:**芋道原始碼**[《精盡 MyBatis 原始碼分析》](http://svip.iocoder.cn/categories/M