您好,登錄后才能下訂單哦!
本篇文章為大家展示了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); } //Plugin public 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類中 @Override public 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:對被代理對象的封裝。
上述內容就是MyBatis插件的原理是什么,你們學到知識或技能了嗎?如果還想學到更多技能或者豐富自己的知識儲備,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。