溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

MyBatis插件的原理是什么

發(fā)布時(shí)間:2021-06-18 14:41:44 來源:億速云 閱讀:170 作者:Leah 欄目:web開發(fā)

本篇文章為大家展示了MyBatis插件的原理是什么,內(nèi)容簡明扼要并且容易理解,絕對(duì)能使你眼前一亮,通過這篇文章的詳細(xì)介紹希望你能有所收獲。

 插件原理分析

mybatis插件涉及到的幾個(gè)類:

MyBatis插件的原理是什么

我將以 Executor 為例,分析 MyBatis 是如何為 Executor 實(shí)例植入插件的。Executor 實(shí)例是在開啟 SqlSession  時(shí)被創(chuàng)建的,因此,我們從源頭進(jìn)行分析。先來看一下 SqlSession 開啟的過程。

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);     } }

上面的for循環(huán)代表了只要是插件,都會(huì)以責(zé)任鏈的方式逐一執(zhí)行(別指望它能跳過某個(gè)節(jié)點(diǎn)),所謂插件,其實(shí)就類似于攔截器。

這里就用到了責(zé)任鏈設(shè)計(jì)模式,責(zé)任鏈設(shè)計(jì)模式就相當(dāng)于我們?cè)贠A系統(tǒng)里發(fā)起審批,領(lǐng)導(dǎo)們一層一層進(jìn)行審批。

以上是 InterceptorChain 的全部代碼,比較簡單。它的 pluginAll 方法會(huì)調(diào)用具體插件的 plugin  方法植入相應(yīng)的插件邏輯。如果有多個(gè)插件,則會(huì)多次調(diào)用 plugin 方法,最終生成一個(gè)層層嵌套的代理類。形如下面:

MyBatis插件的原理是什么

當(dāng) Executor 的某個(gè)方法被調(diào)用的時(shí)候,插件邏輯會(huì)先行執(zhí)行。執(zhí)行順序由外而內(nèi),比如上圖的執(zhí)行順序?yàn)?plugin3 &rarr; plugin2 &rarr;  Plugin1 &rarr; Executor。

plugin 方法是由具體的插件類實(shí)現(xiàn),不過該方法代碼一般比較固定,所以下面找個(gè)示例分析一下。

// TianPlugin類 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í)行的。

執(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ù)列表。下面簡單看一下該類的定義。

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è)過程不難理解,大家簡單看看即可。

自定義插件

下面為了讓大家更好的理解Mybatis的插件機(jī)制,我們來模擬一個(gè)慢sql監(jiān)控的插件。

/**  * 慢查詢sql 插件  */ @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})}) public class SlowSqlPlugin implements Interceptor {      private long slowTime;      //攔截后需要處理的業(yè)務(wù)     @Override     public Object intercept(Invocation invocation) throws Throwable {         //通過StatementHandler獲取執(zhí)行的sql         StatementHandler statementHandler = (StatementHandler) invocation.getTarget();         BoundSql boundSql = statementHandler.getBoundSql();         String sql = boundSql.getSql();          long start = System.currentTimeMillis();         //結(jié)束攔截         Object proceed = invocation.proceed();         long end = System.currentTimeMillis();         long f = end - start;         System.out.println(sql);         System.out.println("耗時(shí)=" + f);         if (f > slowTime) {             System.out.println("本次數(shù)據(jù)庫操作是慢查詢,sql是:");             System.out.println(sql);         }         return proceed;     }      //獲取到攔截的對(duì)象,底層也是通過代理實(shí)現(xiàn)的,實(shí)際上是拿到一個(gè)目標(biāo)代理對(duì)象     @Override     public Object plugin(Object target) {         //觸發(fā)intercept方法         return Plugin.wrap(target, this);     }      //設(shè)置屬性     @Override     public void setProperties(Properties properties) {         //獲取我們定義的慢sql的時(shí)間閾值slowTime         this.slowTime = Long.parseLong(properties.getProperty("slowTime"));     } }

然后把這個(gè)插件類注入到容器中。

MyBatis插件的原理是什么

然后我們來執(zhí)行查詢的方法。

MyBatis插件的原理是什么

耗時(shí)28秒的,大于我們定義的10毫秒,那這條SQL就是我們認(rèn)為的慢SQL。

通過這個(gè)插件,我們就能很輕松的理解setProperties()方法是做什么的了。

回顧分頁插件

也是實(shí)現(xiàn)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方法中

MyBatis插件的原理是什么

//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類的實(shí)現(xiàn)類有如下(也就是此分頁插件支持的數(shù)據(jù)庫就以下幾種):

MyBatis插件的原理是什么

我們用的是MySQL。這里也有與之對(duì)應(yīng)的。

@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();    }

到這里我們就知道了,它無非就是在我們執(zhí)行的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();     }

其他數(shù)據(jù)庫分頁操作類似。關(guān)于具體原理分析,這里就沒必要贅述了,因?yàn)榉猪摬寮创a里注釋基本上全是中文。

Mybatis插件應(yīng)用場景

  • 水平分表

  • 權(quán)限控制

  • 數(shù)據(jù)的加解密

總結(jié)

Spring-Boot+Mybatis繼承了分頁插件,以及使用案例、插件的原理分析、源碼分析、如何自定義插件。

涉及到技術(shù)點(diǎn):JDK動(dòng)態(tài)代理、責(zé)任鏈設(shè)計(jì)模式、模板方法模式。

Mybatis插件關(guān)鍵對(duì)象總結(jié):

  • Inteceptor接口:自定義攔截必須實(shí)現(xiàn)的類。

  • InterceptorChain:存放插件的容器。

  • Plugin:h對(duì)象,提供創(chuàng)建代理類的方法。

  • Invocation:對(duì)被代理對(duì)象的封裝。

上述內(nèi)容就是MyBatis插件的原理是什么,你們學(xué)到知識(shí)或技能了嗎?如果還想學(xué)到更多技能或者豐富自己的知識(shí)儲(chǔ)備,歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI