Mybatis提供了強大的擴展功能,也就是Mybatis的插件(plugins)功能;MyBatis容許你在已映射語句執行過程當中的某一點進行攔截調用,攔截以後能夠對已有方法添加一些定製化的功能,好比常見的分頁功能;試圖修改或重寫已有方法的行爲的時候,你極可能在破壞MyBatis 的核心模塊,這些都是更低層的類和方法,因此使用插件的時候要特別小心。mysql
攔截的點一共包含四個對象,若干方法,具體以下:git
有了攔截點以後,咱們須要告訴Mybatis在哪一個類下執行到哪一個方法的時候進行擴展;Mybatis提供了簡單的配置便可實現;github
使用插件是很是簡單的,只需實現Interceptor接口,並指定想要攔截的方法簽名便可;以下面自定義的插件MyPlugin:sql
@Intercepts({ @Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class }), @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class }) }) public class MyPlugin implements Interceptor { private Properties prop; @Override public Object intercept(Invocation invocation) throws Throwable { System.err.println("====before===="+invocation.getTarget()); Object obj = invocation.proceed(); System.err.println("====after===="+invocation.getTarget()); return obj; } @Override public Object plugin(Object target) { System.err.println("====調用生成代理對象====target:" + target); return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { this.prop = properties; } }
經過註解的方式指定了方法的簽名:類型、方法名、參數列表;若是要攔截多個方法能夠配置多個@Signature經過逗號分隔;如上配置表示在執行到Executor的update和query方法時進行攔截,也就會執行Interceptor接口中定義的intercept方法,咱們能夠簡單的作一些修改,或者乾脆在裏面重寫對應的方法,固然前提是對源碼很熟悉的狀況下;固然還須要Mybatis知道咱們自定義的MyPlugin,須要提供configuration配置:apache
<plugins> <plugin interceptor="com.mybatis.plugin.MyPlugin"> <property name="dbType" value="mysql" /> </plugin> </plugins>
作完以上配置以後,能夠簡單的作一個測試,執行Mybatis的一個查詢功能,日誌輸出以下:mybatis
====調用生成代理對象====target:org.apache.ibatis.executor.CachingExecutor@31d7b7bf ====before====org.apache.ibatis.executor.CachingExecutor@31d7b7bf ====調用生成代理對象====target:org.apache.ibatis.scripting.defaults.DefaultParameterHandler@f5ac9e4 ====調用生成代理對象====target:org.apache.ibatis.executor.resultset.DefaultResultSetHandler@7334aada ====調用生成代理對象====target:org.apache.ibatis.executor.statement.RoutingStatementHandler@4d9e68d0 ====after====org.apache.ibatis.executor.CachingExecutor@31d7b7bf
生成的四個代理對象其實就是咱們上面介紹的四個攔截點,雖然都生成了代理對象,可是在代理對象執行的時候也會檢查是否配置了指定攔截方法,因此執行Mybatis查詢功能的時候,檢查是否配置了query方法。app
咱們在configuration中經過標籤plugins配置的插件,最後都會註冊到一個攔截鏈InterceptorChain中,其中維護了攔截器列表:ide
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); } }
Mybatis在解析plugins標籤的時候,解析到每一個plugin的時候都會調用addInterceptor()方法將插件添加到interceptors,多個插件會在pluginAll()方法執行的時候依次被調用;工具
過濾器鏈中提供了pluginAll()方法,此方法會在咱們上面介紹的四個類被實例化的時候調用,能夠在開發工具中簡單的使用快捷鍵查詢此方法被調用的地方:
能夠看一個最多見的Executor實例化:開發工具
public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; executorType = executorType == null ? ExecutorType.SIMPLE : executorType; 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); } executor = (Executor) interceptorChain.pluginAll(executor); return executor; }
以上首先根據不一樣的executorType類型建立不一樣的Executor,默認的如CachingExecutor,最後會調用interceptorChain的pluginAll方法,傳入參數和返回參數都是Executor,能夠簡單理解就是經過pluginAll返回了Executor的代理類;
生成代理類的方式有:JDK自帶的Proxy和CGlib方式,Mybatis提供了Plugin類提供了生成相關代理類的功能,因此在上面的實例MyPlugin的plugin方法中直接使用了Plugin.wrap(target, this),兩個參數分別是:target對應就是上面的四種類型,this表明當前的自定義插件:
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; }
此方法首先獲取自定義插件中配置的Signature,而後檢查Signature中配置的類是不是當前target類型,若是匹配則經過JDK自帶的Proxy建立代理類,不然直接返回target,不作任何代理處理;如上實例這裏的Target是CachingExecutor,其對應的接口是Executor,而咱們在MyPlugin中的Signature中配置了Executor類,因此能夠匹配成功;
等到真正執行具體方法的時候,實際上是執行建立代理類時指定的InvocationHandler的invoke方法,能夠發如今上節中指定的InvocationHandler是Plugin對象,Plugin自己也是繼承於InvocationHandler,提供了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)) { return interceptor.intercept(new Invocation(target, method, args)); } return method.invoke(target, args); } catch (Exception e) { throw ExceptionUtil.unwrapThrowable(e); } }
首先從signatureMap中獲取攔截類對應的方法列表,而後檢查當前執行的方法是否在要攔截的方法列表中,若是在則調用自定義的插件interceptor,不然執行默認的invoke操做;interceptor調用intercept方法的時候是傳入的Invocation對象,其包含了三個參數分別是:target對應就是上面的四種類型,method當前執行的方法,args當前執行方法的參數;這裏的方法名和參數是能夠在源碼裏面查看的,好比Executor的query方法:
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
調用自定義攔截器的intercept方法,傳入一個Invocation對象,若是隻是記錄一下日誌,如上面的MyPlugin插件,這時候只須要執行proceed方法:
public class Invocation { private final Object target; private final Method method; private final Object[] args; public Object proceed() throws InvocationTargetException, IllegalAccessException { return method.invoke(target, args); } }
此方法其實就是默認的處理,和不使用代理類的狀況產生的結果實際上是同樣的,只不過代理類使用反射的方式調用;固然Mybatis的插件不止記錄日誌這麼簡單,好比咱們經常使用的插件PageHelper用來作物理分頁等等;
Mybatis提供了四個類,能夠被用來攔截,用來進行功能擴展;本文介紹瞭如何使用插件進行功能擴展,而後從源碼層面進行分析如何經過代理類來實現擴展。