美團一面:你既然寫過Mybatis插件,說說它底層是怎麼加載一個自定義插件的

大多數框架,都支持插件,用戶可經過編寫插件來自行擴展功能,Mybatis也不例外。java

咱們從插件配置、插件編寫、插件運行原理、插件註冊與執行攔截的時機、初始化插件、分頁插件的原理等六個方面展開闡述。sql

1. 插件配置

Mybatis的插件配置在configuration內部,初始化時,會讀取這些插件,保存於Configuration對象的InterceptorChain中。整理了一份272頁Mybatis學習筆記apache

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <plugins>
    <plugin interceptor="com.mybatis3.interceptor.MyBatisInterceptor">
      <property name="value" value="100" />
    </plugin>
  </plugins>
</configuration>

public class Configuration {
    protected final InterceptorChain interceptorChain = new InterceptorChain();
}

org.apache.ibatis.plugin.InterceptorChain.java源碼。緩存

public class InterceptorChain {

  private final List<Interceptor> interceptors = new ArrayList<Interceptor>();

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

上面的for循環表明了只要是插件,都會以責任鏈的方式逐一執行(別期望它能跳過某個節點),所謂插件,其實就相似於攔截器。mybatis

2. 如何編寫一個插件

插件必須實現org.apache.ibatis.plugin.Interceptor接口。app

public interface Interceptor {
  
  Object intercept(Invocation invocation) throws Throwable;

  Object plugin(Object target);

  void setProperties(Properties properties);

}

intercept()方法:執行攔截內容的地方,好比想收點保護費。由plugin()方法觸發,interceptor.plugin(target)足以證實。框架

plugin()方法:決定是否觸發intercept()方法。ide

setProperties()方法:給自定義的攔截器傳遞xml配置的屬性參數。性能

下面自定義一個攔截器:學習

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

  private Integer value;

  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    return invocation.proceed();
  }

  @Override
  public Object plugin(Object target) {
    System.out.println(value);
        // Plugin類是插件的核心類,用於給target建立一個JDK的動態代理對象,觸發intercept()方法
    return Plugin.wrap(target, this);
  }

  @Override
  public void setProperties(Properties properties) {
    value = Integer.valueOf((String) properties.get("value"));
  }

}

面對上面的代碼,咱們須要解決兩個疑問:

1.爲何要寫Annotation註解?註解都是什麼含義?

答: Mybatis規定插件必須編寫Annotation註解,是必須,而不是可選。

@Intercepts註解:裝載一個@Signature列表,一個@Signature其實就是一個須要攔截的方法封裝。那麼,一個攔截器要攔截多個方法,天然就是一個@Signature列表。

type = Executor.class, 
method = "query", 
args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class }

解釋: 要攔截Executor接口內的query()方法,參數類型爲args列表。

2. Plugin.wrap(target, this)是幹什麼的?

答: 使用JDK的動態代理,給target對象建立一個delegate代理對象,以此來實現方法攔截和加強功能,它會回調intercept()方法。

org.apache.ibatis.plugin.Plugin.java源碼:

public class Plugin implements InvocationHandler {

  private Object target;
  private Interceptor interceptor;
  private 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);
    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;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      // 判斷是不是須要攔截的方法(很重要)
      if (methods != null && methods.contains(method)) {
        // 回調intercept()方法
        return interceptor.intercept(new Invocation(target, method, args));
      }
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }
//...
}

Map<Class<?>, Set> signatureMap:緩存需攔截對象的反射結果,避免屢次反射,即target的反射結果。

因此,咱們不要動不動就說反射性能不好,那是由於你沒有像Mybatis同樣去緩存一個對象的反射結果。

判斷是不是須要攔截的方法,這句註釋很重要,一旦忽略了,都不知道Mybatis是怎麼判斷是否執行攔截內容的,要記住。

3. Mybatis能夠攔截哪些接口對象?

public class Configuration {
//...
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler); // 1
    return parameterHandler;
  }

  public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
      ResultHandler resultHandler, BoundSql boundSql) {
    ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
    resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler); // 2
    return resultSetHandler;
  }

  public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); // 3
    return statementHandler;
  }

  public Executor newExecutor(Transaction transaction) {
    return newExecutor(transaction, defaultExecutorType);
  }

  public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor); // 4
    return executor;
  }
//...
}

Mybatis只能攔截ParameterHandler、ResultSetHandler、StatementHandler、Executor共4個接口對象內的方法。

從新審視interceptorChain.pluginAll()方法:該方法在建立上述4個接口對象時調用,其含義爲給這些接口對象註冊攔截器功能,注意是註冊,而不是執行攔截。

攔截器執行時機:plugin()方法註冊攔截器後,那麼,在執行上述4個接口對象內的具體方法時,就會自動觸發攔截器的執行,也就是插件的執行。

因此,必定要分清,什麼時候註冊,什麼時候執行。切不可認爲pluginAll()或plugin()就是執行,它只是註冊。

4. Invocation

public class Invocation {
  private Object target;
  private Method method;
  private Object[] args;
}

intercept(Invocation invocation)方法的參數Invocation ,我相信你必定能夠看得懂,不解釋。

5. 初始化插件源碼解析

org.apache.ibatis.builder.xml.XMLConfigBuilder.parseConfiguration(XNode)方法部分源碼。

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 = child.getChildrenAsProperties();
      Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
      // 這裏展現了setProperties()方法的調用時機
      interceptorInstance.setProperties(properties);
      configuration.addInterceptor(interceptorInstance);
    }
  }
}

對於Mybatis,它並不區分是何種攔截器接口,全部的插件都是Interceptor,Mybatis徹底依靠Annotation去標識對誰進行攔截,因此,具有接口一致性。

6. 分頁插件原理

因爲Mybatis採用的是邏輯分頁,而非物理分頁,那麼,市場上就出現了能夠實現物理分頁的Mybatis的分頁插件。

要實現物理分頁,就須要對String sql進行攔截並加強,Mybatis經過BoundSql對象存儲String sql,而BoundSql則由StatementHandler對象獲取。整理了一份272頁Mybatis學習筆記

public interface StatementHandler {
    <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException;
    BoundSql getBoundSql();
}

public class BoundSql {
   public String getSql() {
    return sql;
  }
}

所以,就須要編寫一個針對StatementHandler的query方法攔截器,而後獲取到sql,對sql進行重寫加強。

相關文章
相關標籤/搜索