日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

[AOP] 2. AOP的兩種實現(xiàn)-Spring AOP以及AspectJ

 沙門空海 2017-12-13

在接觸Spring以及種類繁多的Java框架時,很多開發(fā)人員(至少包括我)都會覺得注解是個很奇妙的存在,為什么加上了@Transactional之后,方法會在一個事務(wù)的上下文中被執(zhí)行呢?為什么加上了@Cacheable之后,方法的返回值會被記錄到緩存中,從而讓下次的重復(fù)調(diào)用能夠直接利用緩存的結(jié)果呢?

隨著對AOP的逐漸應(yīng)用和了解,才明白注解只是一個表象,在幕后Spring AOP/AspectJ做了大量的工作才得以實現(xiàn)這些神奇的功能。

那么,本文就來聊一聊Spring AOP和AspectJ的那些事,它們究竟有什么魔力才讓這一切成為現(xiàn)實。

Spring AOP

基于代理(Proxy)的AOP實現(xiàn)

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

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

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

它們之間的調(diào)用先后次序反映在上圖的序號中:

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

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

@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被調(diào)用了");
  }

}

sampleBean.invokeAdvicedMethod(); // 會打印出 "Aspect被調(diào)用了" 嗎?
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

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

答案是不會。

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

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

Spring AOP的兩種實現(xiàn)方式

Spring AOP有兩種實現(xiàn)方式:

  • 基于接口的動態(tài)代理(Dynamic Proxy)
  • 基于子類化的CGLIB代理

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

@Component
public class SampleBean implements SampleInterface {

  public void advicedMethod() {

  }

  public void invokeAdvicedMethod() {
    advicedMethod();
  }

}

public interface SampleInterface {}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

在上述代碼中,我們?yōu)樵瓉淼腂ean實現(xiàn)了一個新的接口SampleInterface,這個接口中并沒有定義任何方法。這個時候,再次運行相關(guān)測試代碼的時候就會出現(xiàn)異常(摘錄了部分異常信息):

org.springframework.beans.factory.BeanCreationException: 
Error creating 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. 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

也就是說在Test類中對于Bean的Autowiring失敗了,原因是創(chuàng)建SampleBeanTest Bean的時候發(fā)生了異常。那么為什么會出現(xiàn)創(chuàng)建Bean的異常呢?從異常信息來看并不明顯,實際上這個問題的根源在于Spring AOP在創(chuàng)建代理的時候出現(xiàn)了問題。

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

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默認(rèn)使用標(biāo)準(zhǔn)的JDK動態(tài)代理來實現(xiàn)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而非接口這是必要的。如果一個業(yè)務(wù)對象沒有實現(xiàn)任何接口,那么默認(rèn)會使用CGLIB。由于面向接口而非面向classes編程是一個良好的實踐;業(yè)務(wù)對象通常都會實現(xiàn)一個或者多個業(yè)務(wù)接口。強制使用CGLIB也是可能的(希望這種情況很少),此時你需要advise的方法沒有被定義在接口中,或者你需要向方法中傳入一個具體的對象作為代理對象。

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

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

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

強制使用CGLIB很簡單:

@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
@ComponentScan(basePackages = "com.destiny1020")
public class CommonConfiguration {}
  • 1
  • 2
  • 3
  • 4

@EnableAspectJAutoProxy注解中添加屬性proxyTargetClass = true即可。
CGLIB實現(xiàn)AOP代理的原理是通過動態(tài)地創(chuàng)建一個目標(biāo)Bean的子類來實現(xiàn)的,該子類的實例就是AOP代理,它建立起了目標(biāo)Bean到Advice的聯(lián)系。

當(dāng)然還有另外一種解決方案,那就是將方法定義聲明在新創(chuàng)建的接口中并且去掉之前添加的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 {}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

這樣就讓業(yè)務(wù)對象實現(xiàn)了一個接口,從而能夠使用基于標(biāo)準(zhǔn)JDK的動態(tài)代理來完成Spring AOP代理對象的創(chuàng)建。

從Debug Stacktrace的角度也可以看出這兩種AOP實現(xiàn)方式上的區(qū)別:

  • JDK動態(tài)代理

  • CGLIB

關(guān)于動態(tài)代理和CGLIB這兩種方式的簡要總結(jié)如下:

  • JDK動態(tài)代理(Dynamic Proxy)

    • 基于標(biāo)準(zhǔn)JDK的動態(tài)代理功能
    • 只針對實現(xiàn)了接口的業(yè)務(wù)對象
  • CGLIB

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

AspectJ

AspectJ是Eclipse旗下的一個項目。至于它和Spring AOP的關(guān)系,不妨可將Spring AOP看成是Spring這個龐大的集成框架為了集成AspectJ而出現(xiàn)的一個模塊。

畢竟很多地方都是直接用到AspectJ里面的代碼。典型的比如@Aspect,@Around,@Pointcut注解等等。而且從相關(guān)概念以及語法結(jié)構(gòu)上而言,兩者其實非常非常相似。比如Pointcut的表達式語法以及Advice的種類,都是一樣一樣的。

那么,它們的區(qū)別在哪里呢?

最大的區(qū)別在于兩者實現(xiàn)AOP的底層原理不太一樣:

  • Spring AOP: 基于代理(Proxying)
  • AspectJ: 基于字節(jié)碼操作(Bytecode Manipulation)

用一張圖來表示AspectJ使用的字節(jié)碼操作,就一目了然了:

通過編織階段(Weaving Phase),對目標(biāo)Java類型的字節(jié)碼進行操作,將需要的Advice邏輯給編織進去,形成新的字節(jié)碼。畢竟JVM執(zhí)行的都是Java源代碼編譯后得到的字節(jié)碼,所以AspectJ相當(dāng)于在這個過程中做了一點手腳,讓Advice能夠參與進來。

而編織階段可以有兩個選擇,分別是加載時編織(也可以成為運行時編織)和編譯時編織:

加載時編織(Load-Time Weaving)

顧名思義,這種編織方式是在JVM加載類的時候完成的。

使用它需要進行相關(guān)的配置,舉例如下:

在類路徑的META-INF目錄下創(chuàng)建一個文件名為aop.xml:

<aspectj>
  <weaver>
    <include within="com.destiny1020..*" />
  </weaver>
  <aspects>
    <aspect name="com.destiny1020.SampleAspect" />
  </aspects>
</aspectj>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

然后添加啟動參數(shù),直接使用AspectJ提供的或者使用Spring提供的工具:

# AspectJ
-javaagent:path_to/aspectjweaver-{version}.jar

# Spring
-javaagent:path_to/org.springframework.instrument-{version}.jar
  • 1
  • 2
  • 3
  • 4
  • 5

當(dāng)使用Spring提供的工具時,還需要進行一些配置,以JavaConfig為例:

@Configuration
@EnableLoadTimeWeaving
@ComponentScan(basePackages = "com.destiny1020")
public class CommonConfiguration {}
  • 1
  • 2
  • 3
  • 4

重點就是上述的@EnableLoadTimeWeaving。

編譯時編織(Compile-Time Weaving)

需要使用AspectJ的編譯器來替換JDK的編譯器??梢越柚鶰aven AspectJ來實現(xiàn),下面是一例:

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>aspectj-maven-plugin</artifactId>
    <version>1.4</version>
    <dependencies>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>${aspectj.version}</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjtools</artifactId>
            <version>${aspectj.version}</version>
        </dependency>
    </dependencies>
    <executions>
        <execution>
            <phase>process-sources</phase>
            <goals>
                <goal>compile</goal>
                <goal>test-compile</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <outxml>true</outxml>
        <source>${java.version}</source>
        <target>${java.version}</target>
    </configuration>
</plugin>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

然后直接通過mvn test進行測試:

自定義的編譯錯誤/警告

舉個例子,有兩個Service1和Service2分別位于兩個包Package1和Package2下,只能在Package2中調(diào)用來自本包內(nèi)部的方法,在Service1中調(diào)用Service2中提供的方法會導(dǎo)致編譯錯誤(能夠用訪問控制符解決的問題強行用這種方式來解決,當(dāng)然只是為了說明問題:)):

@Aspect
public class EmitCompilationErrorAspect {

  @DeclareError("call (* com.destiny1020.biz.package2..*.*(..))"
      + "&& !within(com.destiny1020.biz.package2..*)")
  public static final String notInBizPackage2 = "只能在Package2中調(diào)用來自Package2的方法";

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
package com.destiny1020.biz.package1;

import com.destiny1020.biz.package2.ServiceInPackage2;

public class ServiceInPackage1 {

  ServiceInPackage2 service2 = new ServiceInPackage2();

  public void invokeMethodInPackage2() {
    service2.doBizInPackage2();  // 這里理應(yīng)會出現(xiàn)編譯錯誤
  }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

實際情況也正式如此:

在聲明編譯錯誤Pointcut的時候,出現(xiàn)了兩個新概念:

  • call
  • within

這兩個新出現(xiàn)的Pointcut原語只能在使用AspectJ作為AOP實現(xiàn)的時候才可用。它們表達的是什么意思呢:

  • call:針對所有的調(diào)用者(caller),即哪里調(diào)用了Pointcut表達式匹配的方法,在該方法被執(zhí)行之前就會被匹配到;而我們經(jīng)常使用的execution則是針對所有的被調(diào)用方法,而不會care是誰調(diào)用的該方法
  • within:這個很好理解,它的Pointcut表達式是一個用來匹配完整限定類名的表達式,比如上例中的!within(com.destiny1020.biz.package2..*)意味不在包com.destiny1020.biz.package2中的類。

在使用AspectJ的編譯時編織功能時,由于使用了AspectJ Compiler來完成代碼的編譯,因此可以根據(jù)編碼規(guī)范添加相應(yīng)的編譯錯誤/警告,來進一步地讓代碼更加規(guī)范。這個特性對于輔助實現(xiàn)大型項目的編碼規(guī)范還是很有益處的。

哪種方式更好

先下結(jié)論:It Depends.

得根據(jù)具體需求,不過我個人認(rèn)為在對AOP的需求不那么深入和迫切的時候,使用Spring AOP足矣。

畢竟Spring作為一個以集成起家的框架,在設(shè)計Spring AOP的時候也是為了減輕開發(fā)人員負(fù)擔(dān)而做了不少努力的。它提供的開箱即用(Out-of-the-box)的眾多AOP功能讓很多開發(fā)人員甚至都不知道什么是AOP,就算知道了AOP是Spring的一大基石或者@Transactional和@Cacheable等等常用注解是借助了AOP的力量,但是再深入恐怕就有點勉為其難了。這是優(yōu)點也是缺點,當(dāng)需要對AOP的實現(xiàn)做出精細化調(diào)整的時候,就會有力不從心的感覺。

這個時候,就可以考慮使用AspectJ。AspectJ的功能更加全面和強大。支持全部的Pointcut類型。

這里進行了一個簡單的比較,摘錄并簡單翻譯(括號內(nèi)是我添加的補充)如下:

Spring-AOP Pros

  • 比AspectJ更簡單,不需要使用Load-Time Weaving以及AspectJ編譯器(為了Compile-Time Weaving)
  • 當(dāng)使用@Aspect注解時可以很方便的遷移到AspectJ AOP實現(xiàn)
  • 使用代理模式和裝飾模式

Spring-AOP Cons

  • 由于是基于代理的AOP,所以基本上只能選擇方法execution這一個Pointcut原語
  • 在類本身中調(diào)用另一個方法的時候Aspects不會生效
  • 有一點運行時的額外開銷
  • 無法為不是從Spring Factory中創(chuàng)建的對象添加Aspect(只對Spring Bean有效)

AspectJ Pros

  • 支持所有的Pointcut原語,這意味著你可以做任何事情
  • 運行時開銷比Spring AOP少
  • 能夠添加各種編譯錯誤來保障代碼質(zhì)量(這一條是我自己添加的)

AspectJ Cons

  • 當(dāng)心。檢查是否發(fā)生了意料之外的Weaving操作
  • 使用Compile-Time Weaving時需要額外的構(gòu)建步驟(使用AspectJ Compiler),在使用Load-Time Weaving時需要一些配置(-javaassist)

參考資料

Spring AOP Reference

AspectJ Quick Reference

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多