插件允許對Mybatis的四大對象(Executor、ParameterHandler、ResultSetHandler、StatementHandler)進行攔截 問題 Mybatis插件的註冊順序與調用順序的關係? 使用 在講源碼之前,先看看如何自定義插件。 mybatis-demo,官方文檔 ...
插件允許對Mybatis的四大對象(Executor、ParameterHandler、ResultSetHandler、StatementHandler)進行攔截
問題
Mybatis插件的註冊順序與調用順序的關係?
使用
在講源碼之前,先看看如何自定義插件。
-
創建插件類
自定義插件類需要實現Interceptor
// 註解配置需要攔截的類以及方法 @Intercepts({ @Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}) }) // 實現Interceptor介面 public class SqlLogPlugin implements Interceptor { /** * 具體的攔截邏輯 */ @Override public Object intercept(Invocation invocation) throws Throwable { long begin = System.currentTimeMillis(); try { return invocation.proceed(); } finally { long time = System.currentTimeMillis() - begin; System.out.println("sql 運行了 :" + time + " ms"); } } /** * 判斷是否需要進行代理 * 此方法有預設實現,一般無需重寫 */ /*@Override public Object plugin(Object target) { return Plugin.wrap(target, this); }*/ /** * 自定義參數 */ @Override public void setProperties(Properties properties) { // 這是xml中配置的參數 properties.forEach((k, v) -> { System.out.printf("SqlLogPlugin---key:%s, value:%s%n", k, v); }); } }
-
註冊
在配置文件註冊插件
<plugins> <plugin interceptor="com.wjw.project.intercaptor.SqlLogPlugin"> <property name="key1" value="root"/> <property name="key2" value="123456"/> </plugin> </plugins>
-
效果
控制輸出
SqlLogPlugin---key:key1, value:root SqlLogPlugin---key:key2, value:123456 sql 運行了 :17 ms
源碼
原理:Mybatis四大對象創建時,都回去判斷是否滿足插件的攔截條件,滿足,則四大對象就會被Plugin
類代理
源碼分3部分講。註冊、包裝、調用
-
註冊
xml方式的註冊,是在XMLConfigBuilder#pluginElement完成的。
不明覺厲的同學,請參考上一篇文章:Mybatis源碼解讀-配置載入和Mapper的生成
// XMLConfigBuilder#pluginElement(XNode parent) 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).getDeclaredConstructor().newInstance(); interceptorInstance.setProperties(properties); // 將插件添加到配置的插件鏈中,等待後續使用 configuration.addInterceptor(interceptorInstance); } } }
configuration.addInterceptor做得操作很簡單
-
包裝
上面講了插件的註冊,最後調用的是configuration.addInterceptor,最終調用的是InterceptorChain#addInterceptor
public class InterceptorChain { private final List<Interceptor> interceptors = new ArrayList<>(); /* * 每當四大對象創建時,都會執行此方法 * 滿足攔截條件,則返回Plugin代理,否則返回原對象 * @param target Mybatis四大對象之一 */ public Object pluginAll(Object target) { for (Interceptor interceptor : interceptors) { // 調用每個插件的plugin方法,判斷是否需要代理 target = interceptor.plugin(target); } return target; } // 將攔截器添加interceptors集合中存起來 public void addInterceptor(Interceptor interceptor) { interceptors.add(interceptor); } public List<Interceptor> getInterceptors() { return Collections.unmodifiableList(interceptors); } }
我們案例是攔截StatementHandler,所以也以此為例
/* * 這是創建StatementHandler的方法 * Configuration#newStatementHandler */ 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之後,會調用InterceptorChain的pluginAll方法 statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); return statementHandler; }
那麼我們再仔細分析下
pluginAll
方法,pluginAll
調用的是每個插件的plugin
方法default Object plugin(Object target) { return Plugin.wrap(target, this); }
可以看到,最終調用的是
Plugin.*wrap*
/* * Plugin#wrap * 判斷是否滿足插件的攔截條件,是則返回代理類,否則返回原對象 */ public static Object wrap(Object target, Interceptor interceptor) { // 獲取插件的攔截信息(就是獲取@Intercepts註解的內容) Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); Class<?> type = target.getClass(); // 判斷是否滿足攔截條件 Class<?>[] interfaces = getAllInterfaces(type, signatureMap); if (interfaces.length > 0) { // 滿足攔截條件則返回Plugin代理對象 return Proxy.newProxyInstance( type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); } // 不滿足則返回原對象 return target; }
-
調用
在上一個
包裝
步驟提到,滿足條件會返回代理對象,即調用StatementHandler
的所有方法,都會經過Plugin
的invoke
方法,去看看// Plugin#invoke 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); } }