在上篇中,我們從寫死代碼,到使用代理;從編程式 Spring AOP 到聲明式 Spring AOP。一切都朝着簡單實用主義的方向在發展。沿着 Spring AOP 的方向,Rod Johnson(老羅)花了不少心思,都是爲了讓我們使用 Spring 框架時不會感受到麻煩,但事實卻並非如此。那麼,後來老羅究竟對 Spring AOP 做了哪些改進呢?
現在繼續!
9. Spring AOP:切面
之前談到的 AOP 框架其實可以將它理解爲一個攔截器框架,但這個攔截器似乎非常武斷。比如說,如果它攔截了一個類,那麼它就攔截了這個類中所有的方法。類似地,當我們在使用動態代理的時候,其實也遇到了這個問題。需要在代碼中對所攔截的方法名加以判斷,才能過濾出我們需要攔截的方法,想想這種做法確實不太優雅。在大量的真實項目中,似乎我們只需要攔截特定的方法就行了,沒必要攔截所有的方法。於是,老羅同志藉助了 AOP 的一個很重要的工具,Advisor(切面),來解決這個問題。它也是
AOP 中的核心!是我們關注的重點!
也就是說,我們可以通過切面,將增強類與攔截匹配條件組合在一起,然後將這個切面配置到 ProxyFactory 中,從而生成代理。
這裏提到這個“攔截匹配條件”在 AOP 中就叫做 Pointcut(切點),其實說白了就是一個基於表達式的攔截條件罷了。
歸納一下,Advisor(切面)封裝了 Advice(增強)與 Pointcut(切點 )。當您理解了這句話後,就往下看吧。
我在 GreetingImpl 類中故意增加了兩個方法,都以“good”開頭。下面要做的就是攔截這兩個新增的方法,而對 sayHello() 方法不作攔截。
02 |
public class GreetingImpl implements Greeting
{ |
05 |
public void sayHello(String
name) { |
06 |
System.out.println( "Hello!
" +
name); |
09 |
public void goodMorning(String
name) { |
10 |
System.out.println( "Good
Morning! " +
name); |
13 |
public void goodNight(String
name) { |
14 |
System.out.println( "Good
Night! " +
name); |
在 Spring AOP 中,老羅已經給我們提供了許多切面類了,這些切面類我個人感覺最好用的就是基於正則表達式的切面類。看看您就明白了:
01 |
<? xml version = "1.0" encoding = "UTF-8" ?> |
04 |
< context:component-scan base-package = "aop.demo" /> |
07 |
< bean id = "greetingAdvisor" class = "org.springframework.aop.support.RegexpMethodPointcutAdvisor" > |
08 |
< property name = "advice" ref = "greetingAroundAdvice" /> |
09 |
< property name = "pattern" value = "aop.demo.GreetingImpl.good.*" /> |
13 |
< bean id = "greetingProxy" class = "org.springframework.aop.framework.ProxyFactoryBean" > |
14 |
< property name = "target" ref = "greetingImpl" /> |
15 |
< property name = "interceptorNames" value = "greetingAdvisor" /> |
16 |
< property name = "proxyTargetClass" value = "true" /> |
注意以上代理對象的配置中的 interceptorNames,它不再是一個增強,而是一個切面,因爲已經將增強封裝到該切面中了。此外,切面還定義了一個切點(正則表達式),其目的是爲了只將滿足切點匹配條件的方法進行攔截。
需要強調的是,這裏的切點表達式是基於正則表達式的。示例中的“aop.demo.GreetingImpl.good.*”表達式後面的“.*”表示匹配所有字符,翻譯過來就是“匹配 aop.demo.GreetingImpl 類中以 good 開頭的方法”。
除了 RegexpMethodPointcutAdvisor 以外,在 Spring AOP 中還提供了幾個切面類,比如:
- DefaultPointcutAdvisor:默認切面(可擴展它來自定義切面)
- NameMatchMethodPointcutAdvisor:根據方法名稱進行匹配的切面
- StaticMethodMatcherPointcutAdvisor:用於匹配靜態方法的切面
總的來說,讓用戶去配置一個或少數幾個代理,似乎還可以接受,但隨着項目的擴大,代理配置就會越來越多,配置的重複勞動就多了,麻煩不說,還很容易出錯。能否讓 Spring 框架爲我們自動生成代理呢?
10. Spring AOP:自動代理(掃描 Bean 名稱)
Spring AOP 提供了一個可根據 Bean 名稱來自動生成代理的工具,它就是 BeanNameAutoProxyCreator。是這樣配置的:
01 |
<? xml version = "1.0" encoding = "UTF-8" ?> |
06 |
< bean class = "org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator" > |
07 |
< property name = "beanNames" value = "*Impl" /> |
08 |
< property name = "interceptorNames" value = "greetingAroundAdvice" /> |
09 |
< property name = "optimize" value = "true" /> |
以上使用 BeanNameAutoProxyCreator 只爲後綴爲“Impl”的 Bean 生成代理。需要注意的是,這個地方我們不能定義代理接口,也就是 interfaces 屬性,因爲我們根本就不知道這些 Bean 到底實現了多少接口。此時不能代理接口,而只能代理類。所以這裏提供了一個新的配置項,它就是 optimize。若爲 true 時,則可對代理生成策略進行優化(默認是
false 的)。也就是說,如果該類有接口,就代理接口(使用 JDK 動態代理);如果沒有接口,就代理類(使用 CGLib 動態代理)。而並非像之前使用的 proxyTargetClass 屬性那樣,強制代理類,而不考慮代理接口的方式。可見 Spring AOP 確實爲我們提供了很多很好地服務!
既然 CGLib 可以代理任何的類了,那爲什麼還要用 JDK 的動態代理呢?肯定您會這樣問。
根據多年來實際項目經驗得知:CGLib 創建代理的速度比較慢,但創建代理後運行的速度卻非常快,而 JDK 動態代理正好相反。如果在運行的時候不斷地用 CGLib 去創建代理,系統的性能會大打折扣,所以建議一般在系統初始化的時候用 CGLib 去創建代理,並放入 Spring 的 ApplicationContext 中以備後用。
以上這個例子只能匹配目標類,而不能進一步匹配其中指定的方法,要匹配方法,就要考慮使用切面與切點了。Spring AOP 基於切面也提供了一個自動代理生成器:DefaultAdvisorAutoProxyCreator。
11. Spring AOP:自動代理(掃描切面配置)
爲了匹配目標類中的指定方法,我們仍然需要在 Spring 中配置切面與切點:
01 |
<? xml version = "1.0" encoding = "UTF-8" ?> |
06 |
< bean id = "greetingAdvisor" class = "org.springframework.aop.support.RegexpMethodPointcutAdvisor" > |
07 |
< property name = "pattern" value = "aop.demo.GreetingImpl.good.*" /> |
08 |
< property name = "advice" ref = "greetingAroundAdvice" /> |
11 |
< bean class = "org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" > |
12 |
< property name = "optimize" value = "true" /> |
這裏無需再配置代理了,因爲代理將會由 DefaultAdvisorAutoProxyCreator 自動生成。也就是說,這個類可以掃描所有的切面類,併爲其自動生成代理。
看來不管怎樣簡化,老羅始終解決不了切面的配置,這件繁重的手工勞動。在 Spring 配置文件中,仍然會存在大量的切面配置。然而在有很多情況下 Spring AOP 所提供的切面類真的不太夠用了,比如:想攔截指定註解的方法,我們就必須擴展 DefaultPointcutAdvisor 類,自定義一個切面類,然後在 Spring 配置文件中進行切面配置。不做不知道,做了您就知道相當麻煩了。
老羅的解決方案似乎已經掉進了切面類的深淵,這還真是所謂的“面向切面編程”了,最重要的是切面,最麻煩的也是切面。
必須要把切面配置給簡化掉,Spring 纔能有所突破!
神一樣的老羅總算認識到了這一點,接受了網友們的建議,集成了 AspectJ,同時也保留了以上提到的切面與代理配置方式(爲了兼容老的項目,更爲了維護自己的面子)。將 Spring 與 AspectJ 集成與直接使用 AspectJ 是不同的,我們不需要定義 AspectJ 類(它是擴展了 Java 語法的一種新的語言,還需要特定的編譯器),只需要使用 AspectJ 切點表達式即可(它是比正則表達式更加友好的表現形式)。
12. Spring + AspectJ(基於註解:通過 AspectJ execution
表達式攔截方法)
下面以一個最簡單的例子,實現之前提到的環繞增強。先定義一個 Aspect 切面類:
03 |
public class GreetingAspect
{ |
05 |
@Around ( "execution(*
aop.demo.GreetingImpl.*(..))" ) |
06 |
public Object
around(ProceedingJoinPoint pjp) throws Throwable
{ |
08 |
Object
result = pjp.proceed(); |
13 |
private void before()
{ |
14 |
System.out.println( "Before" ); |
17 |
private void after()
{ |
18 |
System.out.println( "After" ); |
注意:類上面標註的 @Aspect 註解,這表明該類是一個 Aspect(其實就是 Advisor)。該類無需實現任何的接口,只需定義一個方法(方法叫什麼名字都無所謂),只需在方法上標註 @Around 註解,在註解中使用了 AspectJ 切點表達式。方法的參數中包括一個 ProceedingJoinPoint 對象,它在 AOP 中稱爲Joinpoint(連接點),可以通過該對象獲取方法的任何信息,例如:方法名、參數等。
下面重點來分析一下這個切點表達式:
execution(* aop.demo.GreetingImpl.*(..))
- execution():表示攔截方法,括號中可定義需要匹配的規則。
- 第一個“*”:表示方法的返回值是任意的。
- 第二個“*”:表示匹配該類中所有的方法。
- (..):表示方法的參數是任意的。
是不是比正則表達式的可讀性更強呢?如果想匹配指定的方法,只需將第二個“*”改爲指定的方法名稱即可。
如何配置呢?看看是有多簡單吧:
01 |
<?xml
version= "1.0" encoding= "UTF-8" ?> |
02 |
<beans
xmlns= "http://www.springframework.org/schema/beans" |
03 |
xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance" |
04 |
xmlns:context= "http://www.springframework.org/schema/context" |
05 |
xmlns:aop= "http://www.springframework.org/schema/aop" |
06 |
xsi:schemaLocation="http: |
13 |
<context:component-scan
base- package = "aop.demo" /> |
15 |
<aop:aspectj-autoproxy
proxy-target- class = "true" /> |
兩行配置就行了,不需要配置大量的代理,更不需要配置大量的切面,真是太棒了!需要注意的是 proxy-target-class="true" 屬性,它的默認值是 false,默認只能代理接口(使用 JDK 動態代理),當爲 true 時,才能代理目標類(使用 CGLib 動態代理)。
Spring 與 AspectJ 結合的威力遠遠不止這些,我們來點時尚的吧,攔截指定註解的方法怎麼樣?
13. Spring + AspectJ(基於註解:通過 AspectJ @annotation 表達式攔截方法)
爲了攔截指定的註解的方法,我們首先需要來自定義一個註解:
1 |
@Target (ElementType.METHOD) |
2 |
@Retention (RetentionPolicy.RUNTIME) |
3 |
public @interface Tag
{ |
以上定義了一個 @Tag 註解,此註解可標註在方法上,在運行時生效。
只需將前面的 Aspect 類的切點表達式稍作改動:
03 |
public class GreetingAspect
{ |
05 |
@Around ( "@annotation(aop.demo.Tag)" ) |
06 |
public Object
around(ProceedingJoinPoint pjp) throws Throwable
{ |
這次使用了 @annotation() 表達式,只需在括號內定義需要攔截的註解名稱即可。
直接將 @Tag 註解定義在您想要攔截的方法上,就這麼簡單:
2 |
public class GreetingImpl implements Greeting
{ |
6 |
public void sayHello(String
name) { |
7 |
System.out.println( "Hello!
" +
name); |
以上示例中只有一個方法,如果有多個方法,我們只想攔截其中某些時,這種解決方案會更加有價值。
除了 @Around 註解外,其實還有幾個相關的註解,稍微歸納一下吧:
- @Before:前置增強
- @After:後置增強
- @Around:環繞增強
- @AfterThrowing:拋出增強
- @DeclareParents:引入增強
此外還有一個 @AfterReturning(返回後增強),也可理解爲 Finally 增強,相當於 finally 語句,它是在方法結束後執行的,也就說說,它比 @After 還要晚一些。
最後一個 @DeclareParents 竟然就是引入增強!爲什麼不叫做 @Introduction 呢?我也不知道爲什麼,但它乾的活就是引入增強。
14. Spring + AspectJ(引入增強)
爲了實現基於 AspectJ 的引入增強,我們同樣需要定義一個 Aspect 類:
3 |
public class GreetingAspect
{ |
5 |
@DeclareParents (value
= "aop.demo.GreetingImpl" ,
defaultImpl = ApologyImpl. class ) |
6 |
private Apology
apology; |
只需要在 Aspect 類中定義一個需要引入增強的接口,它也就是運行時需要動態實現的接口。在這個接口上標註了 @DeclareParents 註解,該註解有兩個屬性:
- value:目標類
- defaultImpl:引入接口的默認實現類
我們只需要對引入的接口提供一個默認實現類即可完成引入增強:
1 |
public class ApologyImpl implements Apology
{ |
4 |
public void saySorry(String
name) { |
5 |
System.out.println( "Sorry!
" +
name); |
以上這個實現會在運行時自動增強到 GreetingImpl 類中,也就是說,無需修改 GreetingImpl 類的代碼,讓它去實現 Apology 接口,我們單獨爲該接口提供一個實現類(ApologyImpl),來做 GreetingImpl 想做的事情。
還是用一個客戶端來嘗試一下吧:
03 |
public static void main(String[]
args) { |
04 |
ApplicationContext
context = new ClassPathXmlApplicationContext( "aop/demo/spring.xml" ); |
05 |
Greeting
greeting = (Greeting) context.getBean( "greetingImpl" ); |
06 |
greeting.sayHello( "Jack" ); |
08 |
Apology
apology = (Apology) greeting; |
09 |
apology.saySorry( "Jack" ); |
從 Spring ApplicationContext 中獲取 greetingImpl 對象(其實是個代理對象),可轉型爲自己靜態實現的接口 Greeting,也可轉型爲自己動態實現的接口 Apology,切換起來非常方便。
使用 AspectJ 的引入增強比原來的 Spring AOP 的引入增強更加方便了,而且還可面向接口編程(以前只能面向實現類),這也算一個非常巨大的突破。
這一切真的已經非常強大也非常靈活了!但仍然還是有用戶不能嘗試這些特性,因爲他們還在使用 JDK 1.4(根本就沒有註解這個東西),怎麼辦呢?沒想到 Spring AOP 爲那些遺留系統也考慮到了。
15. Spring + AspectJ(基於配置)
除了使用 @Aspect 註解來定義切面類以外,Spring AOP 也提供了基於配置的方式來定義切面類:
01 |
<? xml version = "1.0" encoding = "UTF-8" ?> |
04 |
< bean id = "greetingImpl" class = "aop.demo.GreetingImpl" /> |
06 |
< bean id = "greetingAspect" class = "aop.demo.GreetingAspect" /> |
09 |
< aop:aspect ref = "greetingAspect" > |
10 |
< aop:around method = "around" pointcut = "execution(*
aop.demo.GreetingImpl.*(..))" /> |
使用 <aop:config> 元素來進行 AOP 配置,在其子元素中配置切面,包括增強類型、目標方法、切點等信息。
無論您是不能使用註解,還是不願意使用註解,Spring AOP 都能爲您提供全方位的服務。
好了,我所知道的比較實用的 AOP 技術都在這裏了,當然還有一些更爲高級的特性,由於個人精力有限,這裏就不再深入了。
還是依照慣例,給一張牛逼的高清無碼思維導圖,總結一下以上各個知識點:
再來一張表格,總結一下各類增強類型所對應的解決方案:
增強類型 |
基於 AOP 接口 |
基於 @Aspect |
基於 <aop:config> |
Before Advice(前置增強)
|
MethodBeforeAdvice
|
@Before
|
<aop:before>
|
AfterAdvice(後置增強)
|
AfterReturningAdvice
|
@After
|
<aop:after>
|
AroundAdvice(環繞增強)
|
MethodInterceptor
|
@Around
|
<aop:around>
|
ThrowsAdvice(拋出增強
|
ThrowsAdvice
|
@AfterThrowing
|
<aop:after-throwing>
|
IntroductionAdvice(引入增強)
|
DelegatingIntroductionInterceptor
|
@DeclareParents
|
<aop:declare-parents>
|
最後給一張 UML 類圖描述一下 Spring AOP 的整體架構:
原文出自http://my.oschina.net/huangyong/blog/161338?from=20130922