天天看點

深入淺出Mybatis-插件原理

Mybatis采用責任鍊模式,通過動态代理組織多個攔截器(插件),通過這些攔截器可以改變Mybatis的預設行為(諸如SQL重寫之類的),由于插件會深入到Mybatis的核心,是以在編寫自己的插件前最好了解下它的原理,以便寫出安全高效的插件。

代理鍊的生成

Mybatis支援對Executor、StatementHandler、PameterHandler和ResultSetHandler進行攔截,也就是說會對這4種對象進行代理。下面以Executor為例。Mybatis在建立Executor對象時會執行下面一行代碼:

[java] 

  1. executor =(Executor) interceptorChain.pluginAll(executor);

InterceptorChain裡儲存了所有的攔截器,它在mybatis初始化的時候建立。上面這句代碼的含義是調用攔截器鍊裡的每個攔截器依次對executor進行plugin(插入?)代碼如下:

[java] 

  1. /**
  2. * 每一個攔截器對目标類都進行一次代理
  3. * @paramtarget
  4. * @return 層層代理後的對象
  5. */
  6. public ObjectpluginAll(Object target) {
  7. for(Interceptor interceptor : interceptors) {
  8. target= interceptor.plugin(target);
  9. }
  10. returntarget;
  11. }

下面以一個簡單的例子來看看這個plugin方法裡到底發生了什麼。

[java] 

  1. @Intercepts({@Signature(type = Executor.class, method ="update", args = {MappedStatement.class, Object.class})})
  2. public class ExamplePlugin implements Interceptor {
  3. @Override
  4. public Objectintercept(Invocation invocation) throws Throwable {
  5. returninvocation.proceed();
  6. }
  7. @Override
  8. public Objectplugin(Object target) {
  9. returnPlugin.wrap(target, this);
  10. }
  11. @Override
  12. public voidsetProperties(Properties properties) {
  13. }
  14. }

每一個攔截器都必須實作上面的三個方法,其中:

1)       Object intercept(Invocation invocation)是實作攔截邏輯的地方,内部要通過invocation.proceed()顯式地推進責任鍊前進,也就是調用下一個攔截器攔截目标方法。

2)       Object plugin(Object target) 就是用目前這個攔截器生成對目标target的代理,實際是通過Plugin.wrap(target,this) 來完成的,把目标target和攔截器this傳給了包裝函數。

3)       setProperties(Properties properties)用于設定額外的參數,參數配置在攔截器的Properties節點裡。

注解裡描述的是指定攔截方法的簽名  [type,method,args] (即對哪種對象的哪種方法進行攔截),它在攔截前用于決斷。

Plugin.wrap方法

從前面可以看出,每個攔截器的plugin方法是通過調用Plugin.wrap方法來實作的。代碼如下:

[java] 

  1. public staticObject wrap(Object target, Interceptor interceptor) {
  2. //從攔截器的注解中擷取攔截的類名和方法資訊
  3. Map<Class<?>, Set<Method>> signatureMap =getSignatureMap(interceptor);
  4. Class<?> type = target.getClass();
  5. //解析被攔截對象的所有接口(注意是接口)
  6. Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
  7. if(interfaces.length > 0) {
  8. //生成代理對象, Plugin對象為該代理對象的InvocationHandler  (InvocationHandler屬于java代理的一個重要概念,不熟悉的請參考相關概念)
  9. returnProxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target,interceptor,signatureMap));
  10. }
  11. returntarget;
  12. }

 這個Plugin類有三個屬性:

   private Object target;//被代理的目标類

   private Interceptor interceptor;//對應的攔截器

   private Map<Class<?>, Set<Method>> signatureMap;//攔截器攔截的方法緩存

我們再次結合(Executor)interceptorChain.pluginAll(executor)這個語句來看,這個語句内部對

executor執行了多次plugin,第一次plugin後通過Plugin.wrap方法生成了第一個代理類,姑且就叫executorProxy1,這個代理類的target屬性是該executor對象。第二次plugin後通過Plugin.wrap方法生成了第二個代理類,姑且叫executorProxy2,這個代理類的target屬性是executorProxy1...這樣通過每個代理類的target屬性就構成了一個代理鍊(從最後一個executorProxyN往前查找,通過target屬性可以找到最原始的executor類)。

代理鍊上的攔截

代理鍊生成後,對原始目标的方法調用都轉移到代理者的invoke方法上來了。Plugin作為InvocationHandler的實作類,他的invoke方法是怎麼樣的呢?

[java] 

​​​​

  1. public Objectinvoke(Object proxy, Method method, Object[] args) throws Throwable {
  2. try {
  3. Set<Method> methods = signatureMap.get(method.getDeclaringClass());
  4. if(methods != null && methods.contains(method)) {
  5. //調用代理類所屬攔截器的intercept方法,
  6. return interceptor.intercept(new Invocation(target, method, args));
  7. }
  8. returnmethod.invoke(target, args);
  9. } catch(Exception e) {
  10. throwExceptionUtil.unwrapThrowable(e);
  11. }

在invoke裡,如果方法簽名和攔截中的簽名一緻,就調用攔截器的攔截方法。我們看到傳遞給攔截器的是一個Invocation對象,這個對象是什麼樣子的,他的功能又是什麼呢?

[java] 

  1. public class Invocation {
  2. private Object target;
  3. private Methodmethod;
  4. private Object[]args;
  5. publicInvocation(Object target, Method method, Object[] args) {
  6. this.target =target;
  7. this.method =method;
  8. this.args =args;
  9. }
  10. ...
  11. public Objectproceed() throws InvocationTargetException, IllegalAccessException {
  12. returnmethod.invoke(target, args);
  13. }
  14. }

可以看到,Invocation類儲存了代理對象的目标類,執行的目标類方法以及傳遞給它的參數。

在每個攔截器的intercept方法内,最後一個語句一定是returninvocation.proceed()(不這麼做的話攔截器鍊就斷了,你的mybatis基本上就不能正常工作了)。invocation.proceed()隻是簡單的調用了下target的對應方法,如果target還是個代理,就又回到了上面的Plugin.invoke方法了。這樣就形成了攔截器的調用鍊推進。

[java] 

  1. public Object intercept(Invocation invocation) throws Throwable {
  2. //完成代理類本身的邏輯
  3. ...
  4. //通過invocation.proceed()方法完成調用鍊的推進
  5. return invocation.proceed();
  6. }

總結

我們假設在MyBatis配置了一個插件,在運作時會發生什麼?

1)       所有可能被攔截的處理類都會生成一個代理

2)       處理類代理在執行對應方法時,判斷要不要執行插件中的攔截方法

3)       執行插接中的攔截方法後,推進目标的執行

如果有N個插件,就有N個代理,每個代理都要執行上面的邏輯。這裡面的層層代理要多次生成動态代理,是比較影響性能的。雖然能指定插件攔截的位置,但這個是在執行方法時動态判斷,初始化的時候就是簡單的把插件包裝到了所有可以攔截的地方。

是以,在編寫插件時需注意以下幾個原則: