在接觸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為例):
- 調(diào)用者Beans - 即調(diào)用發(fā)起者,它只知道目標(biāo)方法所在Bean,并不清楚代理以及Advice的存在
- 目標(biāo)方法所在Bean - 被調(diào)用的目標(biāo)方法
- 生成的代理 - 由Spring AOP為目標(biāo)方法所在Bean生成的一個代理對象
- Advice - 切面的執(zhí)行邏輯
它們之間的調(diào)用先后次序反映在上圖的序號中:
- 調(diào)用者Bean嘗試調(diào)用目標(biāo)方法,但是被生成的代理截了胡
- 代理根據(jù)Advice的種類(本例中是@Before Advice),對Advice首先進行調(diào)用
- 代理調(diào)用目標(biāo)方法
- 返回調(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 {}
在上述代碼中,我們?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.
也就是說在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 {}
向@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ū)別:


關(guān)于動態(tài)代理和CGLIB這兩種方式的簡要總結(jié)如下:
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>
然后添加啟動參數(shù),直接使用AspectJ提供的或者使用Spring提供的工具:
# AspectJ
-javaagent:path_to/aspectjweaver-{version}.jar
# Spring
-javaagent:path_to/org.springframework.instrument-{version}.jar
當(dāng)使用Spring提供的工具時,還需要進行一些配置,以JavaConfig為例:
@Configuration
@EnableLoadTimeWeaving
@ComponentScan(basePackages = "com.destiny1020")
public class CommonConfiguration {}
重點就是上述的@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的方法";
}
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)編譯錯誤
}
}
實際情況也正式如此:

在聲明編譯錯誤Pointcut的時候,出現(xiàn)了兩個新概念:
這兩個新出現(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
|