上次發文說到了如何整合分頁外掛MyBatis外掛原理分析,看完感覺自己better了,今天我們接著來聊mybatis外掛的原理。
外掛原理分析mybatis外掛涉及到的幾個類:
我將以 Executor 為例,分析 MyBatis 是如何為 Executor 例項植入外掛的。Executor 例項是在開啟 SqlSession 是被建立的,因此,我們從源頭進行分析。先來看一下 SqlSession 開啟的過程。
public SqlSession openSession() { return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);}private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; try { // 省略部分邏輯 // 建立 Executor final Executor executor = configuration.newExecutor(tx, execType); return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) {...} finally {...}}
Executor 的建立過程封裝在 Configuration 中,我們跟進去看看看。
// Configuration類中public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor executor; // 根據 executorType 建立相應的 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 方法在建立好 Executor 例項後,緊接著透過攔截器鏈 interceptorChain 為 Executor 例項植入代理邏輯。那下面我們看一下 InterceptorChain 的程式碼是怎樣的。
public class InterceptorChain { private final List<Interceptor> interceptors = new ArrayList<Interceptor>(); public Object pluginAll(Object target) { // 遍歷攔截器集合 for (Interceptor interceptor : interceptors) { // 呼叫攔截器的 plugin 方法植入相應的外掛邏輯 target = interceptor.plugin(target); } return target; } /** 新增外掛例項到 interceptors 集合中 */ public void addInterceptor(Interceptor interceptor) { interceptors.add(interceptor); } /** 獲取外掛列表 */ public List<Interceptor> getInterceptors() { return Collections.unmodifiableList(interceptors); }}
上面的for迴圈代表了只要是外掛,都會以責任鏈的方式逐一執行(別指望它能跳過某個節點),所謂外掛,其實就類似於攔截器。
這裡就用到了責任鏈設計模式,責任鏈設計模式就相當於我們在OA系統裡發起審批,領導們一層一層進行審批。
以上是 InterceptorChain 的全部程式碼,比較簡單。它的 pluginAll 方法會呼叫具體外掛的 plugin 方法植入相應的外掛邏輯。如果有多個外掛,則會多次呼叫 plugin 方法,最終生成一個層層巢狀的代理類。形如下面:
當 Executor 的某個方法被呼叫的時候,外掛邏輯會先行執行。執行順序由外而內,比如上圖的執行順序為 plugin3 → plugin2 → Plugin1 → Executor。
plugin 方法是由具體的外掛類實現,不過該方法程式碼一般比較固定,所以下面找個示例分析一下。
// TianPlugin類public Object plugin(Object target) { return Plugin.wrap(target, this);}//Pluginpublic static Object wrap(Object target, Interceptor interceptor) { /* * 獲取外掛類 @Signature 註解內容,並生成相應的對映結構。形如下面: * { * Executor.class : [query, update, commit], * ParameterHandler.class : [getParameterObject, setParameters] * } */ Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); Class<?> type = target.getClass(); // 獲取目標類實現的介面 Class<?>[] interfaces = getAllInterfaces(type, signatureMap); if (interfaces.length > 0) { // 透過 JDK 動態代理為目標類生成代理類 return Proxy.newProxyInstance( type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); } return target;}
如上,plugin 方法在內部呼叫了 Plugin 類的 wrap 方法,用於為目標物件生成代理。Plugin 類實現了 InvocationHandler 介面,因此它可以作為引數傳給 Proxy 的 newProxyInstance 方法。
到這裡,關於外掛植入的邏輯就分析完了。接下來,我們來看看外掛邏輯是怎樣執行的。
執行外掛邏輯Plugin 實現了 InvocationHandler 介面,因此它的 invoke 方法會攔截所有的方法呼叫。invoke 方法會對所攔截的方法進行檢測,以決定是否執行外掛邏輯。該方法的邏輯如下:
//在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)) { // 執行外掛邏輯 return interceptor.intercept(new Invocation(target, method, args)); } // 執行被攔截的方法 return method.invoke(target, args); } catch (Exception e) { throw ExceptionUtil.unwrapThrowable(e); }}
invoke 方法的程式碼比較少,邏輯不難理解。首先,invoke 方法會檢測被攔截方法是否配置在外掛的 @Signature 註解中,若是,則執行外掛邏輯,否則執行被攔截方法。外掛邏輯封裝在 intercept 中,該方法的引數型別為 Invocation。Invocation 主要用於儲存目標類,方法以及方法引數列表。下面簡單看一下該類的定義。
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 { //反射呼叫被攔截的方法 return method.invoke(target, args); }}
關於外掛的執行邏輯就分析到這,整個過程不難理解,大家簡單看看即可。
自定義外掛下面為了讓大家更好的理解Mybatis的外掛機制,我們來模擬一個慢sql監控的外掛。
/** * 慢查詢sql 外掛 */@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})public class SlowSqlPlugin implements Interceptor { private long slowTime; //攔截後需要處理的業務 @Override public Object intercept(Invocation invocation) throws Throwable { //透過StatementHandler獲取執行的sql StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); BoundSql boundSql = statementHandler.getBoundSql(); String sql = boundSql.getSql(); long start = System.currentTimeMillis(); //結束攔截 Object proceed = invocation.proceed(); long end = System.currentTimeMillis(); long f = end - start; System.out.println(sql); System.out.println("耗時=" + f); if (f > slowTime) { System.out.println("本次資料庫操作是慢查詢,sql是:"); System.out.println(sql); } return proceed; } //獲取到攔截的物件,底層也是透過代理實現的,實際上是拿到一個目標代理物件 @Override public Object plugin(Object target) { //觸發intercept方法 return Plugin.wrap(target, this); } //設定屬性 @Override public void setProperties(Properties properties) { //獲取我們定義的慢sql的時間閾值slowTime this.slowTime = Long.parseLong(properties.getProperty("slowTime")); }}
然後把這個外掛類注入到容器中。
然後我們來執行查詢的方法。
耗時28秒的,大於我們定義的10毫秒,那這條SQL就是我們認為的慢SQL。
透過這個外掛,我們就能很輕鬆的理解setProperties()方法是做什麼的了。
回顧分頁外掛也是實現mybatis介面Interceptor。
@SuppressWarnings({"rawtypes", "unchecked"})@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 { ... }
intercept方法中
//AbstractHelperDialect類中@Overridepublic String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) { String sql = boundSql.getSql(); Page page = getLocalPage(); //支援 order by String orderBy = page.getOrderBy(); if (StringUtil.isNotEmpty(orderBy)) { pageKey.update(orderBy); sql = OrderByParser.converToOrderBySql(sql, orderBy); } if (page.isOrderByOnly()) { return sql; } //獲取分頁sql return getPageSql(sql, page, pageKey); }//模板方法模式中的鉤子方法 public abstract String getPageSql(String sql, Page page, CacheKey pageKey);
AbstractHelperDialect類的實現類有如下(也就是此分頁外掛支援的資料庫就以下幾種):
我們用的是MySQL。這裡也有與之對應的。
@Override public String getPageSql(String sql, Page page, CacheKey pageKey) { StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14); sqlBuilder.append(sql); if (page.getStartRow() == 0) { sqlBuilder.append(" LIMIT ? "); } else { sqlBuilder.append(" LIMIT ?, ? "); } pageKey.update(page.getPageSize()); return sqlBuilder.toString(); }
到這裡我們就知道了,它無非就是在我們執行的SQL上再拼接了Limit罷了。同理,Oracle也就是使用rownum來處理分頁了。下面是Oracle處理分頁
@Override public String getPageSql(String sql, Page page, CacheKey pageKey) { StringBuilder sqlBuilder = new StringBuilder(sql.length() + 120); if (page.getStartRow() > 0) { sqlBuilder.append("SELECT * FROM ( "); } if (page.getEndRow() > 0) { sqlBuilder.append(" SELECT TMP_PAGE.*, ROWNUM ROW_ID FROM ( "); } sqlBuilder.append(sql); if (page.getEndRow() > 0) { sqlBuilder.append(" ) TMP_PAGE WHERE ROWNUM <= ? "); } if (page.getStartRow() > 0) { sqlBuilder.append(" ) WHERE ROW_ID > ? "); } return sqlBuilder.toString(); }
其他資料庫分頁操作類似。關於具體原理分析,這裡就沒必要贅述了,因為分頁外掛原始碼裡註釋基本上全是中文。
Mybatis外掛應用場景水平分表許可權控制資料的加解密總結Spring-Boot+Mybatis繼承了分頁外掛,以及使用案例、外掛的原理分析、原始碼分析、如何自定義外掛。
涉及到技術點:JDK動態代理、責任鏈設計模式、模板方法模式。
Mybatis外掛關鍵物件總結:
Inteceptor介面:自定義攔截必須實現的類。InterceptorChain:存放外掛的容器。Plugin:h物件,提供建立代理類的方法。Invocation:對被代理物件的封裝。