前文傳送門:
mybatis原始碼學習:從SqlSessionFactory到代理物件的生成
mybatis原始碼學習:一級快取和二級快取分析
mybatis原始碼學習:基於動態代理實現查詢全過程
一、自定義外掛流程
-
自定義外掛,實現Interceptor介面。
-
實現intercept、plugin和setProperties方法。
-
使用@Intercepts註解完成外掛簽名。
-
在主配置檔案註冊外掛。
/**
* 自定義外掛
* 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配置註冊外掛
<!--註冊外掛-->
<plugins>
<plugin interceptor="com.smday.interceptor.MyPlugin">
<property name="username" value="root"/>
<property name="password" value="123456"/>
</plugin>
</plugins>
二、測試外掛
三、原始碼分析
1、inteceptor在Configuration中的註冊
關於xml檔案的解析,當然還是需要從XMLConfigBuilder中查詢,我們很容易就可以發現關於外掛的解析:
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然後返回:
在返回之前,他進行了下面的操作:
executor = (Executor) interceptorChain.pluginAll(executor);
我們來看看這個方法具體幹了什麼:
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方法:
//看看wrap方法幹了點啥
public static Object wrap(Object target, Interceptor interceptor) {
//獲取獲取註解的資訊,攔截的物件,攔截的方法,攔截方法的引數。
Map<Class<?>, Set<Method>> 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)方法:其實就是獲取註解的資訊,攔截的物件,攔截的方法,攔截方法的引數。
private static Map<Class<?>, Set<Method>> 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<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
for (Signature sig : sigs) {
//先看map裡有沒有methods set
Set<Method> methods = signatureMap.get(sig.type());
if (methods == null) {
//沒有再建立一個
methods = new HashSet<Method>();
//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)方法:確定是否為攔截物件
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
Set<Class<?>> interfaces = new HashSet<Class<?>>();
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方法,不妨來看看:
ok,此時進入了我們定義的intercept方法,感覺無比親切。
//排程被代理物件的真實方法
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
}
如果有多個外掛,每經過一次wrap都會產生上衣個物件的代理物件,此處反射呼叫的方法也是上衣個代理物件的方法。接著,就還是執行目標的parameterize方法,但是當我們明白這些執行流程的時候,我們就可以知道如何進行一些小操作,來自定義方法的實現了。
四、外掛開發外掛pagehelper
外掛文件地址:https://github.com/pagehelper/Mybatis-PageHelper
這款外掛使分頁操作變得更加簡便,來一個簡單的測試如下:
1、引入相關依賴
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.2</version>
</dependency>
2、全域性配置
<!--註冊外掛-->
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin>
</plugins>
3、測試分頁
@Test
public void testPlugin(){
//查詢第一頁,每頁3條記錄
PageHelper.startPage(1,3);
List<User> all = userDao.findAll();
for (User user : all) {
System.out.println(user);
}
}
五、外掛總結
參考:《深入淺出MyBatis技術原理與實戰》
- 外掛生成地是層層代理物件的責任鏈模式,其中設計反射技術實現動態代理,難免會對效能產生一些影響。
- 外掛的定義需要明確需要攔截的物件、攔截的方法、攔截的方法引數。
- 外掛將會改變MyBatis的底層設計,使用時務必謹慎。