MyBatis框架原理4:外掛
外掛的定義和作用
首先引用MyBatis文件對外掛(plugins)的定義:
MyBatis 允許你在已對映語句執行過程中的某一點進行攔截呼叫。預設情況下,MyBatis 允許使用外掛來攔截的方法呼叫包括:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
這些類中方法的細節可以通過檢視每個方法的簽名來發現,或者直接檢視 MyBatis 發行包中的原始碼。 如果你想做的不僅僅是監控方法的呼叫,那麼你最好相當瞭解要重寫的方法的行為。 因為如果在試圖修改或重寫已有方法的行為的時候,你很可能在破壞 MyBatis 的核心模組。 這些都是更低層的類和方法,所以使用外掛的時候要特別當心。
Mybatis外掛所攔截的4個物件正是在之前的文章MyBatis框架原理2:SqlSession執行過程中介紹的4個實現核心功能的介面。那麼外掛攔截這4個介面能做什麼呢?根據之前文章對4個介面的介紹,可以猜測到:
- Executor是SqlSession整個執行過程的總指揮,同時還對快取進行操作,通過外掛可以使用自定義的快取,比如mybatis-enhanced-cache外掛。
- StatementHandler負責SQL的編譯和執行,通過外掛可以改寫SQL語句。
- ParameterHandler負責SQL的引數設定,通過外掛可以改變引數設定。
- ResultSetHandler負責結果集對映和儲存過程輸出引數的組裝,通過外掛可以對結果集對映規則改寫。
外掛的原理
在理解外掛原理之前,得先搞清楚以下三個概念:
動態代理 代理模式是一種給真實物件提供一個代理物件,並由代理物件控制對真實物件的引用的一種設計模式,動態代理是在程式執行時動態生成代理類的模式,JDK動態代理物件是由java提供的一個Proxy類和InvocationHandler介面以及一個真實物件的介面生成的。通常InvocationHandler的實現類持有一個真實物件欄位和定義一個invoke方法,通過Proxy類的newProxyInstance方法就可以生成這個真實物件的代理物件,通過代理物件排程方法實際就是呼叫InvocationHandler實現類的invoke方法,在invoke方法中可以通過反射實現呼叫真實物件的方法。
攔截器(Interceptor) 動態代理物件可以對真實物件方法引用,是因為InvocationHandler實現類持有了一個真實物件的欄位,通過反射就可以實現這個功能。如果InvocationHandler實現類再持有一個Interceptor介面的實現類,Interceptor介面定義了一個入參為真實物件的intercept方法,Interceptor介面的實現類通過重寫intercept方法可以對真實物件的方法引用或者實現增強功能等等,也就是當我們再次使用這個動態代理物件排程方法時,可以根據需求對真實物件的方法做出改變。
從這個Interceptor介面實現類的功能上來看,可以叫做真實物件方法的攔截器。於是我們再想一下,如果前面講到MyBatis的4個核心功能介面的實現類(比如PreparedStatementHandler)是一個真實物件,我們通過JDK動態代理技術生成一個代理物件,並且生成代理類所需的InvocationHandler實現類同時還持有了一個Interceptor介面實現類,通過使用代理物件排程方法,我們就可以根據需求對PreparedStatementHandler的功能進行增強。
實際上MyBatis確實提供了這樣一個Interceptor介面和intercept方法,也提供了這樣的一個InvocationHandler介面的實現類,類名叫Plugin,它們都位於MyBatis的org.apache.ibatis.plugin包下。等等,那麼MyBatis的外掛不就是攔截器嗎?攔截器的原理都講完了,等下還怎麼講什麼外掛原理?
責任鏈模式 我們通過JDK動態代理技術生成一個代理物件,代理的真實物件是個StatementHandler,並且持有StatementHandler的攔截器(外掛)。如果我們把這個代理物件視為一個target物件,再利用動態代理生成一個代理類,並且持有對這個target物件的攔截器(外掛),如果再把新生成的代理視為一個新的target類,同樣持有對新target類的攔截器(外掛),那麼我們就得到了一個像是被包裹了三層攔截器(外掛)的StatementHandler的代理物件:
當MyBatis每一次SqlSession會話需要引用到StatementHandler的方法時,如過符合上圖中攔截器3的攔截邏輯,則按攔截器3的定義的方法執行;如果不符合攔截邏輯,則將執行責任交給攔截器2處理,以此類推,這樣的模式叫做責任鏈模式。MyBatis全域性配置檔案裡可以配置多個外掛,多個外掛的執行就是按照這樣的責任鏈模式執行的。
通過對以上三點的理解,我們已經對MyBatis外掛原理已經有了初步認識,下面就通過原始碼看看MyBatis外掛是如何執行起來的。
外掛的執行過程
外掛的介面 MyBatis提供了一個Interceptor介面,外掛必須實現這個介面,介面定義了3個方法如下:
public interface Interceptor { // 執行外掛實現的方法,Invocation物件持有真實物件,可通過反射呼叫真實物件的方法 Object intercept(Invocation invocation) throws Throwable; // 設定外掛攔截的物件target,通常呼叫Pulgin類的wrap方法生成一個代理類 Object plugin(Object target); // 根據配置檔案初始化外掛 void setProperties(Properties properties); }
外掛的初始化 在MyBatis初始化時XMLConfigBuilderder的pluginElement方法對外掛配置檔案解析:
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物件中 configuration.addInterceptor(interceptorInstance); } } }
Configuration物件最終將解析出的外掛配置儲存在持有InterceptorChain物件中,InterceptorChain物件又是通過一個ArrayList來儲存所有外掛,可見在MyBatis初始化的時候外掛配置就已經載入好了,執行時就會根據外掛編寫的規則執行攔截邏輯。
public class InterceptorChain { // 通過集合來儲存外掛 private final List<Interceptor> interceptors = new ArrayList<Interceptor>(); // 通過責任鏈模式呼叫外掛plugin方法生成代理物件 public Object pluginAll(Object target) { for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; } // Configuration物件呼叫的新增外掛的方法 public void addInterceptor(Interceptor interceptor) { interceptors.add(interceptor); } public List<Interceptor> getInterceptors() { return Collections.unmodifiableList(interceptors); } }
外掛的執行 如果我們需要攔截MyBatis的Executor介面,Configuration在初始化Executor時就會通過責任鏈模式將初始化的Executor作為真實物件,呼叫InterceptorChain的pluginAll放法生成代理物件:
public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor 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); } // 呼叫InterceptorChain的pluginAll放法生成代理物件 executor = (Executor) interceptorChain.pluginAll(executor); return executor; }
InterceptorChain的 pluginAll方法呼叫外掛的plugin方法,plugin方法可以呼叫MyBatis提供的工具類Plugin類來生成代理物件,Plugin類實現了InvocationHandler,在Plugin類中定義invoke方法來實現攔截邏輯和執行外掛方法:
public class Plugin implements InvocationHandler { // target為需要攔截的真實物件 private final Object target; // interceptor為外掛 private final Interceptor interceptor; private final 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) { 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); } } ...
外掛的intercept方法引數為Invocation物件,Invocation物件持有真實物件和一個proceed方法,proceed方法通過反射呼叫真實物件的方法。於是多個外掛生成的責任鏈模式的代理物件,就可以通過一層一層執行proceed方法來呼叫真實物件的方法。
外掛的開發
自己編寫外掛必須繼承MyBatis的Interceptor介面
public interface Interceptor { // 執行外掛實現的方法,Invocation物件持有真實物件,可通過反射呼叫真實物件的方法 Object intercept(Invocation invocation) throws Throwable; // 設定外掛攔截的物件target,通常呼叫Pulgin類的wrap方法生成一個代理類 Object plugin(Object target); // 根據配置檔案初始化外掛 void setProperties(Properties properties); }
使用@Intercepts和@Signature註解
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class ,Integet.class})}) public class MyPlugin implements Interceptor {...}
用@Intercepts註解申明是一個外掛,@Signature註解申明攔截的物件,方法和引數。上面的寫法表明了攔截了StatementHandler物件的prepare方法,引數是一個Connection物件和一個Integet。
編寫攔截方法 MyBatis提供了一個Invocation工具類,通常我們將需要攔截的真實物件,方法及引數封裝在裡面作為一個引數傳給外掛的intercept方法,在外掛intercept方法裡可以編寫攔截邏輯和執行攔截方法,方法引數invocation可以通過反射呼叫被代理物件的方法:
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class ,Integet.class})}) public class MyPlugin implements Interceptor { @override public Object intercept(Invocation invocation) throws Throwable { // do something ... // 呼叫被代理物件的方法 invocation.proceed(); // do something ... @override 呼叫Plugin工具類生成代理物件 public Object plugin(Object target){ return Plugin.wrap(target, this); } ... }
生成代理物件 MyBatis還提供了一個Plugin工具類,其中wrap方法用於生成代理類,invoke方法驗證攔截型別和方法,並選擇是否按攔截器的方法,程式碼如下:
public class Plugin implements InvocationHandler { private final Object target; // 真實物件 private final Interceptor interceptor; // 攔截器(外掛) private final Map<Class<?>, Set<Method>> signatureMap; // Map儲存簽名的型別,方法和引數資訊 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) { // getSignatureMap方法通過反射獲取外掛裡@Intercepts和@Signature註解宣告的攔截型別,方法和引數資訊 Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); Class<?> type = target.getClass(); 從signatureMap中獲取攔截物件的型別 Class<?>[] interfaces = getAllInterfaces(type, signatureMap); // 生成代理物件,如果target的型別不是外掛裡註解宣告的型別則直接返回target不作攔截。 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)) { // 如果是宣告攔截的方法,則呼叫外掛的intercept方法執行攔截處理 return interceptor.intercept(new Invocation(target, method, args)); } // 如果不是宣告攔截的方法,則直接呼叫真實物件的方法 return method.invoke(target, args); } catch (Exception e) { throw ExceptionUtil.unwrapThrowable(e); } } ...
總結
MyBatis外掛執行依靠Java動態代理技術實現,雖然原理很簡單,但是編寫外掛涉及到修改MyBatis框架底層的介面,需要十分謹慎,做為初學者,最好使用現成的外掛。