1. 程式人生 > >Spring AOP實現原理

Spring AOP實現原理

asp 默認 RR force HERE 針對 解決 之前 中介

基於代理(Proxy)的AOP實現

首先,這是一種基於代理(Proxy)的實現方式。下面這張圖很好地表達了這層關系:

技術分享圖片

這張圖反映了參與到AOP過程中的幾個關鍵組件(以@Before Advice為例):

  1. 調用者Beans - 即調用發起者,它只知道目標方法所在Bean,並不清楚代理以及Advice的存在
  2. 目標方法所在Bean - 被調用的目標方法
  3. 生成的代理 - 由Spring AOP為目標方法所在Bean生成的一個代理對象
  4. Advice - 切面的執行邏輯

它們之間的調用先後次序反映在上圖的序號中:

  1. 調用者Bean嘗試調用目標方法,但是被生成的代理截了胡
  2. 代理根據Advice的種類(本例中是@Before Advice),對Advice首先進行調用
  3. 代理調用目標方法
  4. 返回調用結果給調用者Bean(由代理返回,沒有體現在圖中)

為了理解清楚這張圖的意思和代理在中間扮演的角色,不妨看看下面的代碼:

@Component
public class SampleBean {

  public void advicedMethod() {

  }

  public void invokeAdvicedMethod() {
    advicedMethod();
  }

}

@Aspect
@Component
public class SampleAspect {

  @Before("execution(void advicedMethod())")
  public void logException() {
    System.out.println("Aspect被調用了");
  }

}

sampleBean.invokeAdvicedMethod(); // 會打印出 "Aspect被調用了" 嗎?

SampleBean扮演的就是目標方法所在Bean的角色,而SampleAspect扮演的則是Advice的角色。很顯然,被AOP修飾過的方法是advicedMethod(),而非invokeAdvicedMethod()。然而,invokeAdvicedMethod()方法在內部調用了advicedMethod()。那麽會打印出來Advice中的輸出嗎?

答案是不會

如果想不通為什麽會這樣,不妨再去仔細看看上面的示意圖。

這是在使用Spring AOP的時候可能會遇到的一個問題。類似這種間接調用不會觸發Advice的原因在於調用發生在目標方法所在Bean的內部,和外面的代理對象可是沒有半毛錢的關系哦。我們可以把這個代理想象成一個中介,只有它知道Advice的存在,調用者Bean和目標方法所在Bean知道彼此的存在,但是對於代理或者是Advice卻是一無所知的。因此,沒有通過代理的調用是絕無可能觸發Advice的邏輯的。如下圖所示:

技術分享圖片

Spring AOP的兩種實現方式

Spring AOP有兩種實現方式:

  • 基於接口的動態代理(Dynamic Proxy)
  • 基於子類化的CGLIB代理

我們在使用Spring AOP的時候,一般是不需要選擇具體的實現方式的。Spring AOP能根據上下文環境幫助我們選擇一種合適的。那麽是不是每次都能夠這麽”智能”地選擇出來呢?也不盡然,下面的例子就反映了這個問題:

@Component
public class SampleBean implements SampleInterface {

  public void advicedMethod() {

  }

  public void invokeAdvicedMethod() {
    advicedMethod();
  }

}

public interface SampleInterface {}

在上述代碼中,我們為原來的Bean實現了一個新的接口SampleInterface,這個接口中並沒有定義任何方法。這個時候,再次運行相關測試代碼的時候就會出現異常(摘錄了部分異常信息):

org.springframework.beans.factory.BeanCreationException: 
Error ceating bean with name ‘com.destiny1020.SampleBeanTest‘: 
Injection of autowired dependencies failedCaused by: 
org.springframework.beans.factory.NoSuchBeanDefinitionException: 
No qualifying bean of type [com.destiny1020.SampleBean] found for dependency: 
expected at least 1 bean which qualifies as autowire candidate for this dependency. 

也就是說在Test類中對於Bean的Autowiring失敗了,原因是創建SampleBeanTest Bean的時候發生了異常。那麽為什麽會出現創建Bean的異常呢?從異常信息來看並不明顯,實際上這個問題的根源在於Spring AOP在創建代理的時候出現了問題。

這個問題的根源可以在這裏得到一些線索:

Spring AOP Reference - AOP Proxies

文檔中是這樣描述的(每段後加上了翻譯):

Spring AOP defaults to using standard JDK dynamic proxies for AOP proxies. This enables any interface (or set of interfaces) to be proxied.

Spring AOP默認使用標準的JDK動態代理來實現AOP代理。這能使任何借口(或者一組接口)被代理。

Spring AOP can also use CGLIB proxies. This is necessary to proxy classes rather than interfaces. CGLIB is used by default if a business object does not implement an interface. As it is good practice to program to interfaces rather than classes; business classes normally will implement one or more business interfaces. It is possible to force the use of CGLIB, in those (hopefully rare) cases where you need to advise a method that is not declared on an interface, or where you need to pass a proxied object to a method as a concrete type.

Spring AOP也使用CGLIB代理。對於代理classes而非接口這是必要的。如果一個業務對象沒有實現任何接口,那麽默認會使用CGLIB。由於面向接口而非面向classes編程是一個良好的實踐;業務對象通常都會實現一個或者多個業務接口。強制使用CGLIB也是可能的(希望這種情況很少),此時你需要advise的方法沒有被定義在接口中,或者你需要向方法中傳入一個具體的對象作為代理對象。

因此,上面異常的原因在於:

強制使用CGLIB也是可能的(希望這種情況很少),此時你需要advise的方法沒有被定義在接口中。

我們需要advise的方法是SampleBean中的advicedMethod方法。而在添加接口後,這個方法並沒有被定義在該接口中。所以正如文檔所言,我們需要強制使用CGLIB來避免這個問題。

強制使用CGLIB很簡單:

@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
@ComponentScan(basePackages = "com.destiny1020")
public class CommonConfiguration {}

@EnableAspectJAutoProxy註解中添加屬性proxyTargetClass = true即可。
CGLIB實現AOP代理的原理是通過動態地創建一個目標Bean的子類來實現的,該子類的實例就是AOP代理,它建立起了目標Bean到Advice的聯系。

當然還有另外一種解決方案,那就是將方法定義聲明在新創建的接口中並且去掉之前添加的proxyTargetClass = true

@Component
public class SampleBean implements SampleInterface {

  @Override
  public void advicedMethod() {

  }

  @Override
  public void invokeAdvicedMethod() {
    advicedMethod();
  }

}

public interface SampleInterface {

  void invokeAdvicedMethod();

  void advicedMethod();

}

@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages = "com.destiny1020")
public class CommonConfiguration {}
  • 從Debug Stacktrace的角度也可以看出這兩種AOP實現方式上的區別:
  • JDK動態代理
  • 技術分享圖片
  • CGLIB
  • 技術分享圖片
  • 關於動態代理和CGLIB這兩種方式的簡要總結如下:

    • JDK動態代理(Dynamic Proxy)

      • 基於標準JDK的動態代理功能
      • 只針對實現了接口的業務對象
    • CGLIB

      • 通過動態地對目標對象進行子類化來實現AOP代理,上面截圖中的SampleBean$$EnhancerByCGLIB$$1767dd4b即為動態創建的一個子類
      • 需要指定@EnableAspectJAutoProxy(proxyTargetClass = true)來強制使用
      • 當業務對象沒有實現任何接口的時候默認會選擇CGLIB

Spring AOP實現原理