天天看點

[Spring實戰系列](17)編寫切點與聲明切面

版權聲明:本文為部落客原創文章,未經部落客允許不得轉載。 https://blog.csdn.net/SunnyYoona/article/details/50655462

切點用于準确定位應該在什麼地方應用切面的通知。切點和通知是切面的最基本元素。

在Spring AOP中,需要使用AspectJ的切點表達式語言來定義切點。關于Spring AOP的AspectJ切點,最重要的一點是Spring僅支援AspectJ切點訓示器的一個子集。

類型 說明
arg() 限制連接配接點比對參數為指定類型的執行方法。
@args() 限制連接配接點比對參數由指定注解标注的執行方法。
execution() 用于比對的是連接配接點的執行方法。
this() 限制連接配接點比對AOP代理的Bean引用為指定類型的類。
target() 限制連接配接點比對目标對象為指定類型的類。
@target() 限制連接配接點比對特定的執行對象,這些對象對應的類具備指定類型的注解。
within() 限制連接配接點比對指定的類型。
@within() 限制連接配接點比對指定注解所标注的類型(當使用Spring AOP時,方法定義再由指定的注解所标注的類裡)。
@annotation 限制比對帶有指定注解連接配接點。

在Spring中嘗試使用AspectJ其他訓示器時,将會抛出IllegalArgumentException異常。

注意:

隻有execution訓示器是唯一的執行比對,而其他的訓示器都是用于限制比對的。這說明execution訓示器是我們在編寫切點定義時最主要的訓示器。在此基礎上,我們使用其他訓示器來限制所比對的切點。

1. 編寫切點

如下圖所示的切點表達式表示當Singer的perform()方法執行時會觸發通知。我們使用execution()訓示器選擇Singer的perform()方法。方法表達式以*号開始,标示了我們不關心傳回值的類型。然後,我們指定了全限定類名和方法名。對于方法參數清單,我們使用(..)标示切點選擇任意的perform()方法,無論該方法的參數是什麼。

除此之外,我們還可以對上面的比對進行限制,可以使用within()訓示器來限制比對。

我們使用&&操作符把execution()和within()訓示器連接配接在一起形成and關系(切點必須比對所有的訓示器)。

2. 在XML中聲明切面

Spring的AOP配置元素簡化了基于POJO切面的聲明:

<aop:advisor> 定義AOP通知器。
<aop:after> 定義AOP後置通知(不管被通知的方法是否執行成功)
<aop:after-returning> 定義AOP after-returning通知。
<aop:after-throwing> 定義 AOP after-throwing 通知。
<aop:around> 定義 AOP 環繞通知。
<aop:aspect> 定義切面。
<aop:aspectj-autoproxy> 啟用@AspectJ注解驅動的切面。
<aop:before> 定義 AOP前置通知。
<aop:config> 頂層的AOP配置元素。大多數的<aop:*>元素必須包含在<aop:config>元素内。
<aop:declare-parents> 為被通知的對象引入額外的接口,并透明的實作。
<aop:pointcut> 定義切點。

為了闡述Spring AOP,我們建立一個觀衆類(Audience)類:

package com.sjf.bean;

/**

* 觀衆類

* @author sjf0115

*

*/

public class Audience {

public void takeSeats(){

System.out.println("the audience is taking their seats...");

}

public void applaud(){

System.out.println("very good, clap clap clap...");

}

public void demandRefund(){

System.out.println("very bad, We want our money back...");

}

}

Audience類并沒有任何特别之處,她就是一個有幾個方法的簡單Java類。我們可以像其他類一樣,利用XML把它注冊為Spring應用上下文中的一個Bean:

<bean id = "audience" class = "com.sjf.bean.Audience">

</bean>

我們需要Spring AOP就能把它成為一個切面。

2.1 前置聲明和後置聲明

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xmlns:context="http://www.springframework.org/schema/context"

xmlns:aop="http://www.springframework.org/schema/aop"

xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd

http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd

http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

<bean id = "singer" class = "com.sjf.bean.Singer">

</bean>

<bean id = "audience" class = "com.sjf.bean.Audience">

</bean>

<aop:config proxy-target-class="true">

<!-- 聲明定義一個切面 -->

<aop:aspect ref = "audience">

<!-- 表演之前 -->

<aop:before method="takeSeats" pointcut="execution(* com.sjf.bean.Singer.perform(..))"/>

<!-- 表演之後 -->

<aop:after-returning method="applaud" pointcut="execution(* com.sjf.bean.Singer.perform(..))"/>

<!-- 表演失敗之後 -->

<aop:after-throwing method="demandRefund" pointcut="execution(* com.sjf.bean.Singer.perform(..))"/>

</aop:aspect>

</aop:config>

</beans>

大多數的AOP配置元素必須在<aop:config>元素的上下文内使用。這條規則有幾種例外場景,但是把Bean聲明為一個切面時,我們總是從<aop:config>元素開始配置。

在<aop:config>元素内,我們可以聲明一個或者多個通知器,切面或者切點。上述例子中,我們使用<aop:aspect>元素聲明了一個簡單的切面。ref元素引用了一個Bean(Audience),該Bean實作了切面的功能。ref元素應用的Bean提供了在切面上通知所調用的方法。

該切面應用了3個不同的通知。<aop:before>元素定義了比對切點的方法執行之前調用前置通知方法,audience Bean 的takeSeats()方法。<aop:after-returning>元素定義了一個傳回後(after-returning)通知,在切點所比對的方法調用之後在執行applaud()方法。<aop:after-throwing>元素定義了抛出異常後通知,如果所有比對的方法執行時抛出任何異常,都将調用demandRefund()方法。

下面展示了通知邏輯如何嵌入到業務邏輯中:

在所有的通知元素中,pointcut屬性定義了通知所應用的切點。pointcut屬性的值是使用AspectJ切點表達式文法所定義的切點。

你或許注意到所有通知元素中的pointcut屬性的值都是一樣的,這是因為所有的通知都是應用到相同的切點上。這似乎違反了DRY(不要重複你自己)原則。我們做一下修改,可以使用<aop:pointcut>元素定義一個命名切點。

<aop:config proxy-target-class="true">

<!-- 聲明定義一個切面 -->

<aop:aspect ref = "audience">

<aop:pointcut id="singerPerfom" expression="execution(* com.sjf.bean.Singer.perform(..))" />

<!-- 表演之前 -->

<aop:before method="takeSeats" pointcut-ref="singerPerfom"/>

<!-- 表演之後 -->

<aop:after-returning method="applaud" pointcut-ref="singerPerfom"/>

<!-- 表演失敗之後 -->

<aop:after-throwing method="demandRefund" pointcut-ref="singerPerfom"/>

</aop:aspect>

</aop:config>

<aop:pointcut>元素定義了一個id為singerPerfom的切點,同時修改所有的通知元素,用pointcut-ref屬性來引用這個命名切點。

2.2 聲明環繞通知

如果不适用成員變量存儲資訊,那麼在前置通知和後置通知之間共享資訊是非常麻煩的。我們希望實作這樣一個功能:希望觀衆一直關注演出,并報告演出的演出時長。使用前置通知和後置通知實作該功能的唯一方式是:在前置通知中記錄開始時間,并在某個後置通知中報告演出的時長。但這樣的話,我們必須在一個成員變量中儲存開始時間。是以我們可以使用環繞通知,隻需在一個方法中實作即可。

public void PerformTime(ProceedingJoinPoint joinPoint){

// 演出之前

System.out.println("the audience is taking their seats...");

try {

long start = System.currentTimeMillis();

// 執行演出操作

joinPoint.proceed();

long end = System.currentTimeMillis();

// 演出成功

System.out.println("very good, clap clap clap...");

System.out.println("該演出共需要 "+(end - start) + " milliseconds");

} catch (Throwable e) {

// 演出失敗

System.out.println("very bad, We want our money back...");

e.printStackTrace();

}

}

對于這個新的通知方法,我們會注意到它使用了ProceedingJoinPoint作為方法的入參。這個對象非常重要,因為它能讓我們在通知裡調用被通知 的方法。如果希望把控制轉給被通知的方法時,我們可以調用ProceedingJoinPoint的proceed()方法。如果忘記調用proceed()方法,通知将會阻止被通知方法的調用。我們還可以在通知裡多次調用被通知方法,這樣做的一個目的是實作重試邏輯,在被通知的方法執行失敗時反複重試。

PerformTime()方法包含了之前3個通知方法的所有邏輯,并且該方法還會負責自身的異常處理。聲明環繞通知與聲明其他類型的通知并沒有太大的差別,隻需要<aop:around>元素。

<aop:config proxy-target-class="true">

<!-- 聲明定義一個切面 -->

<aop:aspect ref = "audience">

<aop:pointcut id="singerPerfom" expression="execution(* com.sjf.bean.Singer.perform(..))" />

<!-- 聲明環繞通知 -->

<aop:around method="performTime" pointcut-ref="singerPerfom"/>

</aop:aspect>

</aop:config>

運作結果:

the audience is taking their seats...

正在上演個人演唱會...

very good, clap clap clap...

該演出共需要 37 milliseconds  

像其他通知的XML一樣,<aop:around>指定了一個切點和一個通知方法的名字。

2.3 為通知傳遞參數

到目前為止,我們的切面很簡單,沒有任何的參數。但是有時候通知并不是僅僅是對方法進行簡單包裝,還需要校驗傳遞給方法的參數值,這時候為通知傳遞參數就非常有用了。

下面是歌手的實體類:

package com.sjf.bean;

/**

* 歌手實體類

* @author sjf0115

*

*/

public class Singer {

public void perform(String songName) {

System.out.println("正在上演個人演唱會,演唱曲目為 " + songName);

}

}

在這我們提供了一個Organizers(主辦方)實體類,在歌手演唱之前截獲歌手演唱的曲目,然後通知給大家:

package com.sjf.bean;

/**

* 主辦方實體類

* @author sjf0115

*

*/

public class Organizers {

public void BeforeSong(String songName){

System.out.println("演唱會馬上就開始了,演唱歌曲為 " + songName);

}

public void AfterSong(){

System.out.println("演唱曲目結束,謝謝大家...");

}

}

我們像以前一樣使用<aop:aspect>,<aop:before>和<aop:after>元素。但是這次我們通過配置實作将被通知方法的參數傳遞給通知。

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xmlns:context="http://www.springframework.org/schema/context"

xmlns:aop="http://www.springframework.org/schema/aop"

xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd

http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd

http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

<bean id = "singer" class = "com.sjf.bean.Singer">

</bean>

<bean id = "Organizers" class = "com.sjf.bean.Organizers">

</bean>

<aop:config>

<!-- 聲明定義一個切面 -->

<aop:aspect ref = "Organizers">

<aop:pointcut id="singerPerform" expression="execution(* com.sjf.bean.Singer.perform(String)) and args(song)" />

<aop:pointcut id="singerPerform2" expression="execution(* com.sjf.bean.Singer.perform(..))" />

<aop:before method="BeforeSong" pointcut-ref="singerPerform" arg-names="song"/>

<aop:after-returning method="AfterSong" pointcut-ref="singerPerform2"/>

</aop:aspect>

</aop:config>

</beans>

關鍵之處在于切點定義和<aop:before>的arg-names屬性。切點标示了Singer的perform()方法,指定了String參數。然後在args參數中标示了song作為參數。同樣,<aop:before>元素引用了切點中song參數,标示該參數必須傳遞給Organizers的BeforeSong()方法。

演唱會馬上就開始了,演唱歌曲為 你是我的眼淚

正在上演個人演唱會,演唱曲目為 你是我的眼淚

演唱曲目結束,謝謝大家...  

來源于:《Spring實戰》