在接觸Spring以及種類繁多的Java框架時(shí),很多開發(fā)人員(至少包括我)都會(huì)覺得注解是個(gè)很奇妙的存在,為什么加上了@Transactional之后,方法會(huì)在一個(gè)事務(wù)的上下文中被執(zhí)行呢?為什么加上了@Cacheable之后,方法的返回值會(huì)被記錄到緩存中,從而讓下次的重復(fù)調(diào)用能夠直接利用緩存的結(jié)果呢?
隨著對(duì)AOP的逐漸應(yīng)用和了解,才明白注解只是一個(gè)表象,在幕后Spring AOP/AspectJ做了大量的工作才得以實(shí)現(xiàn)這些神奇的功能。
那么,本文就來聊一聊Spring AOP和AspectJ的那些事,它們究竟有什么魔力才讓這一切成為現(xiàn)實(shí)。
首先,這是一種基于代理(Proxy)的實(shí)現(xiàn)方式。下面這張圖很好地表達(dá)了這層關(guān)系:
這張圖反映了參與到AOP過程中的幾個(gè)關(guān)鍵組件(以@Before Advice為例):
它們之間的調(diào)用先后次序反映在上圖的序號(hào)中:
為了理解清楚這張圖的意思和代理在中間扮演的角色,不妨看看下面的代碼:
@Componentpublic class SampleBean { public void advicedMethod() { } public void invokeAdvicedMethod() { advicedMethod(); }}@Aspect@Componentpublic class SampleAspect { @Before("execution(void advicedMethod())") public void logException() { System.out.println("Aspect被調(diào)用了"); }}sampleBean.invokeAdvicedMethod(); // 會(huì)打印出 "Aspect被調(diào)用了" 嗎?
SampleBean
扮演的就是目標(biāo)方法所在Bean的角色,而SampleAspect
扮演的則是Advice的角色。很顯然,被AOP修飾過的方法是advicedMethod()
,而非invokeAdvicedMethod()
。然而,invokeAdvicedMethod()
方法在內(nèi)部調(diào)用了advicedMethod()
。那么會(huì)打印出來Advice中的輸出嗎?
答案是不會(huì)。
如果想不通為什么會(huì)這樣,不妨再去仔細(xì)看看上面的示意圖。
這是在使用Spring AOP的時(shí)候可能會(huì)遇到的一個(gè)問題。類似這種間接調(diào)用不會(huì)觸發(fā)Advice的原因在于調(diào)用發(fā)生在目標(biāo)方法所在Bean的內(nèi)部,和外面的代理對(duì)象可是沒有半毛錢的關(guān)系哦。我們可以把這個(gè)代理想象成一個(gè)中介,只有它知道Advice的存在,調(diào)用者Bean和目標(biāo)方法所在Bean知道彼此的存在,但是對(duì)于代理或者是Advice卻是一無所知的。因此,沒有通過代理的調(diào)用是絕無可能觸發(fā)Advice的邏輯的。如下圖所示:
Spring AOP有兩種實(shí)現(xiàn)方式:
我們?cè)谑褂肧pring AOP的時(shí)候,一般是不需要選擇具體的實(shí)現(xiàn)方式的。Spring AOP能根據(jù)上下文環(huán)境幫助我們選擇一種合適的。那么是不是每次都能夠這么”智能”地選擇出來呢?也不盡然,下面的例子就反映了這個(gè)問題:
@Componentpublic class SampleBean implements SampleInterface { public void advicedMethod() { } public void invokeAdvicedMethod() { advicedMethod(); }}public interface SampleInterface {}
在上述代碼中,我們?yōu)樵瓉淼腂ean實(shí)現(xiàn)了一個(gè)新的接口SampleInterface
,這個(gè)接口中并沒有定義任何方法。這個(gè)時(shí)候,再次運(yùn)行相關(guān)測(cè)試代碼的時(shí)候就會(huì)出現(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類中對(duì)于Bean的Autowiring失敗了,原因是創(chuàng)建SampleBeanTest Bean的時(shí)候發(fā)生了異常。那么為什么會(huì)出現(xiàn)創(chuàng)建Bean的異常呢?從異常信息來看并不明顯,實(shí)際上這個(gè)問題的根源在于Spring AOP在創(chuàng)建代理的時(shí)候出現(xiàn)了問題。
這個(gè)問題的根源可以在這里得到一些線索:
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動(dòng)態(tài)代理來實(shí)現(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代理。對(duì)于代理classes而非接口這是必要的。如果一個(gè)業(yè)務(wù)對(duì)象沒有實(shí)現(xiàn)任何接口,那么默認(rèn)會(huì)使用CGLIB。由于面向接口而非面向classes編程是一個(gè)良好的實(shí)踐;業(yè)務(wù)對(duì)象通常都會(huì)實(shí)現(xiàn)一個(gè)或者多個(gè)業(yè)務(wù)接口。強(qiáng)制使用CGLIB也是可能的(希望這種情況很少),此時(shí)你需要advise的方法沒有被定義在接口中,或者你需要向方法中傳入一個(gè)具體的對(duì)象作為代理對(duì)象。
因此,上面異常的原因在于:
強(qiáng)制使用CGLIB也是可能的(希望這種情況很少),此時(shí)你需要advise的方法沒有被定義在接口中。
我們需要advise的方法是SampleBean中的advicedMethod方法。而在添加接口后,這個(gè)方法并沒有被定義在該接口中。所以正如文檔所言,我們需要強(qiáng)制使用CGLIB來避免這個(gè)問題。
強(qiáng)制使用CGLIB很簡單:
@Configuration@EnableAspectJAutoProxy(proxyTargetClass = true)@ComponentScan(basePackages = "com.destiny1020")public class CommonConfiguration {}
向@EnableAspectJAutoProxy
注解中添加屬性proxyTargetClass = true
即可。
CGLIB實(shí)現(xiàn)AOP代理的原理是通過動(dòng)態(tài)地創(chuàng)建一個(gè)目標(biāo)Bean的子類來實(shí)現(xiàn)的,該子類的實(shí)例就是AOP代理,它建立起了目標(biāo)Bean到Advice的聯(lián)系。
當(dāng)然還有另外一種解決方案,那就是將方法定義聲明在新創(chuàng)建的接口中并且去掉之前添加的proxyTargetClass = true
:
@Componentpublic 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 {}
這樣就讓業(yè)務(wù)對(duì)象實(shí)現(xiàn)了一個(gè)接口,從而能夠使用基于標(biāo)準(zhǔn)JDK的動(dòng)態(tài)代理來完成Spring AOP代理對(duì)象的創(chuàng)建。
從Debug Stacktrace的角度也可以看出這兩種AOP實(shí)現(xiàn)方式上的區(qū)別:
關(guān)于動(dòng)態(tài)代理和CGLIB這兩種方式的簡要總結(jié)如下:
JDK動(dòng)態(tài)代理(Dynamic Proxy)
CGLIB
SampleBean$$EnhancerByCGLIB$$1767dd4b
即為動(dòng)態(tài)創(chuàng)建的一個(gè)子類@EnableAspectJAutoProxy(proxyTargetClass = true)
來強(qiáng)制使用AspectJ是Eclipse旗下的一個(gè)項(xiàng)目。至于它和Spring AOP的關(guān)系,不妨可將Spring AOP看成是Spring這個(gè)龐大的集成框架為了集成AspectJ而出現(xiàn)的一個(gè)模塊。
畢竟很多地方都是直接用到AspectJ里面的代碼。典型的比如@Aspect
,@Around
,@Pointcut
注解等等。而且從相關(guān)概念以及語法結(jié)構(gòu)上而言,兩者其實(shí)非常非常相似。比如Pointcut的表達(dá)式語法以及Advice的種類,都是一樣一樣的。
那么,它們的區(qū)別在哪里呢?
最大的區(qū)別在于兩者實(shí)現(xiàn)AOP的底層原理不太一樣:
用一張圖來表示AspectJ使用的字節(jié)碼操作,就一目了然了:
通過編織階段(Weaving Phase),對(duì)目標(biāo)Java類型的字節(jié)碼進(jìn)行操作,將需要的Advice邏輯給編織進(jìn)去,形成新的字節(jié)碼。畢竟JVM執(zhí)行的都是Java源代碼編譯后得到的字節(jié)碼,所以AspectJ相當(dāng)于在這個(gè)過程中做了一點(diǎn)手腳,讓Advice能夠參與進(jìn)來。
而編織階段可以有兩個(gè)選擇,分別是加載時(shí)編織(也可以成為運(yùn)行時(shí)編織)和編譯時(shí)編織:
顧名思義,這種編織方式是在JVM加載類的時(shí)候完成的。
使用它需要進(jìn)行相關(guān)的配置,舉例如下:
在類路徑的META-INF目錄下創(chuàng)建一個(gè)文件名為aop.xml:
<aspectj> <weaver> <include within="com.destiny1020..*" /> </weaver> <aspects> <aspect name="com.destiny1020.SampleAspect" /> </aspects></aspectj>
然后添加啟動(dòng)參數(shù),直接使用AspectJ提供的或者使用Spring提供的工具:
# AspectJ-javaagent:path_to/aspectjweaver-{version}.jar# Spring-javaagent:path_to/org.springframework.instrument-{version}.jar
當(dāng)使用Spring提供的工具時(shí),還需要進(jìn)行一些配置,以JavaConfig為例:
@Configuration@EnableLoadTimeWeaving@ComponentScan(basePackages = "com.destiny1020")public class CommonConfiguration {}
重點(diǎn)就是上述的@EnableLoadTimeWeaving
。
需要使用AspectJ的編譯器來替換JDK的編譯器??梢越柚鶰aven AspectJ來實(shí)現(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>
然后直接通過mvn test
進(jìn)行測(cè)試:
自定義的編譯錯(cuò)誤/警告
舉個(gè)例子,有兩個(gè)Service1和Service2分別位于兩個(gè)包Package1和Package2下,只能在Package2中調(diào)用來自本包內(nèi)部的方法,在Service1中調(diào)用Service2中提供的方法會(huì)導(dǎo)致編譯錯(cuò)誤(能夠用訪問控制符解決的問題強(qiáng)行用這種方式來解決,當(dāng)然只是為了說明問題:)):
@Aspectpublic 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)會(huì)出現(xiàn)編譯錯(cuò)誤 }}
實(shí)際情況也正式如此:
在聲明編譯錯(cuò)誤Pointcut的時(shí)候,出現(xiàn)了兩個(gè)新概念:
這兩個(gè)新出現(xiàn)的Pointcut原語只能在使用AspectJ作為AOP實(shí)現(xiàn)的時(shí)候才可用。它們表達(dá)的是什么意思呢:
!within(com.destiny1020.biz.package2..*)
意味不在包com.destiny1020.biz.package2
中的類。在使用AspectJ的編譯時(shí)編織功能時(shí),由于使用了AspectJ Compiler來完成代碼的編譯,因此可以根據(jù)編碼規(guī)范添加相應(yīng)的編譯錯(cuò)誤/警告,來進(jìn)一步地讓代碼更加規(guī)范。這個(gè)特性對(duì)于輔助實(shí)現(xiàn)大型項(xiàng)目的編碼規(guī)范還是很有益處的。
先下結(jié)論:It Depends.
得根據(jù)具體需求,不過我個(gè)人認(rèn)為在對(duì)AOP的需求不那么深入和迫切的時(shí)候,使用Spring AOP足矣。
畢竟Spring作為一個(gè)以集成起家的框架,在設(shè)計(jì)Spring AOP的時(shí)候也是為了減輕開發(fā)人員負(fù)擔(dān)而做了不少努力的。它提供的開箱即用(Out-of-the-box)的眾多AOP功能讓很多開發(fā)人員甚至都不知道什么是AOP,就算知道了AOP是Spring的一大基石或者@Transactional和@Cacheable等等常用注解是借助了AOP的力量,但是再深入恐怕就有點(diǎn)勉為其難了。這是優(yōu)點(diǎn)也是缺點(diǎn),當(dāng)需要對(duì)AOP的實(shí)現(xiàn)做出精細(xì)化調(diào)整的時(shí)候,就會(huì)有力不從心的感覺。
這個(gè)時(shí)候,就可以考慮使用AspectJ。AspectJ的功能更加全面和強(qiáng)大。支持全部的Pointcut類型。
這里進(jìn)行了一個(gè)簡單的比較,摘錄并簡單翻譯(括號(hào)內(nèi)是我添加的補(bǔ)充)如下:
Spring-AOP Pros
Spring-AOP Cons
AspectJ Pros
AspectJ Cons
聯(lián)系客服