javascript
Spring AOP切点表达式详解
1. 簡介
???????面向對象編程,也稱為OOP(即Object Oriented Programming)最大的優點在于能夠將業務模塊進行封裝,從而達到功能復用的目的。通過面向對象編程,不同的模板可以相互組裝,從而實現更為復雜的業務模塊,其結構形式可用下圖表示:
????????面向對象編程解決了業務模塊的封裝復用的問題,但是對于某些模塊,其本身并不獨屬于摸個業務模塊,而是根據不同的情況,貫穿于某幾個或全部的模塊之間的。例如登錄驗證,其只開放幾個可以不用登錄的接口給用戶使用(一般登錄使用攔截器實現,但是其切面思想是一致的);再比如性能統計,其需要記錄每個業務模塊的調用,并且監控器調用時間??梢钥吹?#xff0c;這些橫貫于每個業務模塊的模塊,如果使用面向對象的方式,那么就需要在已封裝的每個模塊中添加相應的重復代碼,對于這種情況,面向切面編程就可以派上用場了。
???????面向切面編程,也稱為AOP(即Aspect Oriented Programming),指的是將一定的切面邏輯按照一定的方式編織到指定的業務模塊中,從而將這些業務模塊的調用包裹起來。如下是其結構示意圖:
2. AOP的各個扮演者
2.1 AOP的主要角色
- 切面:使用切點表達式表示,指定了當前切面邏輯所要包裹的業務模塊的范圍大小;
- Advice:也即切面邏輯,指定了當前用于包裹切面指定的業務模塊的邏輯。
2.2 Advice的主要類型
- @Before:該注解標注的方法在業務模塊代碼執行之前執行,其不能阻止業務模塊的執行,除非拋出異常;
- @AfterReturning:該注解標注的方法在業務模塊代碼執行之后執行;
- @AfterThrowing:該注解標注的方法在業務模塊拋出指定異常后執行;
- @After:該注解標注的方法在所有的Advice執行完成后執行,無論業務模塊是否拋出異常,類似于finally的作用;
- @Around:該注解功能最為強大,其所標注的方法用于編寫包裹業務模塊執行的代碼,其可以傳入一個ProceedingJoinPoint用于調用業務模塊的代碼,無論是調用前邏輯還是調用后邏輯,都可以在該方法中編寫,甚至其可以根據一定的條件而阻斷業務模塊的調用;
- @DeclareParents:其是一種Introduction類型的模型,在屬性聲明上使用,主要用于為指定的業務模塊添加新的接口和相應的實現。
- @Aspect:嚴格來說,其不屬于一種Advice,該注解主要用在類聲明上,指明當前類是一個組織了切面邏輯的類,并且該注解中可以指定當前類是何種實例化方式,主要有三種:singleton、perthis和pertarget,具體的使用方式后面會進行講解。
????????這里需要說明的是,@Before是業務邏輯執行前執行,與其對應的是@AfterReturning,而不是@After,@After是所有的切面邏輯執行完之后才會執行,無論是否拋出異常。
3. 切點表達式
3.1 execution
???????由于Spring切面粒度最小是達到方法級別,而execution表達式可以用于明確指定方法返回類型,類名,方法名和參數名等與方法相關的部件,并且在Spring中,大部分需要使用AOP的業務場景也只需要達到方法級別即可,因而execution表達式的使用是最為廣泛的。如下是execution表達式的語法:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)???????這里問號表示當前項可以有也可以沒有,其中各項的語義如下:
- modifiers-pattern:方法的可見性,如public,protected;
- ret-type-pattern:方法的返回值類型,如int,void等;
- declaring-type-pattern:方法所在類的全路徑名,如com.spring.Aspect;
- name-pattern:方法名類型,如buisinessService();
- param-pattern:方法的參數類型,如java.lang.String;
- throws-pattern:方法拋出的異常類型,如java.lang.Exception;
????????如下是一個使用execution表達式的例子:
execution(public * com.spring.service.BusinessObject.businessService(java.lang.String,..))???????上述切點表達式將會匹配使用public修飾,返回值為任意類型,并且是com.spring.BusinessObject類中名稱為businessService的方法,方法可以有多個參數,但是第一個參數必須是java.lang.String類型的方法。上述示例中我們使用了..通配符,關于通配符的類型,主要有兩種:
- *通配符,該通配符主要用于匹配單個單詞,或者是以某個詞為前綴或后綴的單詞。
???????如下示例表示返回值為任意類型,在com.spring.service.BusinessObject類中,并且參數個數為零的方法:
execution(* com.spring.service.BusinessObject.*())???????下述示例表示返回值為任意類型,在com.spring.service包中,以Business為前綴的類,并且是類中參數個數為零方法:
execution(* com.spring.service.Business*.*())- ..通配符,該通配符表示0個或多個項,主要用于declaring-type-pattern和param-pattern中,如果用于declaring-type-pattern中,則表示匹配當前包及其子包,如果用于param-pattern中,則表示匹配0個或多個參數。
???????如下示例表示匹配返回值為任意類型,并且是com.spring.service包及其子包下的任意類的名稱為businessService的方法,而且該方法不能有任何參數:
execution(* com.spring.service..*.businessService())???????這里需要說明的是,包路徑service..*.businessService()中的..應該理解為延續前面的service路徑,表示到service路徑為止,或者繼續延續service路徑,從而包括其子包路徑;后面的*.businessService(),這里的*表示匹配一個單詞,因為是在方法名前,因而表示匹配任意的類。
???????如下示例是使用..表示任意個數的參數的示例,需要注意,表示參數的時候可以在括號中事先指定某些類型的參數,而其余的參數則由..進行匹配:
execution(* com.spring.service.BusinessObject.businessService(java.lang.String,..))3.2 within
???????within表達式的粒度為類,其參數為全路徑的類名(可使用通配符),表示匹配當前表達式的所有類都將被當前方法環繞。如下是within表達式的語法:
within(declaring-type-pattern)???????within表達式只能指定到類級別,如下示例表示匹配com.spring.service.BusinessObject中的所有方法:
within(com.spring.service.BusinessObject)???????within表達式路徑和類名都可以使用通配符進行匹配,比如如下表達式將匹配com.spring.service包下的所有類,不包括子包中的類:
within(com.spring.service.*)???????如下表達式表示匹配com.spring.service包及子包下的所有類:
within(com.spring.service..*)3.3 args
???????args表達式的作用是匹配指定參數類型和指定參數數量的方法,無論其類路徑或者是方法名是什么。這里需要注意的是,args指定的參數必須是全路徑的。如下是args表達式的語法:
args(param-pattern)???????如下示例表示匹配所有只有一個參數,并且參數類型是java.lang.String類型的方法:
args(java.lang.String)???????也可以使用通配符,但這里通配符只能使用..,而不能使用*。如下是使用通配符的實例,該切點表達式將匹配第一個參數為java.lang.String,最后一個參數為java.lang.Integer,并且中間可以有任意個數和類型參數的方法:
args(java.lang.String,..,java.lang.Integer)3.4 this和target
???????this和target需要放在一起進行講解,主要目的是對其進行區別。this和target表達式中都只能指定類或者接口,在面向切面編程規范中,this表示匹配調用當前切點表達式所指代對象方法的對象,target表示匹配切點表達式指定類型的對象。比如有兩個類A和B,并且A調用了B的某個方法,如果切點表達式為this(B),那么A的實例將會被匹配,也即其會被使用當前切點表達式的Advice環繞;如果這里切點表達式為target(B),那么B的實例也即被匹配,其將會被使用當前切點表達式的Advice環繞。
???????在講解Spring中的this和target的使用之前,首先需要講解一個概念:業務對象(目標對象)和代理對象。對于切面編程,有一個目標對象,也有一個代理對象,目標對象是我們聲明的業務邏輯對象,而代理對象是使用切面邏輯對業務邏輯進行包裹之后生成的對象。如果使用的是Jdk動態代理,那么業務對象和代理對象將是兩個對象,在調用代理對象邏輯時,其切面邏輯中會調用目標對象的邏輯;如果使用的是Cglib代理,由于是使用的子類進行切面邏輯織入的,那么只有一個對象,即織入了代理邏輯的業務類的子類對象,此時是不會生成業務類的對象的。
???????在Spring中,其對this的語義進行了改寫,即如果當前對象生成的代理對象符合this指定的類型,那么就為其織入切面邏輯。簡單的說就是,this將匹配代理對象為指定類型的類。target的語義則沒有發生變化,即其將匹配業務對象為指定類型的類。如下是使用this和target表達式的簡單示例:
this(com.spring.service.BusinessObject) target(com.spring.service.BusinessObject)???????通過上面的講解可以看出,this和target的使用區別其實不大,大部分情況下其使用效果是一樣的,但其區別也還是有的。Spring使用的代理方式主要有兩種:Jdk代理和Cglib代理(關于這兩種代理方式的講解可以查看本人的文章代理模式實現方式及優缺點對比)。針對這兩種代理類型,關于目標對象與代理對象,理解如下兩點是非常重要的:
- 如果目標對象被代理的方法是其實現的某個接口的方法,那么將會使用Jdk代理生成代理對象,此時代理對象和目標對象是兩個對象,并且都實現了該接口;
- 如果目標對象是一個類,并且其沒有實現任意接口,那么將會使用Cglib代理生成代理對象,并且只會生成一個對象,即Cglib生成的代理類的對象。
???????結合上述兩點說明,這里理解this和target的異同就相對比較簡單了。我們這里分三種情況進行說明:
- this(SomeInterface)或target(SomeInterface):這種情況下,無論是對于Jdk代理還是Cglib代理,其目標對象和代理對象都是實現SomeInterface接口的(Cglib生成的目標對象的子類也是實現了SomeInterface接口的),因而this和target語義都是符合的,此時這兩個表達式的效果一樣;
- this(SomeObject)或target(SomeObject),這里SomeObject沒實現任何接口:這種情況下,Spring會使用Cglib代理生成SomeObject的代理類對象,由于代理類是SomeObject的子類,子類的對象也是符合SomeObject類型的,因而this將會被匹配,而對于target,由于目標對象本身就是SomeObject類型,因而這兩個表達式的效果一樣;
- this(SomeObject)或target(SomeObject),這里SomeObject實現了某個接口:對于這種情況,雖然表達式中指定的是一種具體的對象類型,但由于其實現了某個接口,因而Spring默認會使用Jdk代理為其生成代理對象,Jdk代理生成的代理對象與目標對象實現的是同一個接口,但代理對象與目標對象還是不同的對象,由于代理對象不是SomeObject類型的,因而此時是不符合this語義的,而由于目標對象就是SomeObject類型,因而target語義是符合的,此時this和target的效果就產生了區別;這里如果強制Spring使用Cglib代理,因而生成的代理對象都是SomeObject子類的對象,其是SomeObject類型的,因而this和target的語義都符合,其效果就是一致的。
???????關于this和target的異同,我們使用如下示例進行簡單演示:
// 目標類 public class Apple {public void eat() {System.out.println("Apple.eat method invoked.");} } // 切面類 @Aspect public class MyAspect {@Around("this(com.business.Apple)")public Object around(ProceedingJoinPoint pjp) throws Throwable {System.out.println("this is before around advice");Object result = pjp.proceed();System.out.println("this is after around advice");return result;} } <!-- bean聲明文件 --> <bean id="apple" class="chapter7.eg1.Apple"/> <bean id="aspect" class="chapter7.eg6.MyAspect"/> <aop:aspectj-autoproxy/> // 驅動類 public class AspectApp {public static void main(String[] args) {ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");Apple fruit = (Apple) context.getBean("apple");fruit.eat();} }???????執行驅動類中的main方法,結果如下:
this is before around advice Apple.eat method invoked. this is after around advice???????上述示例中,Apple沒有實現任何接口,因而使用的是Cglib代理,this表達式會匹配Apple對象。這里將切點表達式更改為target,還是執行上述代碼,會發現結果還是一樣的:
target(com.business.Apple)???????如果我們對Apple的聲明進行修改,使其實現一個接口,那么這里就會顯示出this和target的執行區別了:
public class Apple implements IApple {public void eat() {System.out.println("Apple.eat method invoked.");} } public class AspectApp {public static void main(String[] args) {ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");Fruit fruit = (Fruit) context.getBean("apple");fruit.eat();} }???????我們還是執行上述代碼,對于this表達式,其執行結果如下:
Apple.eat method invoked.???????對于target表達式,其執行結果如下:
this is before around advice Apple.eat method invoked. this is after around advice???????可以看到,這種情況下this和target表達式的執行結果是不一樣的,這正好符合我們前面講解的第三種情況。
3.5 @within
???????前面我們講解了within的語義表示匹配指定類型的類實例,這里的@within表示匹配帶有指定注解的類,其使用語法如下所示:
@within(annotation-type)???????如下所示示例表示匹配使用com.spring.annotation.BusinessAspect注解標注的類:
@within(com.spring.annotation.BusinessAspect)???????這里我們使用一個例子演示@within的用法(這里驅動類和xml文件配置與3.4節使用的一致,這里省略):
// 注解類 @Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface FruitAspect { } // 目標類 @FruitAspect public class Apple {public void eat() {System.out.println("Apple.eat method invoked.");} } // 切面類 @Aspect public class MyAspect {@Around("@within(com.business.annotation.FruitAspect)")public Object around(ProceedingJoinPoint pjp) throws Throwable {System.out.println("this is before around advice");Object result = pjp.proceed();System.out.println("this is after around advice");return result;} }???????上述切面表示匹配使用FruitAspect注解的類,而Apple則使用了該注解,因而Apple類方法的調用會被切面環繞,執行運行驅動類可得到如下結果,說明Apple.eat()方法確實被環繞了:
this is before around advice Apple.eat method invoked. this is after around advice3.6 @annotation
???????@annotation的使用方式與@within的相似,表示匹配使用@annotation指定注解標注的方法將會被環繞,其使用語法如下:
@annotation(annotation-type)???????如下示例表示匹配使用com.spring.annotation.BusinessAspect注解標注的方法:
@annotation(com.spring.annotation.BusinessAspect)???????這里我們繼續復用3.5節使用的例子進行講解@annotation的用法,只是這里需要對Apple和MyAspect使用和指定注解的方式進行修改,FruitAspect不用修改的原因是聲明該注解時已經指定了其可以使用在類,方法和參數上:
// 目標類,將FruitAspect移到了方法上 public class Apple {@FruitAspectpublic void eat() {System.out.println("Apple.eat method invoked.");} } @Aspect public class MyAspect {@Around("@annotation(com.business.annotation.FruitAspect)")public Object around(ProceedingJoinPoint pjp) throws Throwable {System.out.println("this is before around advice");Object result = pjp.proceed();System.out.println("this is after around advice");return result;} }???????這里Apple.eat()方法使用FruitAspect注解進行了標注,因而該方法的執行會被切面環繞,其執行結果如下:
this is before around advice Apple.eat method invoked. this is after around advice3.7 @args
???????@within和@annotation分別表示匹配使用指定注解標注的類和標注的方法將會被匹配,@args則表示使用指定注解標注的類作為某個方法的參數時該方法將會被匹配。如下是@args注解的語法:
@args(annotation-type)???????如下示例表示匹配使用了com.spring.annotation.FruitAspect注解標注的類作為參數的方法:
@args(com.spring.annotation.FruitAspect)???????這里我們使用如下示例對@args的用法進行講解:
<!-- xml配置文件 --> <bean id="bucket" class="chapter7.eg1.FruitBucket"/> <bean id="aspect" class="chapter7.eg6.MyAspect"/> <aop:aspectj-autoproxy/> // 使用注解標注的參數類 @FruitAspect public class Apple {} // 使用Apple參數的目標類 public class FruitBucket {public void putIntoBucket(Apple apple) {System.out.println("put apple into bucket.");} } @Aspect public class MyAspect {@Around("@args(chapter7.eg6.FruitAspect)")public Object around(ProceedingJoinPoint pjp) throws Throwable {System.out.println("this is before around advice");Object result = pjp.proceed();System.out.println("this is after around advice");return result;} } // 驅動類 public class AspectApp {public static void main(String[] args) {ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");FruitBucket bucket = (FruitBucket) context.getBean("bucket");bucket.putIntoBucket(new Apple());} }???????這里FruitBucket.putIntoBucket(Apple)方法的參數Apple使用了@args注解指定的FruitAspect進行了標注,因而該方法的調用將會被環繞。執行驅動類,結果如下:
this is before around advice put apple into bucket. this is after around advice3.8 @DeclareParents
???????@DeclareParents也稱為Introduction(引入),表示為指定的目標類引入新的屬性和方法。關于@DeclareParents的原理其實比較好理解,因為無論是Jdk代理還是Cglib代理,想要引入新的方法,只需要通過一定的方式將新聲明的方法織入到代理類中即可,因為代理類都是新生成的類,因而織入過程也比較方便。如下是@DeclareParents的使用語法:
@DeclareParents(value = "TargetType", defaultImpl = WeaverType.class) private WeaverInterface attribute;???????這里TargetType表示要織入的目標類型(帶全路徑),WeaverInterface中聲明了要添加的方法,WeaverType中聲明了要織入的方法的具體實現。如下示例表示在Apple類中織入IDescriber接口聲明的方法:
@DeclareParents(value = "com.spring.service.Apple", defaultImpl = DescriberImpl.class) private IDescriber describer;???????這里我們使用一個如下實例對@DeclareParents的使用方式進行講解,配置文件與3.4節的一致,這里略:
// 織入方法的目標類 public class Apple {public void eat() {System.out.println("Apple.eat method invoked.");} } // 要織入的接口 public interface IDescriber {void desc(); } // 要織入接口的默認實現 public class DescriberImpl implements IDescriber {@Overridepublic void desc() {System.out.println("this is an introduction describer.");} } // 切面實例 @Aspect public class MyAspect {@DeclareParents(value = "com.spring.service.Apple", defaultImpl = DescriberImpl.class)private IDescriber describer; } // 驅動類 public class AspectApp {public static void main(String[] args) {ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");IDescriber describer = (IDescriber) context.getBean("apple");describer.desc();} }???????在MyAspect中聲明了我們需要將IDescriber的方法織入到Apple實例中,在驅動類中我們可以看到,我們獲取的是apple實例,但是得到的bean卻可以強轉為IDescriber類型,因而說明我們的織入操作成功了。
3.9 perthis和pertarget
???????在Spring AOP中,切面類的實例只有一個,比如前面我們一直使用的MyAspect類,假設我們使用的切面類需要具有某種狀態,以適用某些特殊情況的使用,比如多線程環境,此時單例的切面類就不符合我們的要求了。在Spring AOP中,切面類默認都是單例的,但其還支持另外兩種多例的切面實例的切面,即perthis和pertarget,需要注意的是perthis和pertarget都是使用在切面類的@Aspect注解中的。這里perthis和pertarget表達式中都是指定一個切面表達式,其語義與前面講解的this和target非常的相似,perthis表示如果某個類的代理類符合其指定的切面表達式,那么就會為每個符合條件的目標類都聲明一個切面實例;pertarget表示如果某個目標類符合其指定的切面表達式,那么就會為每個符合條件的類聲明一個切面實例。從上面的語義可以看出,perthis和pertarget的含義是非常相似的。如下是perthis和pertarget的使用語法:
perthis(pointcut-expression) pertarget(pointcut-expression)???????由于perthis和pertarget的使用效果大部分情況下都是一致的,我們這里主要講解perthis和pertarget的區別。關于perthis和pertarget的使用,需要注意的一個點是,由于perthis和pertarget都是為每個符合條件的類聲明一個切面實例,因而切面類在配置文件中的聲明上一定要加上prototype,否則Spring啟動是會報錯的。如下是我們使用的示例:
<!-- xml配置文件 --> <bean id="apple" class="chapter7.eg1.Apple"/> <bean id="aspect" class="chapter7.eg6.MyAspect" scope="prototype"/> <aop:aspectj-autoproxy/> // 目標類實現的接口 public interface Fruit {void eat(); } // 業務類 public class Apple implements Fruit {public void eat() {System.out.println("Apple.eat method invoked.");} } // 切面類 @Aspect("perthis(this(com.spring.service.Apple))") public class MyAspect {public MyAspect() {System.out.println("create MyAspect instance, address: " + toString());}@Around("this(com.spring.service.Apple)")public Object around(ProceedingJoinPoint pjp) throws Throwable {System.out.println("this is before around advice");Object result = pjp.proceed();System.out.println("this is after around advice");return result;} } // 驅動類 public class AspectApp {public static void main(String[] args) {ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");Fruit fruit = context.getBean(Fruit.class);fruit.eat();} }???????這里我們使用的切面表達式語法為perthis(this(com.spring.service.Apple)),這里this表示匹配代理類是Apple類型的類,perthis則表示會為這些類的每個實例都創建一個切面類。由于Apple實現了Fruit接口,因而Spring使用Jdk動態代理為其生成代理類,也就是說代理類與Apple都實現了Fruit接口,但是代理類不是Apple類型,因而這里聲明的切面不會匹配到Apple類。執行上述驅動類,結果如下:
Apple.eat method invoked.???????結果表明Apple類確實沒有被環繞。如果我們講切面類中的perthis和this修改為pertarget和target,效果如何呢:
@Aspect("pertarget(target(com.spring.service.Apple))") public class MyAspect {public MyAspect() {System.out.println("create MyAspect instance, address: " + toString());}@Around("target(com.spring.service.Apple)")public Object around(ProceedingJoinPoint pjp) throws Throwable {System.out.println("this is before around advice");Object result = pjp.proceed();System.out.println("this is after around advice");return result;} }???????執行結果如下:
create MyAspect instance, address: chapter7.eg6.MyAspect@48fa0f47 this is before around advice Apple.eat method invoked. this is after around advice???????可以看到,Apple類被切面環繞了。這里target表示目標類是Apple類型,雖然Spring使用了Jdk動態代理實現切面的環繞,代理類雖不是Apple類型,但是目標類卻是Apple類型,符合target的語義,而pertarget會為每個符合條件的表達式的類實例創建一個代理類實例,因而這里Apple會被環繞。
???????由于代理類與目標類的差別非常小,因而與this和target一樣,perthis和pertarget的區別也非常小,大部分情況下其使用效果是一致的。關于切面多實例的創建,其演示比較簡單,我們可以將xml文件中的Apple實例修改為prototype類型,并且在驅動類中多次獲取Apple類的實例:
<!-- xml配置文件 --> <bean id="apple" class="chapter7.eg1.Apple" scope="prototype"/> <bean id="aspect" class="chapter7.eg6.MyAspect" scope="prototype"/> <aop:aspectj-autoproxy/> public class AspectApp {public static void main(String[] args) {ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");Fruit fruit = context.getBean(Fruit.class);fruit.eat();fruit = context.getBean(Fruit.class);fruit.eat();} }???????執行結果如下:
create MyAspect instance, address: chapter7.eg6.MyAspect@48fa0f47 this is before around advice Apple.eat method invoked. this is after around advice create MyAspect instance, address: chapter7.eg6.MyAspect@56528192 this is before around advice Apple.eat method invoked. this is after around advice???????執行結果中兩次打印的create MyAspect instance表示當前切面實例創建了兩次,這也符合我們進行的兩次獲取Apple實例。
4. 小結
???????本文首先對AOP進行了簡單介紹,然后介紹了切面中的各個角色,最后詳細介紹了切點表達式中各個不同類型表達式的語法。
from:?https://my.oschina.net/zhangxufeng/blog/1824275?
總結
以上是生活随笔為你收集整理的Spring AOP切点表达式详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Spring AOP 切点(pointc
- 下一篇: JMS(Java消息服务)入门教程