Mybatis3.4.x技術內幕(十九):Mybatis之plugin插件設計原理

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

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

1. 插件配置

Mybatis的插件配置在configuration內部,初始化時,會讀取這些插件,保存於Configuration對象的InterceptorChain中。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循環表明了只要是插件,都會以責任鏈的方式逐一執行(別期望它能跳過某個節點),所謂插件,其實就相似於攔截器。網絡

2. 如何編寫一個插件

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

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

  Object plugin(Object target);

  void setProperties(Properties properties);

}

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

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

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

下面自定義一個攔截器:性能

@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<Method>> 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對象獲取。

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進行重寫加強。

任它天高海闊,任它變化多端,咱們只要懂得原理,再多插件,咱們均可以對其投送王之蔑視。

版權提示:文章出自開源中國社區,若對文章感興趣,可關注個人開源中國社區博客(http://my.oschina.net/zudajun)。(通過網絡爬蟲或轉載的文章,常常丟失流程圖、時序圖,格式錯亂等,仍是看原版的比較好)

相關文章
相關標籤/搜索