?
? ????? PHP ??? ???? ??? ?? ??
@AspectJ使用了Java 5的注解,可以將切面聲明為普通的Java類。@AspectJ樣式在AspectJ 5發(fā)布的AspectJ project部分中被引入。Spring 2.0使用了和AspectJ 5一樣的注解,并使用AspectJ來做切入點解析和匹配。但是,AOP在運行時仍舊是純的Spring AOP,并不依賴于AspectJ的編譯器或者織入器(weaver)。
使用AspectJ的編譯器或者織入器的話就可以使用完整的AspectJ語言,我們將在第?6.8?節(jié) “在Spring應(yīng)用中使用AspectJ”中討論這個問題。
為了在Spring配置中使用@AspectJ切面,你首先必須啟用Spring對@AspectJ切面配置的支持,并確保自動代理(autoproxying)的bean是否能被這些切面通知。自動代理是指Spring會判斷一個bean是否使用了一個或多個切面通知,并據(jù)此自動生成相應(yīng)的代理以攔截其方法調(diào)用,并且確保通知在需要時執(zhí)行。
通過在你的Spring的配置中引入下列元素來啟用Spring對@AspectJ的支持:
<aop:aspectj-autoproxy/>
我們假定你正在使用附錄?A, XML Schema-based configuration
所描述的schema支持。關(guān)于如何在aop的命名空間中引入這些標簽,請參見第?A.2.7?節(jié) “The aop
schema”
如果你正在使用DTD,你仍然可以通過在你的application context中添加如下定義來啟用@AspectJ支持:
<bean class="org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator" />
你需要在你的應(yīng)用程序的classpath中引入兩個AspectJ庫:aspectjweaver.jar
和aspectjrt.jar
。這些庫可以在AspectJ的安裝包(1.5.1或者之后的版本)的'lib'
目錄里找到,或者也可以在Spring-with-dependencies發(fā)布包的'lib/aspectj'
目錄下找到。
啟用@AspectJ支持后,在application context中定義的任意帶有一個@Aspect切面(擁有@Aspect
注解)的bean都將被Spring自動識別并用于配置Spring AOP。以下例子展示了為完成一個不是非常有用的切面所需要的最小定義:
application context中一個常見的bean定義,它指向一個使用了@Aspect
注解的bean類:
<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
<!-- configure properties of aspect here as normal -->
</bean>
以及NotVeryUsefulAspect
類的定義,使用了
org.aspectj.lang.annotation.Aspect
注解。
package org.xyz; import org.aspectj.lang.annotation.Aspect; @Aspect public class NotVeryUsefulAspect { }
切面(用@Aspect
注解的類)和其他類一樣有方法和字段定義。他們也可能包括切入點,通知和引入(inter-type)聲明。
在Spring AOP中,擁有切面的類本身不可能是其它切面中通知的目標。一個類上面的@Aspect注解標識它為一個切面,并且從自動代理中排除它。
在前面我們提到,切入點決定了連接點關(guān)注的內(nèi)容,使得我們可以控制通知什么時候執(zhí)行。Spring AOP只支持Spring bean的方法執(zhí)行連接點。所以你可以把切入點看做是Spring bean上方法執(zhí)行的匹配。一個切入點聲明有兩個部分:一個包含名字和任意參數(shù)的簽名,還有一個切入點表達式,該表達式?jīng)Q定了我們關(guān)注那個方法的執(zhí)行。在@AspectJ注解風格的AOP中,一個切入點簽名通過一個普通的方法定義來提供,并且切入點表達式使用@Pointcut
注解來表示(作為切入點簽名的方法必須返回void
類型)。
用一個例子能幫我們清楚的區(qū)分切入點簽名和切入點表達式之間的差別,下面的例子定義了一個切入點'anyOldTransfer'
,這個切入點將匹配任何名為 "transfer" 的方法的執(zhí)行:
@Pointcut("execution(* transfer(..))")// the pointcut expression private void anyOldTransfer() {}// the pointcut signature
切入點表達式,也就是組成@Pointcut
注解的值,是正規(guī)的AspectJ 5切入點表達式。如果你想要更多了解AspectJ的切入點語言,請參見AspectJ編程指南(如果要了解基于Java 5的擴展請參閱AspectJ 5 開發(fā)手冊)或者其他人寫的關(guān)于AspectJ的書,例如Colyer et. al.著的“Eclipse AspectJ”或者Ramnivas Laddad著的“AspectJ in Action”。
Spring AOP支持在切入點表達式中使用如下的AspectJ切入點指示符:
execution - 匹配方法執(zhí)行的連接點,這是你將會用到的Spring的最主要的切入點指示符。
within - 限定匹配特定類型的連接點(在使用Spring AOP的時候,在匹配的類型中定義的方法的執(zhí)行)。
this - 限定匹配特定的連接點(使用Spring AOP的時候方法的執(zhí)行),其中bean reference(Spring AOP 代理)是指定類型的實例。
target - 限定匹配特定的連接點(使用Spring AOP的時候方法的執(zhí)行),其中目標對象(被代理的應(yīng)用對象)是指定類型的實例。
args - 限定匹配特定的連接點(使用Spring AOP的時候方法的執(zhí)行),其中參數(shù)是指定類型的實例。
@target
- 限定匹配特定的連接點(使用Spring AOP的時候方法的執(zhí)行),其中正執(zhí)行對象的類持有指定類型的注解。
@args
- 限定匹配特定的連接點(使用Spring AOP的時候方法的執(zhí)行),其中實際傳入?yún)?shù)的運行時類型持有指定類型的注解。
@within
- 限定匹配特定的連接點,其中連接點所在類型已指定注解(在使用Spring AOP的時候,所執(zhí)行的方法所在類型已指定注解)。
@annotation - 限定匹配特定的連接點(使用Spring AOP的時候方法的執(zhí)行),其中連接點的主題持有指定的注解。
另外,Spring AOP還提供了一個名為'bean
'的PCD。這個PCD允許你限定匹配連接點到一個特定名稱的Spring bean,或者到一個特定名稱Spring bean的集合(當使用通配符時)。'bean
' PCD具有下列的格式:
bean(idOrNameOfBean)
'idOrNameOfBean
'標記可以是任何Spring bean的名字:限定通配符使用'*
'來提供,如果你為Spring bean制定一些命名約定,你可以非常容易地編寫一個'bean
' PCD表達式將它們選出來。和其它連接點指示符一樣,'bean
' PCD也支持&&, ||和 !邏輯操作符。
請注意'bean
' PCD僅僅 被Spring AOP支持而不是AspectJ. 這是Spring對AspectJ中定義的標準PCD的一個特定擴展。
'bean
' PCD不僅僅可以在類型級別(被限制在基于織入AOP上)上操作而還可以在實例級別(基于Spring bean的概念)上操作。
因為Spring AOP限制了連接點必須是方法執(zhí)行級別的,上文pointcut指示符中的討論也給出了一個定義,這個定義和AspectJ的編程指南中的定義相比顯得更加狹窄。除此之外,AspectJ它本身有基于類型的語義,在執(zhí)行的連接點'this'和'target'都是指同一個對象,也就是執(zhí)行方法的對象。Spring AOP是一個基于代理的系統(tǒng),并且嚴格區(qū)分代理對象本身(對應(yīng)于'this')和背后的目標對象(對應(yīng)于'target')
切入點表達式可以使用'&', '||' 和 '!'來組合。還可以通過名字來指向切入點表達式。以下的例子展示了三種切入點表達式: anyPublicOperation
(在一個方法執(zhí)行連接點代表了任意public方法的執(zhí)行時匹配);inTrading
(在一個代表了在交易模塊中的任意的方法執(zhí)行時匹配)和 tradingOperation
(在一個代表了在交易模塊中的任意的公共方法執(zhí)行時匹配)。
@Pointcut("execution(public * *(..))") private void anyPublicOperation() {} @Pointcut("within(com.xyz.someapp.trading..*") private void inTrading() {} @Pointcut("anyPublicOperation() && inTrading()") private void tradingOperation() {}
如上所示,用更少的命名組件來構(gòu)建更加復雜的切入點表達式是一種最佳實踐。當用名字來指定切入點時使用的是常見的Java成員可視性訪問規(guī)則。(比如說,你可以在同一類型中訪問私有的切入點,在繼承關(guān)系中訪問受保護的切入點,可以在任意地方訪問公共切入點)。成員可視性訪問規(guī)則不影響到切入點的匹配。
當開發(fā)企業(yè)級應(yīng)用的時候,你通常會想要從幾個切面來引用模塊化的應(yīng)用和特定操作的集合。我們推薦定義一個“SystemArchitecture”切面來捕捉通用的切入點表達式。一個典型的通用切面看起來可能像下面這樣:
package com.xyz.someapp; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; @Aspect public class SystemArchitecture { @Pointcut("within(com.xyz.someapp.web..*)") public void inWebLayer() {} @Pointcut("within(com.xyz.someapp.service..*)") public void inServiceLayer() {} @Pointcut("within(com.xyz.someapp.dao..*)") public void inDataAccessLayer() {} @Pointcut("execution(* com.xyz.someapp.service.*.*(..))") public void businessService() {} @Pointcut("execution(* com.xyz.someapp.dao.*.*(..))") public void dataAccessOperation() {} }
示例中的切入點定義了一個你可以在任何需要切入點表達式的地方可引用的切面。比如,為了使service層事務(wù)化,你可以寫成:
<aop:config> <aop:advisor pointcut="com.xyz.someapp.SystemArchitecture.businessService()" advice-ref="tx-advice"/> </aop:config> <tx:advice id="tx-advice"> <tx:attributes> <tx:method name="*" propagation="REQUIRED"/> </tx:attributes> </tx:advice>
我們將在第?6.3?節(jié) “基于Schema的AOP支持”中討論 <aop:config>
和<aop:advisor>
標簽。在第?9?章 事務(wù)管理
中討論事務(wù)標簽。
Spring AOP 用戶可能會經(jīng)常使用 execution
切入點指示符。執(zhí)行表達式的格式如下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
除了返回類型模式(上面代碼片斷中的ret-type-pattern),名字模式和參數(shù)模式以外,
所有的部分都是可選的。返回類型模式?jīng)Q定了方法的返回類型必須依次匹配一個連接點。
你會使用的最頻繁的返回類型模式是*
,它代表了匹配任意的返回類型。
一個全限定的類型名將只會匹配返回給定類型的方法。名字模式匹配的是方法名。
你可以使用*
通配符作為所有或者部分命名模式。
參數(shù)模式稍微有點復雜:()
匹配了一個不接受任何參數(shù)的方法,
而(..)
匹配了一個接受任意數(shù)量參數(shù)的方法(零或者更多)。
模式(*)
匹配了一個接受一個任何類型的參數(shù)的方法。
模式(*,String)
匹配了一個接受兩個參數(shù)的方法,第一個可以是任意類型,
第二個則必須是String類型。更多的信息請參閱AspectJ編程指南中
語言語義的部分。
下面給出一些通用切入點表達式的例子。
任意公共方法的執(zhí)行:
execution(public * *(..))
任何一個名字以“set”開始的方法的執(zhí)行:
execution(* set*(..))
AccountService
接口定義的任意方法的執(zhí)行:
execution(* com.xyz.service.AccountService.*(..))
在service包中定義的任意方法的執(zhí)行:
execution(* com.xyz.service.*.*(..))
在service包或其子包中定義的任意方法的執(zhí)行:
execution(* com.xyz.service..*.*(..))
在service包中的任意連接點(在Spring AOP中只是方法執(zhí)行):
within(com.xyz.service.*)
在service包或其子包中的任意連接點(在Spring AOP中只是方法執(zhí)行):
within(com.xyz.service..*)
實現(xiàn)了AccountService
接口的代理對象的任意連接點
(在Spring AOP中只是方法執(zhí)行):
this(com.xyz.service.AccountService)
'this'在綁定表單中更加常用:-
請參見后面的通知一節(jié)中了解如何使得代理對象在通知體內(nèi)可用。
實現(xiàn)AccountService
接口的目標對象的任意連接點
(在Spring AOP中只是方法執(zhí)行):
target(com.xyz.service.AccountService)
'target'在綁定表單中更加常用:-
請參見后面的通知一節(jié)中了解如何使得目標對象在通知體內(nèi)可用。
任何一個只接受一個參數(shù),并且運行時所傳入的參數(shù)是Serializable
接口的連接點(在Spring AOP中只是方法執(zhí)行)
args(java.io.Serializable)
'args'在綁定表單中更加常用:- 請參見后面的通知一節(jié)中了解如何使得方法參數(shù)在通知體內(nèi)可用。
請注意在例子中給出的切入點不同于 execution(* *(java.io.Serializable))
:
args版本只有在動態(tài)運行時候傳入?yún)?shù)是Serializable時才匹配,而execution版本在方法簽名中聲明只有一個
Serializable
類型的參數(shù)時候匹配。
目標對象中有一個 @Transactional
注解的任意連接點
(在Spring AOP中只是方法執(zhí)行)
@target(org.springframework.transaction.annotation.Transactional)
'@target'在綁定表單中更加常用:-
請參見后面的通知一節(jié)中了解如何使得注解對象在通知體內(nèi)可用。
任何一個目標對象聲明的類型有一個 @Transactional
注解的連接點
(在Spring AOP中只是方法執(zhí)行):
@within(org.springframework.transaction.annotation.Transactional)
'@within'在綁定表單中更加常用:-
請參見后面的通知一節(jié)中了解如何使得注解對象在通知體內(nèi)可用。
任何一個執(zhí)行的方法有一個 @Transactional
注解的連接點
(在Spring AOP中只是方法執(zhí)行)
@annotation(org.springframework.transaction.annotation.Transactional)
'@annotation'在綁定表單中更加常用:-
請參見后面的通知一節(jié)中了解如何使得注解對象在通知體內(nèi)可用。
任何一個只接受一個參數(shù),并且運行時所傳入的參數(shù)類型具有@Classified
注解的連接點(在Spring AOP中只是方法執(zhí)行)
@args(com.xyz.security.Classified)
'@args'在綁定表單中更加常用:-
請參見后面的通知一節(jié)中了解如何使得注解對象在通知體內(nèi)可用。
任何一個在名為'tradeService
'的Spring bean之上的連接點
(在Spring AOP中只是方法執(zhí)行):
bean(tradeService)
任何一個在名字匹配通配符表達式'*Service
'的Spring bean之上的連接點
(在Spring AOP中只是方法執(zhí)行):
bean(*Service)
通知是跟一個切入點表達式關(guān)聯(lián)起來的,并且在切入點匹配的方法執(zhí)行之前或者之后或者前后運行。 切入點表達式可能是指向已命名的切入點的簡單引用或者是一個已經(jīng)聲明過的切入點表達式。
一個切面里使用 @Before
注解聲明前置通知:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
}
如果使用一個in-place 的切入點表達式,我們可以把上面的例子換個寫法:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("execution(* com.xyz.myapp.dao.*.*(..))")
public void doAccessCheck() {
// ...
}
}
返回后通知通常在一個匹配的方法返回的時候執(zhí)行。使用 @AfterReturning
注解來聲明:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
}
說明:你可以在相同的切面里定義多個通知,或者其他成員。 我們只是在展示如何定義一個簡單的通知。這些例子主要的側(cè)重點是正在討論的問題。
有時候你需要在通知體內(nèi)得到返回的值。你可以使用@AfterReturning
接口的形式來綁定返回值:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
returning="retVal")
public void doAccessCheck(Object retVal) {
// ...
}
}
在 returning
屬性中使用的名字必須對應(yīng)于通知方法內(nèi)的一個參數(shù)名。
當一個方法執(zhí)行返回后,返回值作為相應(yīng)的參數(shù)值傳入通知方法。
一個returning
子句也限制了只能匹配到返回指定類型值的方法。
(在本例子中,返回值是Object
類,也就是說返回任意類型都會匹配)
請注意當使用后置通知時不允許返回一個完全不同的引用。
拋出異常通知在一個方法拋出異常后執(zhí)行。使用@AfterThrowing
注解來聲明:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doRecoveryActions() {
// ...
}
}
你通常會想要限制通知只在某種特殊的異常被拋出的時候匹配,你還希望可以在通知體內(nèi)得到被拋出的異常。
使用throwing
屬性不僅可以限制匹配的異常類型(如果你不想限制,請使用
Throwable
作為異常類型),還可以將拋出的異常綁定到通知的一個參數(shù)上。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
throwing="ex")
public void doRecoveryActions(DataAccessException ex) {
// ...
}
}
在throwing
屬性中使用的名字必須與通知方法內(nèi)的一個參數(shù)對應(yīng)。
當一個方法因拋出一個異常而中止后,這個異常將會作為那個對應(yīng)的參數(shù)送至通知方法。
throwing
子句也限制了只能匹配到拋出指定異常類型的方法
(上面的示例為DataAccessException
)。
不論一個方法是如何結(jié)束的,最終通知都會運行。使用@After
注解來聲明。最終通知必須準備處理正常返回和異常返回兩種情況。通常用它來釋放資源。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;
@Aspect
public class AfterFinallyExample {
@After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doReleaseLock() {
// ...
}
}
最后一種通知是環(huán)繞通知。環(huán)繞通知在一個方法執(zhí)行之前和之后執(zhí)行。它使得通知有機會 在一個方法執(zhí)行之前和執(zhí)行之后運行。而且它可以決定這個方法在什么時候執(zhí)行,如何執(zhí)行,甚至是否執(zhí)行。 環(huán)繞通知經(jīng)常在某線程安全的環(huán)境下,你需要在一個方法執(zhí)行之前和之后共享某種狀態(tài)的時候使用。 請盡量使用最簡單的滿足你需求的通知。(比如如果簡單的前置通知也可以適用的情況下不要使用環(huán)繞通知)。
環(huán)繞通知使用@Around
注解來聲明。通知的第一個參數(shù)必須是
ProceedingJoinPoint
類型。在通知體內(nèi),調(diào)用
ProceedingJoinPoint
的proceed()
方法會導致
后臺的連接點方法執(zhí)行。proceed
方法也可能會被調(diào)用并且傳入一個
Object[]
對象-該數(shù)組中的值將被作為方法執(zhí)行時的參數(shù)。
當傳入一個Object[]
對象的時候,處理的方法與通過AspectJ編譯器處理環(huán)繞通知略有不同。
對于使用傳統(tǒng)AspectJ語言寫的環(huán)繞通知來說,傳入?yún)?shù)的數(shù)量必須和傳遞給環(huán)繞通知的參數(shù)數(shù)量匹配
(不是后臺的連接點接受的參數(shù)數(shù)量),并且特定順序的傳入?yún)?shù)代替了將要綁定給連接點的原始值
(如果你看不懂不用擔心)。Spring采用的方法更加簡單并且能更好匹配它基于代理(proxy-based)的執(zhí)行語法,
如果你使用AspectJ的編譯器和編織器來編譯為Spring而寫的@AspectJ切面和處理參數(shù),你只需要知道這一區(qū)別即可。
有一種方法可以讓你寫出100%兼容Spring AOP和AspectJ的表達式,我們將會在后續(xù)的通知參數(shù)的章節(jié)中討論它。
import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.ProceedingJoinPoint; @Aspect public class AroundExample { @Around("com.xyz.myapp.SystemArchitecture.businessService()") public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable { // start stopwatch Object retVal = pjp.proceed(); // stop stopwatch return retVal; } }
方法的調(diào)用者得到的返回值就是環(huán)繞通知返回的值。 例如:一個簡單的緩存切面,如果緩存中有值,就返回該值,否則調(diào)用proceed()方法。 請注意proceed可能在通知體內(nèi)部被調(diào)用一次,許多次,或者根本不被調(diào)用,所有這些都是合法的。
Spring 2.0 提供了完整的通知類型 - 這意味著你可以在通知簽名中聲明所需的參數(shù), (就像我們在前面看到的后置和異常通知一樣)而不總是使用Object[]。 我們將會看到如何使得參數(shù)和其他上下文值對通知體可用。 首先讓我們看以下如何編寫普通的通知以找出正在被通知的方法。
任何通知方法可以將第一個參數(shù)定義為org.aspectj.lang.JoinPoint
類型
(環(huán)繞通知需要定義第一個參數(shù)為ProceedingJoinPoint
類型,
它是 JoinPoint
的一個子類)。JoinPoint
接口提供了一系列有用的方法,比如 getArgs()
(返回方法參數(shù))、
getThis()
(返回代理對象)、getTarget()
(返回目標)、
getSignature()
(返回正在被通知的方法相關(guān)信息)和 toString()
(打印出正在被通知的方法的有用信息)。詳細的內(nèi)容請參考JavaDoc。
我們已經(jīng)看到了如何綁定返回值或者異常(使用后置通知和異常通知)。為了可以在通知體內(nèi)訪問參數(shù),
你可以使用args
來綁定。如果在一個args表達式中應(yīng)該使用類型名字的地方
使用一個參數(shù)名字,那么當通知執(zhí)行的時候?qū)?yīng)的參數(shù)值將會被傳遞進來。用一個例子應(yīng)該會使它變得清晰。
假使你想要通知以一個Account對象作為第一個參數(shù)的DAO操作的執(zhí)行,
你想要在通知體內(nèi)也能訪問account對象,可以編寫如下的代碼:
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() &&" +
"args(account,..)")
public void validateAccount(Account account) {
// ...
}
切入點表達式的 args(account,..)
部分有兩個目的:首先它保證了
只會匹配那些接受至少一個參數(shù)的方法的執(zhí)行,而且傳入的參數(shù)必須是Account
類型的實例,
其次它使得在通知體內(nèi)可以通過account
參數(shù)訪問實際的Account
對象。
另外一個辦法是定義一個切入點,這個切入點在匹配某個連接點的時候“提供”了
Account
對象的值,然后直接從通知中訪問那個命名切入點。看起來和下面的示例一樣:
@Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() &&" +
"args(account,..)")
private void accountDataAccessOperation(Account account) {}
@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
// ...
}
有興趣的讀者請參閱 AspectJ 編程指南了解更詳細的內(nèi)容。
代理對象(this
)、目標對象(target
)
和注解(@within, @target, @annotation, @args
)都可以用一種類似的格式來綁定。
以下的例子展示了如何使用 @Auditable
注解來匹配方法執(zhí)行,并提取Audit代碼。
首先是@Auditable
注解的定義:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Auditable { AuditCode value(); }
然后是匹配@Auditable
方法執(zhí)行的通知:
@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && " +
"@annotation(auditable)")
public void audit(Auditable auditable) {
AuditCode code = auditable.value();
// ...
}
綁定在通知上的參數(shù)依賴切入點表達式的匹配名,并借此在(通知和切入點)的方法簽名中聲明參數(shù)名。 參數(shù)名無法 通過Java反射來獲取,所以Spring AOP使用如下的策略來確定參數(shù)名字:
如果參數(shù)名字已經(jīng)被用戶明確指定,則使用指定的參數(shù)名: 通知和切入點注解有一個額外的"argNames"屬性,該屬性用來指定所注解的方法的參數(shù)名 - 這些參數(shù)名在運行時是可以 訪問的。例子如下:
@Before(
value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
argNames="bean,auditable")
public void audit(Object bean, Auditable auditable) {
AuditCode code = auditable.value();
// ... use code and bean
}
如果第一個參數(shù)是JoinPoint
,
ProceedingJoinPoint
,
或者JoinPoint.StaticPart
類型,
你可以在“argNames”屬性的值中省去參數(shù)的名字。例如,如果你修改前面的通知來獲取連接點對象,
"argNames"屬性就不必包含它:
@Before(
value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
argNames="bean,auditable")
public void audit(JoinPoint jp, Object bean, Auditable auditable) {
AuditCode code = auditable.value();
// ... use code, bean, and jp
}
對于第一個JoinPoint
,
ProceedingJoinPoint
,和
JoinPoint.StaticPart
類型的參數(shù)特殊處理特別適合
沒有集合其它連接上下文的通知。在這種情部下,你可以簡單的省略“argNames”屬性。
例如,下面的通知不需要聲明“argNames”屬性:
@Before(
"com.xyz.lib.Pointcuts.anyPublicMethod()")
public void audit(JoinPoint jp) {
// ... use jp
}
使用'argNames'
屬性有一點笨拙,所以如果'argNames'
屬性沒有被指定,Spring AOP將查看類的debug信息并嘗試從本地的變量表確定參數(shù)名。只要類編譯時有debug信息,
(最少要有'-g:vars'
)這個信息將會出現(xiàn)。打開這個標志編譯的結(jié)果是:
(1)你的代碼稍微容易理解(反向工程),
(2)class文件的大小稍微有些大(通常不重要),
(3)你的編譯器將不會應(yīng)用優(yōu)化去移除未使用的本地變量。換句話說,打開這個標志創(chuàng)建時你應(yīng)當不會遇到困難。
如果一個@AspectJ切面已經(jīng)被AspectJ編譯器(ajc)編譯過,即使沒有debug信息,
也不需要添加argNames
參數(shù),因為編譯器會保留必需的信息。
如果不加上必要的debug信息來編譯的話,Spring AOP將會嘗試推斷綁定變量到參數(shù)的配對。
(例如,要是只有一個變量被綁定到切入點表達式,通知方法只接受一個參數(shù), 配對是顯而易見的)。
如果變量的綁定不明確,將會拋出一個AmbiguousBindingException
異常。
如果以上所有策略都失敗了,將會拋出一個IllegalArgumentException
異常。
我們之前提過我們將會討論如何編寫一個帶參數(shù)的的proceed()調(diào)用, 使得在Spring AOP和AspectJ中都能正常工作。解決方法是僅僅確保通知簽名按順序綁定方法參數(shù)。例如:
@Around("execution(List<Account> find*(..)) &&" + "com.xyz.myapp.SystemArchitecture.inDataAccessLayer() && " + "args(accountHolderNamePattern)") public Object preProcessQueryPattern(ProceedingJoinPoint pjp, String accountHolderNamePattern) throws Throwable { String newPattern = preProcess(accountHolderNamePattern); return pjp.proceed(new Object[] {newPattern}); }
大多數(shù)情況下你都會這樣綁定(就像上面的例子那樣)。
如果有多個通知想要在同一連接點運行會發(fā)生什么?Spring AOP遵循跟AspectJ一樣的優(yōu)先規(guī)則來確定通知執(zhí)行的順序。 在“進入”連接點的情況下,最高優(yōu)先級的通知會先執(zhí)行(所以給定的兩個前置通知中,優(yōu)先級高的那個會先執(zhí)行)。 在“退出”連接點的情況下,最高優(yōu)先級的通知會最后執(zhí)行。(所以給定的兩個后置通知中, 優(yōu)先級高的那個會第二個執(zhí)行)。
當定義在不同的切面里的兩個通知都需要在一個相同的連接點中運行,
那么除非你指定,否則執(zhí)行的順序是未知的。你可以通過指定優(yōu)先級來控制執(zhí)行順序。
在標準的Spring方法中可以在切面類中實現(xiàn)org.springframework.core.Ordered
接口或者用Order
注解做到這一點。在兩個切面中,
Ordered.getValue()
方法返回值(或者注解值)較低的那個有更高的優(yōu)先級。
當定義在相同的切面里的兩個通知都需要在一個相同的連接點中運行, 執(zhí)行的順序是未知的(因為這里沒有方法通過反射javac編譯的類來獲取聲明順序)。 考慮在每個切面類中按連接點壓縮這些通知方法到一個通知方法,或者重構(gòu)通知的片段到各自的切面類中 - 它能在切面級別進行排序。
引入(在AspectJ中被稱為inter-type聲明)使得一個切面可以定義被通知對象實現(xiàn)給定的接口, 并且可以為那些對象提供具體的實現(xiàn)。
使用@DeclareParents
注解來定義引入。這個注解用來定義匹配的類型
擁有一個新的父類(所以有了這個名字)。比如,給定一個接口UsageTracked
,
和接口的具體實現(xiàn)DefaultUsageTracked
類,
接下來的切面聲明了所有的service接口的實現(xiàn)都實現(xiàn)了UsageTracked
接口。
(比如為了通過JMX輸出統(tǒng)計信息)。
@Aspect public class UsageTracking { @DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class) public static UsageTracked mixin; @Before("com.xyz.myapp.SystemArchitecture.businessService() &&" + "this(usageTracked)") public void recordUsage(UsageTracked usageTracked) { usageTracked.incrementUseCount(); } }
實現(xiàn)的接口通過被注解的字段類型來決定。@DeclareParents
注解的
value
屬性是一個AspectJ的類型模式:- 任何匹配類型的bean都會實現(xiàn)
UsageTracked
接口。請注意,在上面的前置通知的例子中,service beans
可以直接用作UsageTracked
接口的實現(xiàn)。如果需要編程式的來訪問一個bean,
你可以這樣寫:
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
(這是一個高級主題,所以如果你剛開始學習AOP你可以跳過它到后面的章節(jié))
默認情況下,在application context中每一個切面都會有一個實例。AspectJ把這個叫做單例化模型。
也可以用其他的生命周期來定義切面:Spring支持AspectJ的 perthis
和pertarget
實例化模型(現(xiàn)在還不支持percflow、percflowbelow
和pertypewithin
)。
一個"perthis" 切面通過在@Aspect
注解中指定perthis
子句來聲明。讓我們先來看一個例子,然后解釋它是如何運作的:
@Aspect("perthis(com.xyz.myapp.SystemArchitecture.businessService())")
public class MyAspect {
private int someState;
@Before(com.xyz.myapp.SystemArchitecture.businessService())
public void recordServiceUsage() {
// ...
}
}
這個'perthis'
子句的效果是每個獨立的service對象執(zhí)行一個業(yè)務(wù)時都會
創(chuàng)建一個切面實例(切入點表達式所匹配的連接點上的每一個獨立的對象都會綁定到'this'上)。
在service對象上第一次調(diào)用方法的時候,切面實例將被創(chuàng)建。切面在service對象失效的同時失效。
在切面實例被創(chuàng)建前,所有的通知都不會被執(zhí)行,一旦切面對象創(chuàng)建完成,
定義的通知將會在匹配的連接點上執(zhí)行,但是只有當service對象是和切面關(guān)聯(lián)的才可以。
請參閱 AspectJ 編程指南了解更多關(guān)于per-clauses的信息。
'pertarget'
實例模型的跟“perthis”完全一樣,只不過是為每個匹配于連接點
的獨立目標對象創(chuàng)建一個切面實例。
現(xiàn)在你已經(jīng)看到了每個獨立的部分是如何運作的了,是時候把他們放到一起做一些有用的事情了!
因為并發(fā)的問題,有時候業(yè)務(wù)服務(wù)(business services)可能會失?。ɡ?,死鎖失敗)。如果重新嘗試一下,
很有可能就會成功。對于業(yè)務(wù)服務(wù)來說,重試幾次是很正常的(Idempotent操作不需要用戶參與,否則會得出矛盾的結(jié)論)
我們可能需要透明的重試操作以避免客戶看到一個PessimisticLockingFailureException
異常。
很明顯,在一個橫切多層的情況下,這是非常有必要的,因此通過切面來實現(xiàn)是很理想的。
因為我們想要重試操作,我們會需要使用到環(huán)繞通知,這樣我們就可以多次調(diào)用proceed()方法。 下面是簡單的切面實現(xiàn):
@Aspect public class ConcurrentOperationExecutor implements Ordered { private static final int DEFAULT_MAX_RETRIES = 2; private int maxRetries = DEFAULT_MAX_RETRIES; private int order = 1; public void setMaxRetries(int maxRetries) { this.maxRetries = maxRetries; } public int getOrder() { return this.order; } public void setOrder(int order) { this.order = order; } @Around("com.xyz.myapp.SystemArchitecture.businessService()") public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable { int numAttempts = 0; PessimisticLockingFailureException lockFailureException; do { numAttempts++; try { return pjp.proceed(); } catch(PessimisticLockingFailureException ex) { lockFailureException = ex; } } while(numAttempts <= this.maxRetries); throw lockFailureException; } }
請注意切面實現(xiàn)了 Ordered
接口,這樣我們就可以把切面的優(yōu)先級設(shè)定為高于事務(wù)通知
(我們每次重試的時候都想要在一個全新的事務(wù)中進行)。maxRetries
和order
屬性都可以在Spring中配置。主要的動作在doConcurrentOperation
這個環(huán)繞通知方法中發(fā)生。
請注意這個時候我們所有的businessService()
方法都會使用這個重試策略。
我們首先會嘗試處理,如果得到一個PessimisticLockingFailureException
異常,
我們僅僅重試直到耗盡所有預設(shè)的重試次數(shù)。
對應(yīng)的Spring配置如下:
<aop:aspectj-autoproxy/> <bean id="concurrentOperationExecutor" class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor"> <property name="maxRetries" value="3"/> <property name="order" value="100"/> </bean>
為改進切面,使之僅僅重試idempotent操作,我們可以定義一個
Idempotent
注解:
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
// marker annotation
}
并且對service操作的實現(xiàn)進行注解。為了只重試idempotent操作,切面的修改只需要改寫切入點表達式,
使得只匹配@Idempotent
操作:
@Around("com.xyz.myapp.SystemArchitecture.businessService() && " + "@annotation(com.xyz.myapp.service.Idempotent)") public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable { ... }