大多數框架,都支持插件,用戶可通過撰寫插件來自行擴展功能,Mybatis也不例外,
我們從插件配置、插件撰寫、插件運行原理、插件注冊與執行攔截的時機、初始化插件、分頁插件的原理等六個方面展開闡述,
1. 插件配置
Mybatis的插件配置在configuration內部,初始化時,會讀取這些插件,保存于Configuration物件的InterceptorChain中,整理了一份272頁Mybatis學習筆記
<?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="https://www.cnblogs.com/bainannan/archive/2021/02/18/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介面,
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
void setProperties(Properties properties);
}
intercept()方法:執行攔截內容的地方,比如想收點保護費,由plugin()方法觸發,interceptor.plugin(target)足以證明,
plugin()方法:決定是否觸發intercept()方法,
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 = https://www.cnblogs.com/bainannan/archive/2021/02/18/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進行重寫增強,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/260878.html
標籤:其他
上一篇:SpringBoot進階教程(七十一)詳解Prometheus+Grafana
下一篇:JPG學習筆記5(附完整代碼)
