本篇文章是「深刻淺出MyBatis:技術原理與實踐」書籍的總結筆記。java
上一篇介紹了 MyBatis解析和運行原理 ,包括SqlSessionFactory的構建和SqlSession的執行過程,其中,SqlSession包含四大對象,能夠在四大對象調度的時候插入自定義的代碼,以知足特殊的需求,這即是MyBatis提供的插件技術。mysql
有些特殊場景,須要使用插件統一處理,好比:在進行多租戶開發時,數據要按租戶隔離,能夠在sql語句後面統一添加租戶編號篩選條件。sql
本篇就來介紹下插件,經過本篇的介紹,你會了解到:微信
在MyBatis中使用插件,須要實現Interceptor接口,定義以下:mybatis
public interface Interceptor { Object intercept(Invocation invocation) throws Throwable; Object plugin(Object target); void setProperties(Properties properties); }
詳細說說這3個方法:ide
插件的初始化時在MyBatis初始化的時候完成的,讀入插件節點和配置的參數,使用反射技術生成插件實例,而後調用插件方法中的setProperties方法設置參數,並將插件實例保存到配置對象中,具體過程看下面代碼。工具
plugin配置示例以下:性能
<plugins> <plugin interceptor="com.qqdong.study.mybatis.TenantPlugin"> <property name="dbType" value="mysql"/> </plugin> <plugins>
插件初始化過程:ui
public class XMLConfigBuilder extends BaseBuilder { ...... 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.addInterceptor(interceptorInstance); } } } ...... }
配置對象Configuration的添加插件方法:插件
public class Configuration { protected final InterceptorChain interceptorChain = new InterceptorChain(); public void addInterceptor(Interceptor interceptor) { interceptorChain.addInterceptor(interceptor); } }
InterceptorChain是一個類,主要包含一個List屬性,保存Interceptor對象:
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); } public List<Interceptor> getInterceptors() { return Collections.unmodifiableList(interceptors); } }
插件用的是責任鏈模式,責任鏈模式是一種對象行爲模式。在責任鏈模式裏,不少對象由每個對象對其下家的引用而鏈接起來造成一條鏈,請求在這個鏈上傳遞,直到鏈上的某一個對象決定處理此請求。
前面提到了InterceptorChain類,其中有個pluginAll方法,責任鏈就是在該方法定義的。
public Object pluginAll(Object target) { for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; }
上面介紹過plugin方法,它是生成代理對象的方法,從第一個對象(四大對象中的一個)開始,將對象傳遞給了plugin方法,返回一個代理;若是存在第二個插件,就拿着第一個代理對象,傳遞給plugin方法,返回第一個代理對象的代理.....
plugin方法是須要咱們去實現的,如何生成代理類呢,MyBatis提供了Plugin工具類,它實現了InvocationHandler接口(JDK動態代理的接口),看看它的2個方法:
public class Plugin implements InvocationHandler { 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); } } }
分析下這塊代碼,Plugin提供了靜態方法wrap方法,它會根據插件的簽名配置,使用JDK動態代理的方法,生成一個代理類,當四大對象執行方法時,會調用Plugin的invoke方法,若是方法包含在聲明的簽名裏,就會調用自定義插件的intercept方法,傳入Invocation對象。
另外,Invocation對象包含一個proceed方法,這個方法就是調用被代理對象的真實方法,若是有n個插件,第一個傳遞的參數是四大對象自己,而後調用一次wrap方法產生第一個代理對象,這裏的反射就是四大對象的真實方法,若是有第二個插件,這裏的反射就是第一個代理對象的invoke方法。
因此,在多個插件的狀況下,調度proceed方法,MyBatis老是從最後一個代理對象運行到第一個代理對象,最後是真實被攔截的對象方法被執行。
MetaObject是MyBatis給咱們提供的工具類,它能夠有效的獲取或修改一些重要對象的屬性。
舉例說明,咱們攔截StatementHandler對象,首先要獲取它要執行的SQL,添加返回行數限制。
編寫一個自定義插件,實現intercept方法,方法實現以下
StatementHandler statementHandler=(StatementHandler)invocation.getTarget(); MetaObject metaObj=SystemMetaObject.forObject(statementHandler); //獲取sql String sql=(String)metaStatementHandler.getValue("delegate.bound.sql"); //添加limit條件 sql="select * from (" + sql + ") limit 1000"; //從新設置sql metaStatementHandler.setValue("delegate.bound.sql",sql);
最後總結下插件的開發步驟。
好比想攔截StatementHandler對象的prepare方法,該方法有一個參數Connection對象,能夠這樣聲明:
@Intercepts({ @Signature(type =StatementHandler.class, method="prepare" , args={Connection.class})}) public class MyPlugin implements Interceptor{ ...... }
上面已經分析過原理,實現Interceptor接口的方法便可,經過Plugin工具類方便生成代理類,經過MetaObject工具類方便操做四大對象的屬性,修改對應的值。
最後配置自定義的插件:
<plugins> <plugin interceptor="com.qqdong.study.mybatis.TenantPlugin"> <property name="dbType" value="mysql"/> </plugin> <plugins>
自定義插件仍是比較複雜的,若是不瞭解原理,很容易出錯,能不用插件儘可能不要使用,由於它是修改MyBatis的底層設計。 插件生成的是層層代理對象的責任鏈模式,經過反射方法運行,性能不高,要考慮全面,特別是多個插件層層代理的邏輯。
下一篇會介紹MyBatis與Spring的集成。
歡迎掃描下方二維碼,關注個人我的微信公衆號 ~