mybatis原始碼學習:外掛定義+執行流程責任鏈
阿新 • • 發佈:2020-04-26
[toc]
前文傳送門:
[mybatis原始碼學習:從SqlSessionFactory到代理物件的生成](https://www.cnblogs.com/summerday152/p/12773121.html)
[mybatis原始碼學習:一級快取和二級快取分析](https://www.cnblogs.com/summerday152/p/12773135.html)
[mybatis原始碼學習:基於動態代理實現查詢全過程](https://www.cnblogs.com/summerday152/p/12773141.html)
# 一、自定義外掛流程
- 自定義外掛,實現Interceptor介面。
- 實現intercept、plugin和setProperties方法。
- 使用@Intercepts註解完成外掛簽名。
- 在主配置檔案註冊外掛。
```java
/**
* 自定義外掛
* Intercepts:完成外掛簽名,告訴mybatis當前外掛攔截哪個物件的哪個方法
*
* @author Summerday
*/
@Intercepts({
@Signature(type = StatementHandler.class, method = "parameterize", args = Statement.class)
})
public class MyPlugin implements Interceptor {
/**
* 攔截目標方法執行
*
* @param invocation
* @return
* @throws Throwable
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("MyPlugin.intercept getMethod: "+invocation.getMethod());
System.out.println("MyPlugin.intercept getTarget:"+invocation.getTarget());
System.out.println("MyPlugin.intercept getArgs:"+ Arrays.toString(invocation.getArgs()));
System.out.println("MyPlugin.intercept getClass:"+invocation.getClass());
//執行目標方法
Object proceed = invocation.proceed();
//返回執行後的返回值
return proceed;
}
/**
* 包裝目標物件,為目標物件建立一個代理物件
*
* @param target
* @return
*/
@Override
public Object plugin(Object target) {
System.out.println("MyPlugin.plugin :mybatis將要包裝的物件:"+target);
//藉助Plugin類的wrap方法使用當前攔截器包裝目標物件
Object wrap = Plugin.wrap(target, this);
//返回為當前target建立的動態代理
return wrap;
}
/**
* 將外掛註冊時的properties屬性設定進來
*
* @param properties
*/
@Override
public void setProperties(Properties properties) {
System.out.println("外掛配置的資訊:" + properties);
}
}
```
xml配置註冊外掛
```xml
```
# 二、測試外掛
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200425194123283.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1NreV9RaWFvQmFfU3Vt,size_16,color_FFFFFF,t_70)
# 三、原始碼分析
## 1、inteceptor在Configuration中的註冊
關於xml檔案的解析,當然還是需要從XMLConfigBuilder中查詢,我們很容易就可以發現關於外掛的解析:
```java
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
//獲取到全類名
String interceptor = child.getStringAttribute("interceptor");
//獲取properties屬性
Properties properties = child.getChildrenAsProperties();
//通過反射建立例項
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
//設定屬性
interceptorInstance.setProperties(properties);
//在Configuration中新增外掛
configuration.addInterceptor(interceptorInstance);
}
}
}
public void addInterceptor(Interceptor interceptor) {
//interceptorChain是一個儲存interceptor的Arraylist
interceptorChain.addInterceptor(interceptor);
}
```
此時初始化成功,我們在配置檔案中定義的外掛,已經成功加入interceptorChain。
## 2、基於責任鏈的設計模式
我們看到chain這個詞應該並不會陌生,我們之前學習過的過濾器也存在類似的玩意,什麼意思呢?我們以Executor為例,當建立Executor物件的時候,並不是直接new Executor然後返回:
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200425194348153.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1NreV9RaWFvQmFfU3Vt,size_16,color_FFFFFF,t_70)
在返回之前,他進行了下面的操作:
```java
executor = (Executor) interceptorChain.pluginAll(executor);
```
我們來看看這個方法具體幹了什麼:
```java
public Object pluginAll(Object target) {
//遍歷所有的攔截器
for (Interceptor interceptor : interceptors) {
//呼叫plugin,返回target包裝後的物件
target = interceptor.plugin(target);
}
return target;
}
```
很明顯,現在它要從chain中一一取出interceptor,並依次呼叫各自的plugin方法,暫且不談plugin的方法,我們就能感受到責任鏈的功能:讓一個物件能夠被鏈上的任何一個角色寵幸,真好。
## 3、基於動態代理的plugin
那接下來,我們就成功進入我們自定義plugin的plugin方法:
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200425194239427.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1NreV9RaWFvQmFfU3Vt,size_16,color_FFFFFF,t_70)
```java
//看看wrap方法幹了點啥
public static Object wrap(Object target, Interceptor interceptor) {
//獲取獲取註解的資訊,攔截的物件,攔截的方法,攔截方法的引數。
Map, Set> signatureMap = getSignatureMap(interceptor);
//獲取當前物件的Class
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;
}
```
getSignatureMap(interceptor)方法:其實就是獲取註解的資訊,攔截的物件,攔截的方法,攔截方法的引數。
```java
private static Map, Set> getSignatureMap(Interceptor interceptor) {
//定位到interceptor上的@Intercepts註解
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
//如果註解不存在,則報錯
if (interceptsAnnotation == null) {
throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
}
//獲取@Signature組成的陣列
Signature[] sigs = interceptsAnnotation.value();
Map, Set> signatureMap = new HashMap, Set>();
for (Signature sig : sigs) {
//先看map裡有沒有methods set
Set methods = signatureMap.get(sig.type());
if (methods == null) {
//沒有再建立一個
methods = new HashSet();
//class:methods設定進去
signatureMap.put(sig.type(), methods);
}
try {
//獲取攔截的方法
Method method = sig.type().getMethod(sig.method(), sig.args());
//加入到set中
methods.add(method);
} catch (NoSuchMethodException e) {
throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
}
}
return signatureMap;
}
```
getAllInterfaces(type, signatureMap)方法:確定是否為攔截物件
```java
private static Class>[] getAllInterfaces(Class> type, Map, Set> signatureMap) {
Set> interfaces = new HashSet>();
while (type != null) {
//介面型別
for (Class> c : type.getInterfaces()) {
//如果確實是攔截的物件,則加入interfaces set
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
//從父介面中檢視
type = type.getSuperclass();
}
//最後set裡面存在的元素就是要攔截的物件
return interfaces.toArray(new Class>[interfaces.size()]);
}
```
我們就可以猜測,外掛只會對我們要求的物件和方法進行攔截。
## 4、攔截方法的intercept(invocation)
確實,我們一路debug,遇到了Executor、ParameterHandler、ResultHandler都沒有進行攔截,然而,當StatementHandler物件出現的時候,就出現了微妙的變化,當我們呼叫代理的方法必然會執行其invoke方法,不妨來看看:
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200425194152135.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1NreV9RaWFvQmFfU3Vt,size_16,color_FFFFFF,t_70)
ok,此時進入了我們定義的intercept方法,感覺無比親切。
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200425194158783.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1NreV9RaWFvQmFfU3Vt,size_16,color_FFFFFF,t_70)
```java
//排程被代理物件的真實方法
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
}
```
如果有多個外掛,每經過一次wrap都會產生上衣個物件的代理物件,此處反射呼叫的方法也是上衣個代理物件的方法。接著,就還是執行目標的parameterize方法,但是當我們明白這些執行流程的時候,我們就可以知道如何進行一些小操作,來自定義方法的實現了。
# 四、外掛開發外掛pagehelper
外掛文件地址:[https://github.com/pagehelper/Mybatis-PageHelper](https://github.com/pagehelper/Mybatis-PageHelper)
這款外掛使分頁操作變得更加簡便,來一個簡單的測試如下:
## 1、引入相關依賴
```xml
com.github.pagehelper
pagehelper
5.1.2
```
## 2、全域性配置
```xml
```
## 3、測試分頁
```java
@Test
public void testPlugin(){
//查詢第一頁,每頁3條記錄
PageHelper.startPage(1,3);
List all = userDao.findAll();
for (User user : all) {
System.out.println(user);
}
}
```
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200425194130480.png)
# 五、外掛總結
參考:《深入淺出MyBatis技術原理與實戰》
- 外掛生成地是層層代理物件的責任鏈模式,其中設計反射技術實現動態代理,難免會對效能產生一些影響。
- 外掛的定義需要明確需要攔截的物件、攔截的方法、攔截的方法引數。
- 外掛將會改變MyBatis的底層設計,使用時務必謹慎。