本篇文章是「深入淺出MyBatis:技術原理與實踐」書籍的總結筆記。
上一篇介紹了 MyBatis解析和執行原理 ,包括SqlSessionFactory的構建和SqlSession的執行過程,其中,SqlSession包含四大物件,可以在四大物件排程的時候插入自定義的程式碼,以滿足特殊的需求,這便是MyBatis提供的外掛技術。
系列索引:
有些特殊場景,需要使用外掛統一處理,比如:在進行多租戶開發時,資料要按租戶隔離,可以在sql語句後面統一新增租戶編號篩選條件。
本篇就來介紹下外掛,通過本篇的介紹,你會了解到:
- 外掛介面和初始化
- 外掛的代理和反射設計
- 工具類MetaObject介紹
- 外掛的開發過程
外掛的介面和初始化分析
外掛介面
在MyBatis中使用外掛,需要實現Interceptor介面,定義如下:
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
void setProperties(Properties properties);
}
複製程式碼
詳細說說這3個方法:
- intercept:它將直接覆蓋你所攔截的物件,有個引數Invocation物件,通過該物件,可以反射排程原來物件的方法;
- plugin:target是被攔截的物件,它的作用是給被攔截物件生成一個代理物件;
- setProperties:允許在plugin元素中配置所需引數,該方法在外掛初始化的時候會被呼叫一次;
外掛初始化
外掛的初始化時在MyBatis初始化的時候完成的,讀入外掛節點和配置的引數,使用反射技術生成外掛例項,然後呼叫外掛方法中的setProperties方法設定引數,並將外掛例項儲存到配置物件中,具體過程看下面程式碼。
plugin配置示例如下:
<plugins>
<plugin interceptor="com.qqdong.study.mybatis.TenantPlugin">
<property name="dbType" value="mysql"/>
</plugin>
<plugins>
複製程式碼
外掛初始化過程:
public class XMLConfigBuilder extends BaseBuilder {
......
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();
interceptorInstance.setProperties(properties);
configuration.addInterceptor(interceptorInstance);
}
}
}
......
}
複製程式碼
配置物件Configuration的新增外掛方法:
public class Configuration {
protected final InterceptorChain interceptorChain = new InterceptorChain();
public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}
}
複製程式碼
InterceptorChain是一個類,主要包含一個List屬性,儲存Interceptor物件:
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);
}
}
複製程式碼
外掛的代理和反射設計原理
責任鏈模式
外掛用的是責任鏈模式,責任鏈模式是一種物件行為模式。在責任鏈模式裡,很多物件由每一個物件對其下家的引用而連線起來形成一條鏈,請求在這個鏈上傳遞,直到鏈上的某一個物件決定處理此請求。
設計細節
前面提到了InterceptorChain類,其中有個pluginAll方法,責任鏈就是在該方法定義的。
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
複製程式碼
上面介紹過plugin方法,它是生成代理物件的方法,從第一個物件(四大物件中的一個)開始,將物件傳遞給了plugin方法,返回一個代理;如果存在第二個外掛,就拿著第一個代理物件,傳遞給plugin方法,返回第一個代理物件的代理.....
plugin方法是需要我們去實現的,如何生成代理類呢,MyBatis提供了Plugin工具類,它實現了InvocationHandler介面(JDK動態代理的介面),看看它的2個方法:
public class Plugin implements InvocationHandler {
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) {
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)) {
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
}
複製程式碼
分析下這塊程式碼,Plugin提供了靜態方法wrap方法,它會根據外掛的簽名配置,使用JDK動態代理的方法,生成一個代理類,當四大物件執行方法時,會呼叫Plugin的invoke方法,如果方法包含在宣告的簽名裡,就會呼叫自定義外掛的intercept方法,傳入Invocation物件。
另外,Invocation物件包含一個proceed方法,這個方法就是呼叫被代理物件的真實方法,如果有n個外掛,第一個傳遞的引數是四大物件本身,然後呼叫一次wrap方法產生第一個代理物件,這裡的反射就是四大物件的真實方法,如果有第二個外掛,這裡的反射就是第一個代理物件的invoke方法。
所以,在多個外掛的情況下,排程proceed方法,MyBatis總是從最後一個代理物件執行到第一個代理物件,最後是真實被攔截的物件方法被執行。
工具類MetaObject介紹
MetaObject是MyBatis給我們提供的工具類,它可以有效的獲取或修改一些重要物件的屬性。
舉例說明,我們攔截StatementHandler物件,首先要獲取它要執行的SQL,新增返回行數限制。
編寫一個自定義外掛,實現intercept方法,方法實現如下
StatementHandler statementHandler=(StatementHandler)invocation.getTarget();
MetaObject metaObj=SystemMetaObject.forObject(statementHandler);
//獲取sql
String sql=(String)metaStatementHandler.getValue("delegate.bound.sql");
//新增limit條件
sql="select * from (" + sql + ") limit 1000";
//重新設定sql
metaStatementHandler.setValue("delegate.bound.sql",sql);
複製程式碼
外掛的開發過程
最後總結下外掛的開發步驟。
確定要攔截的簽名
- 確定要攔截的物件,四大物件之一;
- 確定攔截的方法和引數;
比如想攔截StatementHandler物件的prepare方法,該方法有一個引數Connection物件,可以這樣宣告:
@Intercepts({
@Signature(type =StatementHandler.class,
method="prepare" ,
args={Connection.class})})
public class MyPlugin implements Interceptor{
......
}
複製程式碼
定義外掛類,實現攔截方法
上面已經分析過原理,實現Interceptor介面的方法即可,通過Plugin工具類方便生成代理類,通過MetaObject工具類方便操作四大物件的屬性,修改對應的值。
配置
最後配置自定義的外掛:
<plugins>
<plugin interceptor="com.qqdong.study.mybatis.TenantPlugin">
<property name="dbType" value="mysql"/>
</plugin>
<plugins>
複製程式碼
自定義外掛還是比較複雜的,如果不瞭解原理,很容易出錯,能不用外掛儘量不要使用,因為它是修改MyBatis的底層設計。 外掛生成的是層層代理物件的責任鏈模式,通過反射方法執行,效能不高,要考慮全面,特別是多個外掛層層代理的邏輯。
歡迎掃描下方二維碼,關注我的個人微信公眾號 ~