本文轉(zhuǎn)自?黃勇
本文是《AOP 那點(diǎn)事兒》的續(xù)集灵疮。
在上篇中谆棱,我們從寫死代碼阶祭,到使用代理;從編程式?Spring AOP 到聲明式 Spring AOP勒奇。一切都朝著簡單實(shí)用主義的方向在發(fā)展预鬓。沿著 Spring AOP 的方向,Rod Johnson(老羅)花了不少心思赊颠,都是為了讓我們使用 Spring 框架時(shí)不會(huì)感受到麻煩格二,但事實(shí)卻并非如此。那么竣蹦,后來老羅究竟對(duì) Spring AOP 做了哪些改進(jìn)呢顶猜?
現(xiàn)在繼續(xù)!
9.?Spring AOP:切面
之前談到的 AOP 框架其實(shí)可以將它理解為一個(gè)攔截器框架痘括,但這個(gè)攔截器似乎非常武斷长窄。比如說滔吠,如果它攔截了一個(gè)類,那么它就攔截了這個(gè)類中所有的方法挠日。類似地疮绷,當(dāng)我們?cè)谑褂脛?dòng)態(tài)代理的時(shí)候,其實(shí)也遇到了這個(gè)問題肆资。需要在代碼中對(duì)所攔截的方法名加以判斷矗愧,才能過濾出我們需要攔截的方法,想想這種做法確實(shí)不太優(yōu)雅郑原。在大量的真實(shí)項(xiàng)目中唉韭,似乎我們只需要攔截特定的方法就行了,沒必要攔截所有的方法犯犁。于是属愤,老羅同志借助了 AOP 的一個(gè)很重要的工具,Advisor(切面)酸役,來解決這個(gè)問題住诸。它也是 AOP 中的核心!是我們關(guān)注的重點(diǎn)涣澡!
也就是說贱呐,我們可以通過切面,將增強(qiáng)類與攔截匹配條件組合在一起入桂,然后將這個(gè)切面配置到 ProxyFactory 中奄薇,從而生成代理。
這里提到這個(gè)“攔截匹配條件”在 AOP 中就叫做?Pointcut(切點(diǎn))抗愁,其實(shí)說白了就是一個(gè)基于表達(dá)式的攔截條件罷了馁蒂。
歸納一下,Advisor(切面)封裝了 Advice(增強(qiáng))與 Pointcut(切點(diǎn)?)蜘腌。當(dāng)您理解了這句話后沫屡,就往下看吧。
我在 GreetingImpl 類中故意增加了兩個(gè)方法撮珠,都以“good”開頭沮脖。下面要做的就是攔截這兩個(gè)新增的方法,而對(duì) sayHello() 方法不作攔截芯急。
@Componentpublicclass?GreetingImpl?implements?Greeting?{@Overridepublic?void?sayHello(String?name)?{????????System.out.println("Hello!?"+?name);????}public?void?goodMorning(String?name)?{????????System.out.println("Good?Morning!?"+?name);????}public?void?goodNight(String?name)?{????????System.out.println("Good?Night!?"+?name);????}}
在 Spring AOP 中倘潜,老羅已經(jīng)給我們提供了許多切面類了,這些切面類我個(gè)人感覺最好用的就是基于正則表達(dá)式的切面類志于′桃颍看看您就明白了:
注意以上代理對(duì)象的配置中的?interceptorNames,它不再是一個(gè)增強(qiáng)伺绽,而是一個(gè)切面养泡,因?yàn)橐呀?jīng)將增強(qiáng)封裝到該切面中了嗜湃。此外,切面還定義了一個(gè)切點(diǎn)(正則表達(dá)式)澜掩,其目的是為了只將滿足切點(diǎn)匹配條件的方法進(jìn)行攔截购披。
需要強(qiáng)調(diào)的是,這里的切點(diǎn)表達(dá)式是基于正則表達(dá)式的肩榕。示例中的“aop.demo.GreetingImpl.good.*”表達(dá)式后面的“.*”表示匹配所有字符刚陡,翻譯過來就是“匹配 aop.demo.GreetingImpl 類中以 good 開頭的方法”。
除了?RegexpMethodPointcutAdvisor 以外株汉,在 Spring AOP 中還提供了幾個(gè)切面類筐乳,比如:
DefaultPointcutAdvisor:默認(rèn)切面(可擴(kuò)展它來自定義切面)
NameMatchMethodPointcutAdvisor:根據(jù)方法名稱進(jìn)行匹配的切面
StaticMethodMatcherPointcutAdvisor:用于匹配靜態(tài)方法的切面
總的來說,讓用戶去配置一個(gè)或少數(shù)幾個(gè)代理乔妈,似乎還可以接受蝙云,但隨著項(xiàng)目的擴(kuò)大,代理配置就會(huì)越來越多路召,配置的重復(fù)勞動(dòng)就多了勃刨,麻煩不說,還很容易出錯(cuò)股淡。能否讓 Spring 框架為我們自動(dòng)生成代理呢身隐?
10.?Spring AOP:自動(dòng)代理(掃描 Bean 名稱)
Spring AOP 提供了一個(gè)可根據(jù) Bean 名稱來自動(dòng)生成代理的工具,它就是?BeanNameAutoProxyCreator唯灵。是這樣配置的:
...
以上使用 BeanNameAutoProxyCreator 只為后綴為“Impl”的 Bean 生成代理贾铝。需要注意的是,這個(gè)地方我們不能定義代理接口早敬,也就是?interfaces 屬性忌傻,因?yàn)槲覀兏揪筒恢肋@些 Bean 到底實(shí)現(xiàn)了多少接口大脉。此時(shí)不能代理接口搞监,而只能代理類。所以這里提供了一個(gè)新的配置項(xiàng)镰矿,它就是?optimize琐驴。若為 true 時(shí),則可對(duì)代理生成策略進(jìn)行優(yōu)化(默認(rèn)是 false 的)秤标。也就是說绝淡,如果該類有接口,就代理接口(使用 JDK 動(dòng)態(tài)代理)苍姜;如果沒有接口牢酵,就代理類(使用 CGLib 動(dòng)態(tài)代理)。而并非像之前使用的?proxyTargetClass 屬性那樣衙猪,強(qiáng)制代理類馍乙,而不考慮代理接口的方式布近。可見 Spring AOP 確實(shí)為我們提供了很多很好地服務(wù)丝格!
既然 CGLib 可以代理任何的類了撑瞧,那為什么還要用 JDK 的動(dòng)態(tài)代理呢?肯定您會(huì)這樣問显蝌。
根據(jù)多年來實(shí)際項(xiàng)目經(jīng)驗(yàn)得知:CGLib 創(chuàng)建代理的速度比較慢预伺,但創(chuàng)建代理后運(yùn)行的速度卻非常快曼尊,而 JDK 動(dòng)態(tài)代理正好相反酬诀。如果在運(yùn)行的時(shí)候不斷地用 CGLib 去創(chuàng)建代理,系統(tǒng)的性能會(huì)大打折扣涩禀,所以建議一般在系統(tǒng)初始化的時(shí)候用 CGLib 去創(chuàng)建代理料滥,并放入 Spring 的 ApplicationContext?中以備后用。
以上這個(gè)例子只能匹配目標(biāo)類艾船,而不能進(jìn)一步匹配其中指定的方法葵腹,要匹配方法,就要考慮使用切面與切點(diǎn)了屿岂。Spring AOP 基于切面也提供了一個(gè)自動(dòng)代理生成器:DefaultAdvisorAutoProxyCreator践宴。
11.?Spring AOP:自動(dòng)代理(掃描切面配置)
為了匹配目標(biāo)類中的指定方法,我們?nèi)匀恍枰?Spring 中配置切面與切點(diǎn):
...
這里無需再配置代理了爷怀,因?yàn)榇韺?huì)由?DefaultAdvisorAutoProxyCreator 自動(dòng)生成阻肩。也就是說,這個(gè)類可以掃描所有的切面類运授,并為其自動(dòng)生成代理烤惊。
看來不管怎樣簡化,老羅始終解決不了切面的配置吁朦,這件繁重的手工勞動(dòng)柒室。在 Spring 配置文件中,仍然會(huì)存在大量的切面配置逗宜。然而在有很多情況下 Spring AOP 所提供的切面類真的不太夠用了雄右,比如:想攔截指定注解的方法,我們就必須擴(kuò)展?DefaultPointcutAdvisor 類纺讲,自定義一個(gè)切面類擂仍,然后在 Spring 配置文件中進(jìn)行切面配置。不做不知道熬甚,做了您就知道相當(dāng)麻煩了逢渔。
老羅的解決方案似乎已經(jīng)掉進(jìn)了切面類的深淵,這還真是所謂的“面向切面編程”了乡括,最重要的是切面肃廓,最麻煩的也是切面冲簿。
必須要把切面配置給簡化掉,Spring 才能有所突破亿昏!?
神一樣的老羅總算認(rèn)識(shí)到了這一點(diǎn)峦剔,接受了網(wǎng)友們的建議,集成了 AspectJ角钩,同時(shí)也保留了以上提到的切面與代理配置方式(為了兼容老的項(xiàng)目吝沫,更為了維護(hù)自己的面子)。將 Spring 與?AspectJ 集成與直接使用 AspectJ 是不同的递礼,我們不需要定義?AspectJ 類(它是擴(kuò)展了 Java 語法的一種新的語言惨险,還需要特定的編譯器),只需要使用 AspectJ 切點(diǎn)表達(dá)式即可(它是比正則表達(dá)式更加友好的表現(xiàn)形式)脊髓。
12.?Spring + AspectJ(基于注解:通過 AspectJ execution 表達(dá)式攔截方法)
下面以一個(gè)最簡單的例子辫愉,實(shí)現(xiàn)之前提到的環(huán)繞增強(qiáng)。先定義一個(gè) Aspect 切面類:
@Aspect@Componentpublic?class?GreetingAspect?{@Around("execution(*?aop.demo.GreetingImpl.*(..))")????public?Object?around(ProceedingJoinPoint?pjp)?throws?Throwable?{????????before();????????Object?result?=?pjp.proceed();????????after();????????return?result;????}????private?void?before()?{????????System.out.println("Before");????}????private?void?after()?{????????System.out.println("After");????}}
注意:類上面標(biāo)注的?@Aspect 注解将硝,這表明該類是一個(gè) Aspect(其實(shí)就是?Advisor)恭朗。該類無需實(shí)現(xiàn)任何的接口,只需定義一個(gè)方法(方法叫什么名字都無所謂)依疼,只需在方法上標(biāo)注?@Around 注解痰腮,在注解中使用了 AspectJ 切點(diǎn)表達(dá)式。方法的參數(shù)中包括一個(gè)?ProceedingJoinPoint 對(duì)象律罢,它在 AOP 中稱為?Joinpoint(連接點(diǎn))膀值,可以通過該對(duì)象獲取方法的任何信息,例如:方法名误辑、參數(shù)等沧踏。
下面重點(diǎn)來分析一下這個(gè)切點(diǎn)表達(dá)式:
execution(* aop.demo.GreetingImpl.*(..))
execution():表示攔截方法,括號(hào)中可定義需要匹配的規(guī)則巾钉。
第一個(gè)“*”:表示方法的返回值是任意的翘狱。
第二個(gè)“*”:表示匹配該類中所有的方法。
(..):表示方法的參數(shù)是任意的睛琳。
是不是比正則表達(dá)式的可讀性更強(qiáng)呢盒蟆?如果想匹配指定的方法踏烙,只需將第二個(gè)“*”改為指定的方法名稱即可师骗。
如何配置呢?看看是有多簡單吧:
???????http://www.springframework.org/schema/beans/spring-beans.xsd
???????http://www.springframework.org/schema/context
???????http://www.springframework.org/schema/context/spring-context.xsd
???????http://www.springframework.org/schema/aop
???????http://www.springframework.org/schema/aop/spring-aop.xsd">
兩行配置就行了讨惩,不需要配置大量的代理辟癌,更不需要配置大量的切面,真是太棒了荐捻!需要注意的是?proxy-target-class="true" 屬性黍少,它的默認(rèn)值是 false寡夹,默認(rèn)只能代理接口(使用 JDK 動(dòng)態(tài)代理),當(dāng)為 true 時(shí)厂置,才能代理目標(biāo)類(使用 CGLib 動(dòng)態(tài)代理)菩掏。
Spring 與 AspectJ 結(jié)合的威力遠(yuǎn)遠(yuǎn)不止這些,我們來點(diǎn)時(shí)尚的吧昵济,攔截指定注解的方法怎么樣智绸?
13. Spring + AspectJ(基于注解:通過 AspectJ @annotation 表達(dá)式攔截方法)?
為了攔截指定的注解的方法,我們首先需要來自定義一個(gè)注解:
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public@interfaceTag?{}
以上定義了一個(gè) @Tag 注解访忿,此注解可標(biāo)注在方法上瞧栗,在運(yùn)行時(shí)生效。
只需將前面的 Aspect 類的切點(diǎn)表達(dá)式稍作改動(dòng):
@Aspect@Componentpublic?class?GreetingAspect?{@Around("@annotation(aop.demo.Tag)")????public?Object?around(ProceedingJoinPoint?pjp)?throws?Throwable?{????????...????}????...}
這次使用了?@annotation() 表達(dá)式海铆,只需在括號(hào)內(nèi)定義需要攔截的注解名稱即可迹恐。
直接將 @Tag 注解定義在您想要攔截的方法上,就這么簡單:
@Componentpublic?class?GreetingImpl?implements?Greeting?{@Tag@Overridepublic?void?sayHello(String?name)?{????????System.out.println("Hello!?"+?name);????}}
以上示例中只有一個(gè)方法卧斟,如果有多個(gè)方法殴边,我們只想攔截其中某些時(shí),這種解決方案會(huì)更加有價(jià)值珍语。
除了 @Around 注解外找都,其實(shí)還有幾個(gè)相關(guān)的注解,稍微歸納一下吧:
@Before:前置增強(qiáng)
@After:后置增強(qiáng)
@Around:環(huán)繞增強(qiáng)
@AfterThrowing:拋出增強(qiáng)
@DeclareParents:引入增強(qiáng)
此外還有一個(gè) @AfterReturning(返回后增強(qiáng))廊酣,也可理解為 Finally 增強(qiáng)能耻,相當(dāng)于 finally 語句,它是在方法結(jié)束后執(zhí)行的亡驰,也就說說晓猛,它比 @After 還要晚一些。
最后一個(gè) @DeclareParents 竟然就是引入增強(qiáng)凡辱!為什么不叫做 @Introduction 呢戒职?我也不知道為什么,但它干的活就是引入增強(qiáng)透乾。
14. Spring + AspectJ(引入增強(qiáng))
為了實(shí)現(xiàn)基于 AspectJ 的引入增強(qiáng)洪燥,我們同樣需要定義一個(gè) Aspect 類:
@Aspect@Componentpublic?class?GreetingAspect?{@DeclareParents(value?="aop.demo.GreetingImpl",?defaultImpl?=?ApologyImpl.class)????private?Apology?apology;}
只需要在 Aspect 類中定義一個(gè)需要引入增強(qiáng)的接口,它也就是運(yùn)行時(shí)需要?jiǎng)討B(tài)實(shí)現(xiàn)的接口乳乌。在這個(gè)接口上標(biāo)注了?@DeclareParents 注解捧韵,該注解有兩個(gè)屬性:
value:目標(biāo)類
defaultImpl:引入接口的默認(rèn)實(shí)現(xiàn)類
我們只需要對(duì)引入的接口提供一個(gè)默認(rèn)實(shí)現(xiàn)類即可完成引入增強(qiáng):
publicclass?ApologyImpl?implements?Apology?{@Overridepublic?void?saySorry(String?name)?{????????System.out.println("Sorry!?"+?name);????}}
以上這個(gè)實(shí)現(xiàn)會(huì)在運(yùn)行時(shí)自動(dòng)增強(qiáng)到 GreetingImpl 類中,也就是說汉操,無需修改?GreetingImpl 類的代碼再来,讓它去實(shí)現(xiàn)Apology 接口,我們單獨(dú)為該接口提供一個(gè)實(shí)現(xiàn)類(ApologyImpl),來做?GreetingImpl?想做的事情芒篷。
還是用一個(gè)客戶端來嘗試一下吧:
publicclass?Client?{public?static?void?main(String[]?args)?{????????ApplicationContext?context?=newClassPathXmlApplicationContext("aop/demo/spring.xml");????????Greeting?greeting?=?(Greeting)?context.getBean("greetingImpl");????????greeting.sayHello("Jack");????????Apology?apology?=?(Apology)?greeting;//?強(qiáng)制轉(zhuǎn)型為?Apology?接口apology.saySorry("Jack");????}}
從 Spring ApplicationContext 中獲取?greetingImpl 對(duì)象(其實(shí)是個(gè)代理對(duì)象)搜变,可轉(zhuǎn)型為自己靜態(tài)實(shí)現(xiàn)的接口?Greeting,也可轉(zhuǎn)型為自己動(dòng)態(tài)實(shí)現(xiàn)的接口?Apology针炉,切換起來非常方便挠他。
使用 AspectJ 的引入增強(qiáng)比原來的 Spring AOP 的引入增強(qiáng)更加方便了,而且還可面向接口編程(以前只能面向?qū)崿F(xiàn)類)篡帕,這也算一個(gè)非常巨大的突破绩社。
這一切真的已經(jīng)非常強(qiáng)大也非常靈活了!但仍然還是有用戶不能嘗試這些特性赂苗,因?yàn)樗麄冞€在使用 JDK 1.4(根本就沒有注解這個(gè)東西)愉耙,怎么辦呢?沒想到Spring AOP 為那些遺留系統(tǒng)也考慮到了拌滋。
15.?Spring + AspectJ(基于配置)
除了使用 @Aspect 注解來定義切面類以外朴沿,Spring AOP 也提供了基于配置的方式來定義切面類:
使用 元素來進(jìn)行 AOP 配置,在其子元素中配置切面败砂,包括增強(qiáng)類型赌渣、目標(biāo)方法、切點(diǎn)等信息昌犹。
無論您是不能使用注解坚芜,還是不愿意使用注解,Spring AOP 都能為您提供全方位的服務(wù)斜姥。
好了鸿竖,我所知道的比較實(shí)用的 AOP 技術(shù)都在這里了,當(dāng)然還有一些更為高級(jí)的特性铸敏,由于個(gè)人精力有限缚忧,這里就不再深入了。
還是依照慣例杈笔,給一張牛逼的高清無碼思維導(dǎo)圖闪水,總結(jié)一下以上各個(gè)知識(shí)點(diǎn):
再來一張表格,總結(jié)一下各類增強(qiáng)類型所對(duì)應(yīng)的解決方案:
增強(qiáng)類型基于 AOP 接口基于 @Aspect基于?
Before Advice(前置增強(qiáng))
MethodBeforeAdvice
@Before
AfterAdvice(后置增強(qiáng))
AfterReturningAdvice
@After
AroundAdvice(環(huán)繞增強(qiáng))
MethodInterceptor
@Around
ThrowsAdvice(拋出增強(qiáng)
ThrowsAdvice
@AfterThrowing
IntroductionAdvice(引入增強(qiáng))
DelegatingIntroductionInterceptor
@DeclareParents
最后給一張 UML 類圖描述一下 Spring AOP 的整體架構(gòu):
個(gè)人公號(hào):【排骨肉段】蒙具,可以關(guān)注一下球榆。