大多數框架,都支持插件,用戶可經過編寫插件來自行擴展功能,Mybatis也不例外。java
咱們從插件配置、插件編寫、插件運行原理、插件註冊與執行攔截的時機、初始化插件、分頁插件的原理等六個方面展開闡述。sql
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
插件必須實現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是怎麼判斷是否執行攔截內容的,要記住。
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()就是執行,它只是註冊。
public class Invocation {
private Object target;
private Method method;
private Object[] args;
}
intercept(Invocation invocation)方法的參數Invocation ,我相信你必定能夠看得懂,不解釋。
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去標識對誰進行攔截,因此,具有接口一致性。
因爲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進行重寫加強。