AOP 那些事兒 續(xù)

本文轉自?黃勇

本文是《AOP 那點事兒》的續(xù)集。

在上篇中,我們從寫死代碼,到使用代理;從編程式?Spring AOP 到聲明式 Spring AOP。一切都朝著簡單實用主義的方向在發(fā)展。沿著 Spring AOP 的方向,Rod Johnson(老羅)花了不少心思,都是為了讓我們使用 Spring 框架時不會感受到麻煩,但事實卻并非如此。那么,后來老羅究竟對 Spring AOP 做了哪些改進呢?

現(xiàn)在繼續(xù)!

9.?Spring AOP:切面

之前談到的 AOP 框架其實可以將它理解為一個攔截器框架,但這個攔截器似乎非常武斷。比如說,如果它攔截了一個類,那么它就攔截了這個類中所有的方法。類似地,當我們在使用動態(tài)代理的時候,其實也遇到了這個問題。需要在代碼中對所攔截的方法名加以判斷,才能過濾出我們需要攔截的方法,想想這種做法確實不太優(yōu)雅。在大量的真實項目中,似乎我們只需要攔截特定的方法就行了,沒必要攔截所有的方法。于是,老羅同志借助了 AOP 的一個很重要的工具,Advisor(切面),來解決這個問題。它也是 AOP 中的核心!是我們關注的重點!

也就是說,我們可以通過切面,將增強類與攔截匹配條件組合在一起,然后將這個切面配置到 ProxyFactory 中,從而生成代理。

這里提到這個“攔截匹配條件”在 AOP 中就叫做?Pointcut(切點),其實說白了就是一個基于表達式的攔截條件罷了。

歸納一下,Advisor(切面)封裝了 Advice(增強)與 Pointcut(切點?)。當您理解了這句話后,就往下看吧。

我在 GreetingImpl 類中故意增加了兩個方法,都以“good”開頭。下面要做的就是攔截這兩個新增的方法,而對 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)給我們提供了許多切面類了,這些切面類我個人感覺最好用的就是基于正則表達式的切面類??纯茨兔靼琢耍?/p>

注意以上代理對象的配置中的?interceptorNames,它不再是一個增強,而是一個切面,因為已經(jīng)將增強封裝到該切面中了。此外,切面還定義了一個切點(正則表達式),其目的是為了只將滿足切點匹配條件的方法進行攔截。

需要強調的是,這里的切點表達式是基于正則表達式的。示例中的“aop.demo.GreetingImpl.good.*”表達式后面的“.*”表示匹配所有字符,翻譯過來就是“匹配 aop.demo.GreetingImpl 類中以 good 開頭的方法”。

除了?RegexpMethodPointcutAdvisor 以外,在 Spring AOP 中還提供了幾個切面類,比如:

DefaultPointcutAdvisor:默認切面(可擴展它來自定義切面)

NameMatchMethodPointcutAdvisor:根據(jù)方法名稱進行匹配的切面

StaticMethodMatcherPointcutAdvisor:用于匹配靜態(tài)方法的切面

總的來說,讓用戶去配置一個或少數(shù)幾個代理,似乎還可以接受,但隨著項目的擴大,代理配置就會越來越多,配置的重復勞動就多了,麻煩不說,還很容易出錯。能否讓 Spring 框架為我們自動生成代理呢?

10.?Spring AOP:自動代理(掃描 Bean 名稱)

Spring AOP 提供了一個可根據(jù) Bean 名稱來自動生成代理的工具,它就是?BeanNameAutoProxyCreator。是這樣配置的:

...

以上使用 BeanNameAutoProxyCreator 只為后綴為“Impl”的 Bean 生成代理。需要注意的是,這個地方我們不能定義代理接口,也就是?interfaces 屬性,因為我們根本就不知道這些 Bean 到底實現(xiàn)了多少接口。此時不能代理接口,而只能代理類。所以這里提供了一個新的配置項,它就是?optimize。若為 true 時,則可對代理生成策略進行優(yōu)化(默認是 false 的)。也就是說,如果該類有接口,就代理接口(使用 JDK 動態(tài)代理);如果沒有接口,就代理類(使用 CGLib 動態(tài)代理)。而并非像之前使用的?proxyTargetClass 屬性那樣,強制代理類,而不考慮代理接口的方式??梢?Spring AOP 確實為我們提供了很多很好地服務!

既然 CGLib 可以代理任何的類了,那為什么還要用 JDK 的動態(tài)代理呢?肯定您會這樣問。

根據(jù)多年來實際項目經(jīng)驗得知:CGLib 創(chuàng)建代理的速度比較慢,但創(chuàng)建代理后運行的速度卻非常快,而 JDK 動態(tài)代理正好相反。如果在運行的時候不斷地用 CGLib 去創(chuàng)建代理,系統(tǒng)的性能會大打折扣,所以建議一般在系統(tǒng)初始化的時候用 CGLib 去創(chuàng)建代理,并放入 Spring 的 ApplicationContext?中以備后用。

以上這個例子只能匹配目標類,而不能進一步匹配其中指定的方法,要匹配方法,就要考慮使用切面與切點了。Spring AOP 基于切面也提供了一個自動代理生成器:DefaultAdvisorAutoProxyCreator。

11.?Spring AOP:自動代理(掃描切面配置)

為了匹配目標類中的指定方法,我們仍然需要在 Spring 中配置切面與切點:

...

這里無需再配置代理了,因為代理將會由?DefaultAdvisorAutoProxyCreator 自動生成。也就是說,這個類可以掃描所有的切面類,并為其自動生成代理。

看來不管怎樣簡化,老羅始終解決不了切面的配置,這件繁重的手工勞動。在 Spring 配置文件中,仍然會存在大量的切面配置。然而在有很多情況下 Spring AOP 所提供的切面類真的不太夠用了,比如:想攔截指定注解的方法,我們就必須擴展?DefaultPointcutAdvisor 類,自定義一個切面類,然后在 Spring 配置文件中進行切面配置。不做不知道,做了您就知道相當麻煩了。

老羅的解決方案似乎已經(jīng)掉進了切面類的深淵,這還真是所謂的“面向切面編程”了,最重要的是切面,最麻煩的也是切面。

必須要把切面配置給簡化掉,Spring 才能有所突破!?

神一樣的老羅總算認識到了這一點,接受了網(wǎng)友們的建議,集成了 AspectJ,同時也保留了以上提到的切面與代理配置方式(為了兼容老的項目,更為了維護自己的面子)。將 Spring 與?AspectJ 集成與直接使用 AspectJ 是不同的,我們不需要定義?AspectJ 類(它是擴展了 Java 語法的一種新的語言,還需要特定的編譯器),只需要使用 AspectJ 切點表達式即可(它是比正則表達式更加友好的表現(xiàn)形式)。

12.?Spring + AspectJ(基于注解:通過 AspectJ execution 表達式攔截方法)

下面以一個最簡單的例子,實現(xiàn)之前提到的環(huán)繞增強。先定義一個 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");????}}

注意:類上面標注的?@Aspect 注解,這表明該類是一個 Aspect(其實就是?Advisor)。該類無需實現(xiàn)任何的接口,只需定義一個方法(方法叫什么名字都無所謂),只需在方法上標注?@Around 注解,在注解中使用了 AspectJ 切點表達式。方法的參數(shù)中包括一個?ProceedingJoinPoint 對象,它在 AOP 中稱為?Joinpoint(連接點),可以通過該對象獲取方法的任何信息,例如:方法名、參數(shù)等。

下面重點來分析一下這個切點表達式:

execution(* aop.demo.GreetingImpl.*(..))

execution():表示攔截方法,括號中可定義需要匹配的規(guī)則。

第一個“*”:表示方法的返回值是任意的。

第二個“*”:表示匹配該類中所有的方法。

(..):表示方法的參數(shù)是任意的。

是不是比正則表達式的可讀性更強呢?如果想匹配指定的方法,只需將第二個“*”改為指定的方法名稱即可。

如何配置呢?看看是有多簡單吧:


???????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" 屬性,它的默認值是 false,默認只能代理接口(使用 JDK 動態(tài)代理),當為 true 時,才能代理目標類(使用 CGLib 動態(tài)代理)。

Spring 與 AspectJ 結合的威力遠遠不止這些,我們來點時尚的吧,攔截指定注解的方法怎么樣?

13. Spring + AspectJ(基于注解:通過 AspectJ @annotation 表達式攔截方法)?

為了攔截指定的注解的方法,我們首先需要來自定義一個注解:

@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public@interfaceTag?{}

以上定義了一個 @Tag 注解,此注解可標注在方法上,在運行時生效。

只需將前面的 Aspect 類的切點表達式稍作改動:

@Aspect@Componentpublic?class?GreetingAspect?{@Around("@annotation(aop.demo.Tag)")????public?Object?around(ProceedingJoinPoint?pjp)?throws?Throwable?{????????...????}????...}

這次使用了?@annotation() 表達式,只需在括號內定義需要攔截的注解名稱即可。

直接將 @Tag 注解定義在您想要攔截的方法上,就這么簡單:

@Componentpublic?class?GreetingImpl?implements?Greeting?{@Tag@Overridepublic?void?sayHello(String?name)?{????????System.out.println("Hello!?"+?name);????}}

以上示例中只有一個方法,如果有多個方法,我們只想攔截其中某些時,這種解決方案會更加有價值。

除了 @Around 注解外,其實還有幾個相關的注解,稍微歸納一下吧:

@Before:前置增強

@After:后置增強

@Around:環(huán)繞增強

@AfterThrowing:拋出增強

@DeclareParents:引入增強

此外還有一個 @AfterReturning(返回后增強),也可理解為 Finally 增強,相當于 finally 語句,它是在方法結束后執(zhí)行的,也就說說,它比 @After 還要晚一些。

最后一個 @DeclareParents 竟然就是引入增強!為什么不叫做 @Introduction 呢?我也不知道為什么,但它干的活就是引入增強。

14. Spring + AspectJ(引入增強)

為了實現(xiàn)基于 AspectJ 的引入增強,我們同樣需要定義一個 Aspect 類:

@Aspect@Componentpublic?class?GreetingAspect?{@DeclareParents(value?="aop.demo.GreetingImpl",?defaultImpl?=?ApologyImpl.class)????private?Apology?apology;}

只需要在 Aspect 類中定義一個需要引入增強的接口,它也就是運行時需要動態(tài)實現(xiàn)的接口。在這個接口上標注了?@DeclareParents 注解,該注解有兩個屬性:

value:目標類

defaultImpl:引入接口的默認實現(xiàn)類

我們只需要對引入的接口提供一個默認實現(xiàn)類即可完成引入增強:

publicclass?ApologyImpl?implements?Apology?{@Overridepublic?void?saySorry(String?name)?{????????System.out.println("Sorry!?"+?name);????}}

以上這個實現(xiàn)會在運行時自動增強到 GreetingImpl 類中,也就是說,無需修改?GreetingImpl 類的代碼,讓它去實現(xiàn)Apology 接口,我們單獨為該接口提供一個實現(xiàn)類(ApologyImpl),來做?GreetingImpl?想做的事情。

還是用一個客戶端來嘗試一下吧:

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;//?強制轉型為?Apology?接口apology.saySorry("Jack");????}}

從 Spring ApplicationContext 中獲取?greetingImpl 對象(其實是個代理對象),可轉型為自己靜態(tài)實現(xiàn)的接口?Greeting,也可轉型為自己動態(tài)實現(xiàn)的接口?Apology,切換起來非常方便。

使用 AspectJ 的引入增強比原來的 Spring AOP 的引入增強更加方便了,而且還可面向接口編程(以前只能面向實現(xiàn)類),這也算一個非常巨大的突破。

這一切真的已經(jīng)非常強大也非常靈活了!但仍然還是有用戶不能嘗試這些特性,因為他們還在使用 JDK 1.4(根本就沒有注解這個東西),怎么辦呢?沒想到Spring AOP 為那些遺留系統(tǒng)也考慮到了。

15.?Spring + AspectJ(基于配置)

除了使用 @Aspect 注解來定義切面類以外,Spring AOP 也提供了基于配置的方式來定義切面類:

使用 元素來進行 AOP 配置,在其子元素中配置切面,包括增強類型、目標方法、切點等信息。

無論您是不能使用注解,還是不愿意使用注解,Spring AOP 都能為您提供全方位的服務。

好了,我所知道的比較實用的 AOP 技術都在這里了,當然還有一些更為高級的特性,由于個人精力有限,這里就不再深入了。

還是依照慣例,給一張牛逼的高清無碼思維導圖,總結一下以上各個知識點:


再來一張表格,總結一下各類增強類型所對應的解決方案:

增強類型基于 AOP 接口基于 @Aspect基于?

Before Advice(前置增強)

MethodBeforeAdvice

@Before

AfterAdvice(后置增強)

AfterReturningAdvice

@After

AroundAdvice(環(huán)繞增強)

MethodInterceptor

@Around

ThrowsAdvice(拋出增強

ThrowsAdvice

@AfterThrowing

IntroductionAdvice(引入增強)

DelegatingIntroductionInterceptor

@DeclareParents

最后給一張 UML 類圖描述一下 Spring AOP 的整體架構:




個人公號:【排骨肉段】,可以關注一下。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,694評論 19 139
  • 本章內容: 面向切面編程的基本原理 通過POJO創(chuàng)建切面 使用@AspectJ注解 為AspectJ切面注入依賴 ...
    謝隨安閱讀 3,436評論 0 9
  • **** AOP 面向切面編程 底層原理 代理?。?! 今天AOP課程1、 Spring 傳統(tǒng) AOP2、 Spri...
    luweicheng24閱讀 1,517評論 0 1
  • 時勢造就英雄,成長背景影響人的價值觀,這是很難逾越的。 比如老子仙人,大約生在春秋,經(jīng)歷“繁華皆浮云”的階段,整個...
    UNCLE黑牙閱讀 483評論 1 4
  • 二十歲之前,總覺得那些一直說自己【永遠十八】的老女人是神經(jīng)病。 滿口嚷著我不怕老去,那是因為你還年輕。 一晃自己也...
    德齡與帛桉閱讀 657評論 0 2

友情鏈接更多精彩內容