Mybatis的插件設(shè)計源碼分析

在這里插入圖片描述

Mybatis的插件設(shè)計你知道多少?

本文主要分為兩部分,第一部分我們看插件設(shè)計原理和如何從 Mybatis 中學(xué)習(xí)設(shè)計插件,第二部分我們學(xué)習(xí)如何開發(fā)Mybatis插件。

一、插件設(shè)計原理

Mybatis 中的插件都是通過代理方式來實現(xiàn)的,通過攔截執(zhí)行器中指定的方法來達到改變核心執(zhí)行代碼的方式。舉一個列子,查詢方法核心都是通過 Executor來進行sql執(zhí)行的。那么我們就可以通過攔截下面的方法來改變核心代碼?;驹砭褪沁@樣,下面我們在來看 Mybatis 是如何處理插件。

public interface Executor {

  ResultHandler NO_RESULT_HANDLER = null;

  int update(MappedStatement ms, Object parameter) throws SQLException;

  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;

  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;

  <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;
  ...
}
在這里插入圖片描述
名稱 類型 描述
Interceptor 接口 插件都需要實現(xiàn)的接口,封裝代理執(zhí)行方法及參數(shù)信息
InterceptorChain 攔截鏈
InvocationHandler 接口 JDK代理的接口,凡是JDK中的代理都要實現(xiàn)該接口
@Intercepts 注解 用于聲明要代理和 @Signature 配合使用
@Signature 注解 用于聲明要代理攔截的方法
Plugin 代理的具體生成類

1. Interceptor

插件都需要實現(xiàn)的接口,封裝代理執(zhí)行方法及參數(shù)信息

public interface Interceptor {
    // 執(zhí)行方法體的封裝,所有的攔截方法邏輯都在這里面寫。
  Object intercept(Invocation invocation) throws Throwable;
    // 如果要代理,就用Plugin.wrap(...),如果不代理就原樣返回
  Object plugin(Object target);
    // 可以添加配置,主要是xml配置時候可以從xml中讀取配置信息到攔截器里面自己解析
  void setProperties(Properties properties);
}

2. InterceptorChain

攔截鏈,為什么需要攔截鏈,假如我們要對A進行代理, 具體的代理類有B和C。 我們要同時將B和C的邏輯都放到代理類里面,那我們會首先將A和B生成代理類,然后在前面生成代理的基礎(chǔ)上將C和前面生成的代理類在生成一個代理對象。這個類就是要做這件事 pluginAll

public class InterceptorChain {

  private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
  
  // 這里target就是A,而List中的Interceptor就相當(dāng)于B和C,通過循環(huán)方式生成統(tǒng)一代理類
  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      //1. 是否需要代理,需要代理生成代理類放回,不需要原樣返回。通過for循環(huán)的方式將所有對應(yīng)的插件整合成一個代理對象
      target = interceptor.plugin(target);
    }
    return target;
  }
  ...
}

3. InvocationHandler

JDK代理的接口,凡是JDK中的代理都要實現(xiàn)該接口。這個比較基礎(chǔ),如果這個不清楚,那么代理就看不懂了。所以就不說了。

public interface InvocationHandler {
      public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

4. @Intercepts@Signature

這兩個注解是配合使用的,用于指定要代理的類和方法。前面①說了,插件的核心邏輯是攔截執(zhí)行器的方法,那么這里我們看下如何聲明要攔截的類和方法。我們看一下分頁插件如何聲明攔截。

Signaturetype 就是要攔截的類, method 要攔截的方法, args 要攔截的方法的入?yún)?因為有相同的方法,所以要指定攔截的方法和方法參數(shù))

@Intercepts(@Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,
        RowBounds.class, ResultHandler.class }))
public class MybatisPagerPlugin implements Interceptor {
}

args 要攔截的方法的入?yún)?因為有相同的方法,所以要指定攔截的方法和方法參數(shù))
比如 Executor 中就有2個 query 方法。所以要通過args來確定要攔截哪一個。

在這里插入圖片描述

Mybatis這種插件管理模式, 在 Mybatis 的架構(gòu)中, 是有指定的,并不是說可以攔截任何類的任何方法,。它具體可以攔截什么類及方法,我們可以通過閱讀官方文檔 查看。

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

但是這種插件管理模式我們項目中也是可以用的。比如看下面例子。

public class Test {
    public static void main(String[] args) {
        InterceptorChain chain = new InterceptorChain();
        PrintInterceptor printInterceptor = new PrintInterceptor();
        Properties properties = new Properties();
        properties.setProperty("name","https://blog.springlearn.cn");
        printInterceptor.setProperties(properties);
        chain.addInterceptor(printInterceptor);
        Animal person = (Animal) chain.pluginAll(new Person());
        String nihao = person.say("nihao");
        System.out.println(nihao);
    }

    public interface Animal{
        String say(String message);
        String say(String name, String message);
    }

    public static class Person implements Animal {
        public String say(String message) {
            return message;
        }

        public String say(String name, String message) {
            return name + " say: " + message;
        }
    }

    @Intercepts(@Signature(type = Animal.class, method = "say", args = {String.class}))
    public static class PrintInterceptor implements Interceptor {
        private String name;

        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            System.out.println(name + ": before print ...");
            Object proceed = invocation.proceed();
            System.out.println(name + ": after print ...");
            return proceed;
        }

        @Override
        public Object plugin(Object target) {
            if (target instanceof Person) {
                return Plugin.wrap(target, this);
            }
            return target;
        }

        @Override
        public void setProperties(Properties properties) {
            this.name = properties.getProperty("name");
        }
    }
}

5. Plugin

代理的具體生成類,解析 @Intercepts@Signature 注解生成代理。

我們看幾個重要的方法。

方法名 處理邏輯
getSignatureMap 解析@Intercepts和@Signature,找到要攔截的方法
getAllInterfaces 找到代理類的接口,jdk代理必須要有接口
invoke 是否需要攔截判斷
public class Plugin implements InvocationHandler {
  
  //解析@Intercepts和@Signature找到要攔截的方法
  private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    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());      
    }
    Signature[] sigs = interceptsAnnotation.value();
    Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
    for (Signature sig : sigs) {
      Set<Method> methods = signatureMap.get(sig.type());
      if (methods == null) {
        methods = new HashSet<Method>();
        signatureMap.put(sig.type(), methods);
      }
      try {
        //通過方法名和方法參數(shù)查找方法
        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;
  }
  
  //因為是jdk代理所以必須要有接口,如果沒有接口,就不會生成代理
  private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
    Set<Class<?>> interfaces = new HashSet<Class<?>>();
    while (type != null) {
      for (Class<?> c : type.getInterfaces()) {
        if (signatureMap.containsKey(c)) {
          interfaces.add(c);
        }
      }
      type = type.getSuperclass();
    }
    return interfaces.toArray(new Class<?>[interfaces.size()]);
  }
  
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      //執(zhí)行時候看當(dāng)前執(zhí)行的方法是否需要被攔截,如果需要就調(diào)用攔截器中的方法
      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);
    }
  }
}

6. 總結(jié)

以上就是本篇文章的第一部分,主要講 "插件設(shè)計原理和如何從 Mybatis 中學(xué)習(xí)設(shè)計插件“

原理: 代理 ,并通過 @Intercepts@Signature 配合指定要代理的方法。 注意Mybatis中那些類能指定是有限制的哦。

在這里插入圖片描述

我們可以通過閱讀官方文檔 查看。

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

Mybatis 的插件模式,我們在項目中可以直接引入使用??梢詤⒖忌厦娴睦印?/p>

二、如何開發(fā)Mybatis插件代碼

如何開發(fā) Mybatis 插件,首先要知道原理, Mybatis 的原理前面就說了就是代理核心類的核心方法。前面我們也知道如何定義一個插件了。即就是用 @Intercepts@Signature 來聲明要攔截的類和方法。 但是知道這些只能說會定義插件了,具體插件代碼怎么寫。我們要在看下 Mybatis 官方限制的那幾個類都有什么能力。

在這里插入圖片描述

圖片描述的不是很具體,但是大概意思是這樣。 下面會一一簡述。

1. Executor

public interface Executor {

  int update(MappedStatement ms, Object parameter) throws SQLException;

  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;

}

數(shù)據(jù)庫操作的第一步就是先調(diào)用 Executor , 如果要對sql語句進行增強 ,或者說是所有操作都進行增強都可以再這個里面處理。

2. ParameterHandler

sql入?yún)谶@里被解析并進行操作,哎呀,這么說真的太抽象了。舉例來說

public interface UserMapper {

    @Insert("insert into bbs_role (role_id,role_name,created_date,updated_date,created_by,updated_by) values(#{user" +
            ".roleId}," +
            "#{user.roleName},#{user.createdDate},#{user.updatedDate},#{user.createdBy},#{user.updatedBy})")
    Integer insert(@Param("user") User user);
}

insert 方法中的user對象,如何填充到 sql 中,就是在 ParameterHandler 里面完成的。

  1. 第一步將sql中占位符替換成 ? 符號, 然后解析參數(shù)類型到 ParameterMapping

    在這里插入圖片描述

    最終這些信息都會在 BoundSql 中保存。 總的來說 Sql信息(包括入?yún)⒌男畔?都會放在 BoundSql 中保存。 這里我們認(rèn)識了一個在ORM框架中非常重要的一個類
    BoundSql 如果想動態(tài)的修改sql就要跟著這個類的步伐。

  2. 將已經(jīng)解析好的sql提交給 PreparedStatement 進行處理。
    ParameterHandler 重要的一步就是將 BoundSql 里面的sql及入?yún)⒌姆诺?PreparedStatement 里面進行數(shù)據(jù)查詢或者其他操作。 PreparedStatement 不解釋了,學(xué)JDBC的時候老師應(yīng)該都講過了。

如果要對sql到PreparedStatement的過程進行增強就可以代理整個類。

3. StatementHandler

在這里插入圖片描述
在這里插入圖片描述

代理 StatementHandler 能做什么?

前面 ParameterHandler 已經(jīng)可以將Sql信息寫入到 Statement 中,但是調(diào)用的邏輯就在 StatementHandler里面來處理了。如果要對這部分代碼做處理就可以攔截該方法。

在這里插入圖片描述

4. ResultSetHandler

從名字就知道這個是對數(shù)據(jù)庫查詢后的記過進行處理的一個類。就是將jdbc的API返回數(shù)據(jù)轉(zhuǎn)換成方法簽名中的返回值。

public interface UserMapper {
    @Select("select * from bbs_role")
    List<User> query();
}

這里就是將 Statement 返回值轉(zhuǎn)換成 List<User>

以上就是Mybatis給我們提供插件增強的地方,以及每個地方要做的事情

但是到這里真的會寫插件了嗎? 我們還必須要參與實踐。如果我們要做一個功能將數(shù)據(jù)庫的sql信息打印出來,應(yīng)該知道在哪里處理了吧,只要獲取BoundSql對象打印sql即可。如果我們要寫分頁那就是對sql后面加上分頁的語法,這些說起來簡單,其實并不簡單,因為 Mybatis 提供對很多數(shù)據(jù)庫的支持, 每個數(shù)據(jù)庫的語法可能還不一樣,所以在寫插件時候要考慮的東西還是很多的, 如果我們不需要寫插件,也沒興趣做開源項目其實了解到這里已經(jīng)可以了。

但是如果感興趣的話可以關(guān)注我哦!

image

感謝您的閱讀,本文由 程序猿升級課 版權(quán)所有。如若轉(zhuǎn)載,請注明出處:程序猿升級課(https://blog.springlearn.cn/

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容