面試官:說一下Mybatis插件的實現原理?

  • 2020 年 2 月 26 日
  • 筆記

介紹

我之前有篇文章大概寫了一下mybatis插件的實現原理

Mybatis框架和插件將動態代理玩出了新境界

Mybaits插件的實現主要用了責任鏈模式和動態代理

動態代理可以對SQL語句執行過程中的某一點進行攔截,當配置多個插件時,責任鏈模式可以進行多次攔截,責任鏈模式的UML圖如下

可以看到在一條責任鏈中,每個Handler對象都包含對下一個Handler對象的引用,一個Handler對象處理完消息會把請求傳給下一個Handler對象繼續處理,以此類推,直至整條責任鏈結束。這時我們可以改變Handler的執行順序,增加或者刪除Handler,符合開閉原則

寫一個Mybatis插件

mybatis可以攔截如下方法的調用

  1. Executor(update,query,flushStatements,commit,rollback,getTransaction,close,isClosed)
  2. ParameterHandler(getParameterObject,setParameters)
  3. ResultSetHandler(handleResultSets,handleOutputParameters)
  4. StatementHandler(prepare,parameterize,batch,update,query)

至於為什麼是這些對象,後面會提到

寫一個列印SQL執行時間的插件

@Intercepts({@Signature(type = StatementHandler.class, method = "query", args = { Statement.class, ResultHandler.class }),          @Signature(type = StatementHandler.class, method = "update", args = { Statement.class }),          @Signature(type = StatementHandler.class, method = "batch", args = { Statement.class })})  public class SqlCostTimeInterceptor implements Interceptor {        public static final Logger logger = LoggerFactory.getLogger(SqlCostTimeInterceptor.class);        public Object intercept(Invocation invocation) throws Throwable {          StatementHandler statementHandler = (StatementHandler) invocation.getTarget();          long start = System.currentTimeMillis();          try {              // 執行被攔截的方法              return invocation.proceed();          } finally {              BoundSql boundSql = statementHandler.getBoundSql();              String sql = boundSql.getSql();              long end = System.currentTimeMillis();              long cost = end - start;              logger.info("{}, cost is {}", sql, cost);          }      }        public Object plugin(Object target) {          return Plugin.wrap(target, this);      }        public void setProperties(Properties properties) {        }  }  

在mybatis配置文件中配置插件

<plugins>      <plugin interceptor="com.javashitang.part1.plugins.SqlCostTimeInterceptor"></plugin>  </plugins>  

此時就可以列印出執行的SQL和耗費的時間,效果如下

select id, role_name as roleName, note from role where id = ?, cost is 35  

原理分析

前面說過Mybatis是通過動態代理的方式來額外增加功能的,因此調用目標對象的方法後走的是代理對象的方法而不是原方法

說到這你可以能意識到攔截器只需要實現InvocationHandler介面就行了,先對指定對象生成一個代理類,然後在InvocationHandler的invoke方法中對指定方法進行增強。

但繼承InvocationHandler後,生成代理類,並對指定方法進行增強這不是個累活么,框架完全可以再幫你封裝一下啊

於是就有了@Intercepts註解,裡面主要放多個@Signature註解,而@Signature註解則定義了要攔截的類和方法、

並且提供了Interceptor介面和Plugin類方便你實現動態代理,來看看他們怎麼配合使用的吧

我們先從Interceptor介面來分析,因為要插件必須要實現Interceptor介面

public interface Interceptor {      /** 執行攔截邏輯的方法,Invocation只是將動態代理中獲取到的一些參數封裝成一個對象 */    Object intercept(Invocation invocation) throws Throwable;      /**     * target是被攔截的對象,它的作用是給被攔截對象生成一個代理對象,並返回它。     * 為了方便,可以直接使用Mybatis中org.apache.ibatis.plugin.Plugin類的wrap方法(是靜態方法)生成代理對象     */    Object plugin(Object target);      /** 根據配置初始化Interceptor對象 */    void setProperties(Properties properties);    }  

其中plugin方法就是生成代理對象的,一般的做法是直接調用Plugin.wrap(target, this);方法來生成代理對象,到Plugin類裡面看看,主要的方法如下

public class Plugin implements InvocationHandler {      /** 目標對象 */    private final Object target;    /** Interceptor對象 */    private final Interceptor interceptor;    /** 記錄了@Signature註解中的資訊 */    /** 被攔截的type->被攔截的方法 */    private final Map<Class<?>, Set<Method>> signatureMap;      private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {      this.target = target;      this.interceptor = interceptor;      this.signatureMap = signatureMap;    }      public static Object wrap(Object target, Interceptor interceptor) {      // 拿到攔截器要攔截的類及其方法      Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);      // 取得要改變行為的類 (ParameterHandler | ResultSetHandler | StatementHandler | Executor)      Class<?> type = target.getClass();      // 拿到被代理對象的攔截方法,所實現的介面      Class<?>[] interfaces = getAllInterfaces(type, signatureMap);      // 如果當前傳入的Target的介面中有@Intercepts註解中定義的介面,那麼為之生成代理,否則原Target返回      if (interfaces.length > 0) {        return Proxy.newProxyInstance(            type.getClassLoader(),            interfaces,            new Plugin(target, interceptor, signatureMap));      }      return target;    }      @Override    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {      try {        // 獲取當前方法所在類或介面中,可被當前 Interceptor 攔截的方法        Set<Method> methods = signatureMap.get(method.getDeclaringClass());        // 如果當前調用的方法需要被攔截,則調用interceptor.intercept()方法進行攔截處理        if (methods != null && methods.contains(method)) {          return interceptor.intercept(new Invocation(target, method, args));        }        // 如果當前調用的方法不能被攔截,則調用target對象的相應方法        return method.invoke(target, args);      } catch (Exception e) {        throw ExceptionUtil.unwrapThrowable(e);      }    }  }  

到現在為止,實現代理類和攔截特定方法用一個Plugin.wrap()方法就搞定了,賊方便。

在Plugin.invoke()方法中,最終調用了Interceptor介面的intercept方法,並把目標類,目標方法,參數封裝成一個Invocation對象

return interceptor.intercept(new Invocation(target, method, args));  

接著看Invocation的定義

/**   * @author Clinton Begin   * 將要調用的類,方法,參數封裝成一個對象,方便傳遞給攔截器   */  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 getTarget() {      return target;    }      public Method getMethod() {      return method;    }      public Object[] getArgs() {      return args;    }      /** 這個方法是給攔截器調用的,攔截器最後會調用這個方法來執行本來要執行的方法,這樣就可以在方法前後加上攔截的邏輯了 */    public Object proceed() throws InvocationTargetException, IllegalAccessException {      return method.invoke(target, args);    }    }  

只有一個方法proceed()方法,而proceed()只是執行被攔截的方法,這時清楚了應該在Interceptor對象的intercept方法中做哪些操作了,只需要寫增強的邏輯,最後調用Invocation對象的proceed()方法即可

至此我們已經大概理解了插件的工作原理,只差最後一步了,給目標對象生成代理對象,我們從Mybatis初始化找答案

在配置文件中配置插件的格式如下,interceptor填全類名,下面可以寫多個key和value值,實現了Interceptor介面後,會有一個setProperties方法,會把這些屬性值封裝成一個Properties對象,設置進來

<plugin interceptor="">      <property name="" value=""/>      <property name="" value=""/>  </plugin>  

mybatis配置文件的解析在XMLConfigBuilder的parseConfiguration方法中,這裡我們只看一下插件的解析過程

pluginElement(root.evalNode("plugins"));  
  private void pluginElement(XNode parent) throws Exception {      if (parent != null) {        for (XNode child : parent.getChildren()) {          String interceptor = child.getStringAttribute("interceptor");          // 解析攔截器中配置的屬性,並封裝成一個Properties對象          Properties properties = child.getChildrenAsProperties();          // 通過類名示例化一個Interceptor對象          Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();          // 可以給攔截器的Properties屬性賦值          interceptorInstance.setProperties(properties);          configuration.addInterceptor(interceptorInstance);        }      }    }  

實例化好的Interceptor對象,會被放到InterceptorChain對象的interceptors屬性中

public class InterceptorChain {      private final List<Interceptor> interceptors = new ArrayList<Interceptor>();      /** 這裡有個特別有意思的地方,先添加的攔截器最後才會執行,因為代理是一層一層套上去的,就像這個函數f(f(f(x))) */    public Object pluginAll(Object target) {      for (Interceptor interceptor : interceptors) {        target = interceptor.plugin(target);      }      return target;    }      public void addInterceptor(Interceptor interceptor) {      interceptors.add(interceptor);    }      public List<Interceptor> getInterceptors() {      return Collections.unmodifiableList(interceptors);    }    }  

InterceptorChain對象的pluginAll方法不就是用來生成代理對象的嗎?看看在哪調用了

parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);  resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);  statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);  executor = (Executor) interceptorChain.pluginAll(executor);  

這不就是前面提到的mybatis只能攔截特定類的原因嗎?因為只對這些類做了代理。

至此Mybatis插件的原理就分析完了,還是挺簡單的。但是要寫一個實用的Mybatis插件並不容易,因為你要明白ParameterHandler等類的方法做了哪些事情,應該如何進行增強

最後總結一下,@Signature註解主要用來定義要攔截的類及其方法,而Interceptor介面和Plugin來配合為指定對象生成代理對象,並攔截指定方法,這樣就能在其前後做一些額外操作