關於IoC與AOP的一些理解

  • 2020 年 2 月 10 日
  • 筆記

最近在複習一些舊的知識,隨著工作經驗的增加,看待問題的眼光也在發生變化,重新談談對IoC與AOP新的理解.


IoC(Inversion of Control)

IoC叫做控制反轉,IoC提出的目的是解決項目中複雜的依賴關係,使用非硬編碼的方式來擴展這些關係,簡單來說就是為了解耦,在你需要的地方IoC容器會自動幫你注入對應的服務實例.

控制反轉,反轉的是什麼?

在沒有IOC的時代,A依賴B介面,但是介面又不能實例化,因此A需要知道B的子類,然後實例化B的子類,這種依賴實際上是依賴具體實現,而不是依賴介面,不符合面向對象設計原則依賴倒置原則。

那麼IOC的出現就是為了反轉這個依賴,也就是控制反轉的意義。有了IOC,A只需要依賴B的介面,運行時需要B的實現子類會自動注入進來,這是IOC的魅力所在。

DI(依賴注入)

IoC控制反轉是一種思想,這種思想在實施過程中會有很多問題,比如上述例子IB這個介面實現類很多,該怎麼管理?A依賴IB,但是介面不能實例化,該怎麼把具體實現類注入到A中?等等問題,那麼一種實現方式就是DI(依賴注入),其中Spring就是選擇了依賴注入實現IoC設計。

處理循環依賴

對於IoC來說一直存在循環依賴的難題,當A依賴B,B依賴C,C依賴A,彼此的依賴關係構成的是一個環形時,就是循環依賴,解決這種環形的依賴才是IoC的最關鍵的本質.(系統中出現循環依賴的話一不小心就掉進了死遞歸,因此儘可能避免循環依賴設計)

構造注入 構造注入時利用構造函數在實例化類的時候注入需要參數的一種方式.對於構造注入的循環依賴如下所示:

class A {    private B b;    // A的創建依賴B    public A(B b) {      this.b = b;    }  }    class B {    private A a;    // B的創建依賴A    public B(A a) {      this.a = a;    }  }

那麼結果自然是死鎖,A需要B才能實例化,B需要A才能實例化,系統中有沒有兩個類的實例,互相僵持就是死鎖,無法解決循環依賴問題.

屬性注入 屬性注入是在類實例化之後,通過set方法或者反射直接注入到對應的成員變數上的依賴注入方式,如下所示:

class A {    private B b;      public A() {    }    // 實例化之後set方法注入B    public A setB(B b) {      this.b = b;      return this;    }  }    class B {    private A a;      public B() {    }    // 實例化之後set方法注入A    public B setA(A a) {      this.a = a;      return this;    }  }

與構造函數最大的不同點是去除了類的實例化對外部的強依賴關係,轉而用程式碼邏輯保證這個強依賴的邏輯,比如屬性注入失敗直接拋異常讓系統停止.那麼此時的循環依賴解決辦法就很簡單了.

  • 做法1: 系統初始化時不考慮依賴關係把所有的Bean都實例化出來,然後依次執行屬性注入,因為每個Bean都有實例,所以循環依賴不存在死鎖.
  • 做法2: 按需實例化,實例化A,然後執行A的屬性注入,發現依賴B,接著去實例化B,執行B的屬性注入,此時A已經存在,那麼B可以注入A,回到A的屬性注入,拿到了B的實例,注入B.到此循環依賴解決.

回歸本質,概括一下就是轉變強依賴到弱依賴,把實例化與屬性注入兩個步驟分開來解決循環依賴的死鎖.IoC的核心思想在於資源統一管理,你所持有的資源全部放入到IoC容器中,而你也只需要依賴IoC容器,該容器會自動為你裝配所需要的具體依賴.

循環依賴的深入思考

循環依賴實際上場景有很多,在JDK當中就有類似的場景,比如Object類是所有類的父類,但是Java中每一個類都有一個對應的Class實例,那麼問題就出來了Object類與Object對應的Class類就是一種雞生蛋,蛋生雞的問題。 那麼這種問題解決的本質就是把強依賴關係轉換成弱依賴關係,比如可以先把Object與Class對應的記憶體區域先創建出來,拿到地址引用後相互賦值,最後再一口氣把兩個都創建出來,和Spring IoC的處理是一模一樣的。

用一個很形象的例子比喻:

番茄炒蛋,並不是先把番茄炒熟再炒雞蛋,也不是把雞蛋炒熟再炒番茄,而是先把番茄或者雞蛋炒半熟,然後再混合炒,最紅炒出來番茄炒雞蛋,那麼這個過程就是一種循環依賴的解決思路。

這一段參考知乎 https://www.zhihu.com/question/30301819

AOP

AOP到底該怎麼理解?

AOP是一種設計思想,這種設計思想的目的是不侵入你原有程式碼的基礎上做一定的功能增強實現,這種思想在設計模式中有裝飾者模式,代理模式等等。那麼Spring AOP就是基於動態代理這一設計模式實現了AOP設計,接下來聊一聊動態代理的本質,動態代理得益於Java的類載入機制,記憶體中生成位元組碼,然後使用類載入器進行載入,之後實例化出來就是可以用的實例.

從軟體重用的角度來看,OOP設計只能在對象繼承樹的縱向上擴展重用,AOP則使的可以在橫向上擴展重用,藉助三稜鏡分光原理可以更好地理解其AOP橫向擴展的本質。

JDK的動態代理方式

JDK的動態代理是基於ProxyInvocationHandler實現的,其中Proxy是攔截髮生的地方,而InvocationHandler是發生調用的地方,創建動態代理方式如下:

Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{interfaceClass}, new InvokerInvocationHandler(interfaceClass));

JDK的動態代理只能應用於介面,本質原因是其動態生成一個extends Proxy implements yourInterface的代理類,如下所示,由於Java是單繼承的存在,因此針對非介面的類是無法動態代理. 其代理方法也很簡單,直接將所有操作都轉向到對應的InvocationHandler,然後用戶的InvocationHandler就可以收到相關調用資訊,然後做出相關的AOP動作.

public final class $proxy4 extends Proxy implements IUserService {      private static Method m1;      private static Method m4;      private static Method m2;      private static Method m3;      private static Method m0;        public $proxy4(InvocationHandler var1) throws  {        super(var1);      }        public final User findById(Long var1) throws  {        try {          return (User)super.h.invoke(this, m3, new Object[]{var1});        } catch (RuntimeException | Error var3) {          throw var3;        } catch (Throwable var4) {          throw new UndeclaredThrowableException(var4);        }      }    ......      public final int hashCode() throws  {        try {          return (Integer)super.h.invoke(this, m0, (Object[])null);        } catch (RuntimeException | Error var2) {          throw var2;        } catch (Throwable var3) {          throw new UndeclaredThrowableException(var3);        }      }    .....    }

Cglib的動態代理

cglib的動態代理相比JDK方式其更加靈活,支援非final修飾的類,其使用的策略是繼承,然後覆蓋上層方法,並且自己生成一個轉向上層的方法,在覆蓋的方法中傳入轉向上層的方法.說的有點抽象,看下面的hashCode實現: 其中public final int hashCode()屬於覆蓋的方法,final int CGLIB$hashCode$2()是轉向上層的方法,然後再MethodInterceptor調用時將CGLIB$hashCode$2作為Method參數傳入,這樣保證了調用時可以轉向父類已有的方法.

public class IUserService$$EnhancerByCGLIB$$4ed3797 implements IUserService, Factory {      ....    private static final Callback[] CGLIB$STATIC_CALLBACKS;    private MethodInterceptor CGLIB$CALLBACK_0;    private static final Method CGLIB$hashCode$2$Method;    private static final MethodProxy CGLIB$hashCode$2$Proxy;    private static final Method CGLIB$findById$4$Method;    private static final MethodProxy CGLIB$findById$4$Proxy;      final int CGLIB$hashCode$2() {      return super.hashCode();    }      public final int hashCode() {      MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;      if (this.CGLIB$CALLBACK_0 == null) {        CGLIB$BIND_CALLBACKS(this);        var10000 = this.CGLIB$CALLBACK_0;      }      if (var10000 != null) {        Object var1 = var10000.intercept(this, CGLIB$hashCode$2$Method, CGLIB$emptyArgs, CGLIB$hashCode$2$Proxy);        return var1 == null ? 0 : ((Number)var1).intValue();      } else {        return super.hashCode();      }    }      final User CGLIB$findById$4(Long var1) {      return super.findById(var1);    }      public final User findById(Long var1) {      MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;      if (this.CGLIB$CALLBACK_0 == null) {        CGLIB$BIND_CALLBACKS(this);        var10000 = this.CGLIB$CALLBACK_0;      }      return var10000 != null ? (User)var10000.intercept(this, CGLIB$findById$4$Method, new Object[]{var1}, CGLIB$findById$4$Proxy) : super.findById(var1);    }  }

Spring AOP的實現

Spring AOP是基於動態代理實現了一種無侵入式的程式碼擴展方式,與動態代理本身不同的是AOP的前提是已經存在了目標類的實例,因此在AOP要做的就是在目標類執行目標方法前後織入相應的操作,對於AOP的實現有兩個很重要的介面:

  • MethodInvocation: AOP需要增強的那個方法的封裝,其中包括被AOP的目標target,這個是為了解決嵌套問題所必須持有的對象.
  • MethodInterceptor: AOP的攔截,AOP相關操作一般在其內部完成. 兩者混合使用可以構造出如下結構: MethodInvocation是對HelloService.sayHello();的封裝,而MethodInterceptor持有了MethodInvocation,在調用其之前進行了增強處理,這就是AOP的實質.

處理this

假設HelloService被AOP增強,那麼調用sayHello()時執行this.sayWorld()這行程式碼會走AOP處理嗎?

public class HelloService {      public void sayHello() {      System.out.println("hello");      // 這裡調用了本類的方法      this.sayWorld();    }      public void sayWorld() {      System.out.println("world");    }  }

答案當然是不會,由上圖可以得知: 無論AOP怎麼增強最終調用sayHello()這個方法的實例一定是HelloService,那麼這裡的this也一定是HelloService,既然這樣肯定不會走AOP代理了.還有一點要理解,造成這個的原因是AOP要代理的那個類是實實在在存在的類,動態代理只是起到了方法調用的轉發作用.

解決辦法也很簡單,就是獲取到代理類,然後再執行這個方法,對於Spring,可以從ApplicationContext中獲取到當前的HelloService實例,這裡獲取到的自然是代理類,然後利用該實例調用sayWorld()就會走AOP代理了,大概形式如下,當然可以更好地封裝下.

public class HelloService implements ApplicationContextAware {    private ApplicationContext applicationContext;      public void sayHello() {      System.out.println("hello");      // 這裡拿到代理類後再執行      applicationContext.getBean("helloService", HelloService.class)          .sayWorld();    }      public void sayWorld() {      System.out.println("world");    }      @Override    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {      this.applicationContext = applicationContext;    }  }

AOP嵌套問題

動態代理之後會產生一個代理類,那麼把這個類當成target,也就是AOP後要轉向的真實類操作,封裝後然後接著AOP,就實現了嵌套.本質上是一樣的道理,既然都是實實在在的類,那麼就可以一直嵌套下去,這樣的嵌套一般會形成一個功能鏈,Mybatis的Plugin就是利用這種形式來實現的. 程式碼上的實現就是在MethodInvocation對象中存儲著要轉向的Object target,如果這個target是代理類,那麼這個傳遞轉向會向責任鏈一樣一直傳下去,直到遇到最初被AOP的真實類.

public class ReflectiveMethodInvocation implements MethodInvocation {    	private Object target;    	private Method method;    	private Object[] args;        .....  	@Override  	public Object proceed() throws Throwable {  		return method.invoke(target, args);  	}        ....  }