本文基于《Spring實(shí)戰(zhàn)(第4版)》所寫炊苫。
在軟件開發(fā)中,散布于應(yīng)用中多的功能被稱為橫切關(guān)注點(diǎn)(cross-cutting concern)茬故。通常來講,這些橫切關(guān)注點(diǎn)從概念上是與應(yīng)用的業(yè)務(wù)邏輯相分離的(但往往會(huì)直接嵌入到應(yīng)用的業(yè)務(wù)邏輯之中)放棒。把這些橫切關(guān)注點(diǎn)與業(yè)務(wù)相分離正是面向切面編程(AOP)所要解決的問題姻报。
切面的應(yīng)用場(chǎng)景包括:日志、安全和事務(wù)管理等间螟。
面向切面編程的基本原理
在使用面向切面編程時(shí)吴旋,我們?nèi)匀辉谝粋€(gè)地方定義通用功能损肛,但是可以通過聲明的方式定義這個(gè)功能要以何種方式在何處應(yīng)用,而無需修改受影響的類荣瑟。橫切關(guān)注點(diǎn)可以被模塊化為特殊的類治拿,這些類被稱為切面(aspect)。
描述切面的常用術(shù)語有通知(advice)褂傀、切點(diǎn)(pointcut)和連接點(diǎn)(join point)忍啤。
通知(Advice)
在AOP術(shù)語中,切面的工作被稱為通知仙辟。
Spring切面可以應(yīng)用5種類型的通知:
- 前置通知(Before):在目標(biāo)方法被調(diào)用之前調(diào)用通知功能同波;
- 后置通知(After):在目標(biāo)方法完成之后調(diào)用通知,此時(shí)不會(huì)關(guān)心方法的輸出是什么叠国;
- 返回通知(After-returning ):在目標(biāo)方法成功執(zhí)行之后調(diào)用通知未檩;
- 異常通知(After-throwing):在目標(biāo)方法拋出異常后調(diào)用通知;
- 環(huán)繞通知(Around):通知包裹了被通知的方法粟焊,在被通知的方法調(diào)用之前和調(diào)用之后執(zhí)行自定義的行為冤狡。
連接點(diǎn)(Join point)
應(yīng)用可能有數(shù)以千計(jì)的時(shí)機(jī)應(yīng)用通知。這些時(shí)機(jī)被稱為連接點(diǎn)项棠。連接點(diǎn)是在應(yīng)用執(zhí)行過程中能夠插入切面的一個(gè)點(diǎn)悲雳。這個(gè)點(diǎn)可以是調(diào)用方法時(shí)、拋出異常時(shí)香追、甚至修改一個(gè)字段時(shí)合瓢。切面代碼可以利用這些點(diǎn)插入到應(yīng)用的正常流程之中,并添加新的行為透典。
切點(diǎn)(Pointcut)
切點(diǎn)的定義會(huì)匹配通知所要織入的一個(gè)或多個(gè)連接點(diǎn)晴楔。我們通常使用明確的類和方法名稱,或是利用正則表達(dá)式定義所匹配的類和方法名稱來指定這些切點(diǎn)峭咒。
切面(Aspect)
切面是通知和切點(diǎn)的結(jié)合税弃。通知和切點(diǎn)共同定義了切面的全部內(nèi)容。
引入(Introduction)
引入允許我們向現(xiàn)有類添加新方法或?qū)傩浴?/p>
織入(Weaving)
織入是把切面應(yīng)用到目標(biāo)對(duì)象并創(chuàng)建新的代理對(duì)象的過程凑队。切面在指定的連接點(diǎn)被織入到目標(biāo)對(duì)象中则果。在目標(biāo)對(duì)象的生命周期里有多少個(gè)點(diǎn)可以進(jìn)行織入:
- 編譯期:切面在目標(biāo)類編譯時(shí)被織入。AspectJ的織入編譯器是以這種方式織入切面的顽决。
- 類加載期:切面在目標(biāo)類加載到JVM時(shí)被織入短条。需要特殊的類加載器,它可以在目標(biāo)類被引入應(yīng)用之前增強(qiáng)該目標(biāo)類的字節(jié)碼才菠。AspectJ5的加載時(shí)織入就支持以這種方式織入切面茸时。
- 運(yùn)行期:切面在應(yīng)用運(yùn)行的某個(gè)時(shí)刻被織入。一般情況下赋访,在織入切面時(shí)可都,AOP容器會(huì)為目標(biāo)對(duì)象動(dòng)態(tài)地創(chuàng)建一個(gè)代理對(duì)象缓待。SpringAOP就是以這種方式織入切面。
Spring提供了4種類型的AOP支持:
- 基于代理的經(jīng)典SpringAOP渠牲;
- 純POJO切面旋炒;
- @AspectJ注解驅(qū)動(dòng)的切面;
- 注入式AspectJ切面签杈。
Spring在運(yùn)行時(shí)通知對(duì)象
通過在代理類中包裹切面瘫镇,Spring在運(yùn)行期把切面織入到Spring管理的bean中。代理封裝了目標(biāo)類答姥,并攔截被通知方法的調(diào)用铣除,再把調(diào)用轉(zhuǎn)發(fā)給真正的目標(biāo)bean。當(dāng)代理攔截到方法調(diào)用時(shí)鹦付,在調(diào)用目標(biāo)bean方法之前尚粘,會(huì)執(zhí)行切面邏輯。
直到應(yīng)用需要被代理的bean時(shí)敲长,Spring才創(chuàng)建代理對(duì)象郎嫁。如果使用的是ApplicationContext的話,在ApplicationContext從BeanFactory中加載所有bean的時(shí)候祈噪,Spring才會(huì)創(chuàng)建被代理的對(duì)象泽铛。因?yàn)镾pring運(yùn)行時(shí)才創(chuàng)建代理對(duì)象,所以我們不需要特殊的編譯器來織入SpringAOP的切面辑鲤。
Spring只支持方法級(jí)別的連接點(diǎn)
因?yàn)镾pring基于動(dòng)態(tài)代理厚宰,所以Spring只支持方法連接點(diǎn)。Spring缺少對(duì)字段連接點(diǎn)的支持遂填,而且它不支持構(gòu)造器連接點(diǎn)。
方法之外的連接點(diǎn)攔截功能澈蝙,我們可以利用Aspect來補(bǔ)充吓坚。
通過切點(diǎn)來選擇連接點(diǎn)
在Spring AOP中,要使用AspectJ的切點(diǎn)表達(dá)式語言來定義切點(diǎn)灯荧。
由于Sring是基于代理的礁击,而某切切點(diǎn)表達(dá)式是與基于代理的AOP無關(guān)的。下表列出了Spring AOP所支持的AspectJ切點(diǎn)指示器逗载。
AspectJ指示器 | 描述 |
---|---|
arg() | 限制連接點(diǎn)匹配參數(shù)為指定類型的執(zhí)行方法 |
@args() | 限制連接點(diǎn)匹配參數(shù)有指定注解標(biāo)注的執(zhí)行方法 |
execution() | 用于匹配是連接點(diǎn)的執(zhí)行方法 |
this() | 限制連接點(diǎn)匹配AOP代理的bean引用為指定類型的類 |
target | 限制連接點(diǎn)匹配目標(biāo)對(duì)象為指定類型的類 |
@target() | 限制連接點(diǎn)匹配特定的執(zhí)行對(duì)象哆窿,這些對(duì)象對(duì)應(yīng)的類要具有指定類型的注解 |
within() | 限制連接點(diǎn)匹配指定的類型 |
@within() | 限制連接點(diǎn)匹配指定注解所標(biāo)注的類型(當(dāng)使用Spring AOP時(shí),方法定義在由指定的注解所標(biāo)注的類里) |
@annotation | 限制匹配帶有指定注解的連接點(diǎn) |
在Spring中嘗試使用AspectJ其他指示器時(shí)厉斟,將會(huì)拋出IllegalArgument-Exception異常挚躯。
注意:如上所展示的這些Spring支持的指示器時(shí),注意只有execution指示器是實(shí)際執(zhí)行匹配的擦秽,而其他的指示器都是用來限制匹配的码荔。
編寫切點(diǎn)
為了闡述Spring中的切面漩勤,我們定義一個(gè)Performance接口:
package concert;
public interface Performance {
public void perform() ;
}
假設(shè)我們想編寫Performance的perform()方法觸發(fā)的通知,下面展示了切點(diǎn)表達(dá)式的寫法
現(xiàn)在假設(shè)我們需要配置的切點(diǎn)僅匹配concert包缩搅。我們可以使用within()指示器來限制匹配越败,如下圖
注意:我們使用了“&&”操作符把execution()和within()指示器連接在一起形成與(可以用and代替)關(guān)系(切點(diǎn)必須匹配所有的指示器)。類似地硼瓣,我們可以使用“||操作符”來標(biāo)識(shí)或(可以用or代替)關(guān)系究飞,而使用“!”操作符來標(biāo)識(shí)非(可以用not代替)操作。
在切點(diǎn)中選擇bean
除了表所列的指示器外堂鲤,Spring還引入了一個(gè)新的bean()指示器亿傅,它允許我們?cè)谇悬c(diǎn)表達(dá)式中使用bean的ID來標(biāo)識(shí)bean。bean()使用beanID或bean名稱作為參數(shù)限制切點(diǎn)只匹配特定的bean筑累。
例如袱蜡,考慮如下的切點(diǎn):
execution(* concert.Performance.perform())
and bean('woodstock')
還可以使用非操作為除了特定ID以外的其他bean應(yīng)用通知:
execution(* concert.Performance.perform())
and !bean('woodstock')
使用注解創(chuàng)建切面
我們已經(jīng)定義了Performance接口,它是切面中切點(diǎn)的目標(biāo)對(duì)象÷冢現(xiàn)在坪蚁,讓我們使用AspectJ注解來定義切面
定義切面
我們將觀眾定義為一個(gè)切面,并將其應(yīng)用到演出上就是較為明智的做法镜沽。
下面為Audience類的代碼
package com.springinaction.perf;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
//切面 POJO
@Aspect
public class Audience {
//定義命名的切點(diǎn)
@Pointcut("execution(** com.springinaction.perf.Performance.perform(..))")
public void performance(){
}
//定義通知
@Before("performance()") // 表演之前
public void silenceCellPhones(){
System.out.println("Silencing cell phones");
}
@Before("performance()") // 表演之前
public void takeSeats(){
System.out.println("Taking seats");
}
@AfterReturning("performance()") // 表演之后
public void applause(){
System.out.println("CLAP CLAP CLAP");
}
@AfterThrowing("performance()") // 表演失敗之后
public void demandRefund(){
System.out.println("Demanding a refund");
}
@Around("performance()") // 環(huán)繞通知方法
public void watchPerformance(ProceedingJoinPoint jp){
try {
System.out.println("Silencing cell phones Again");
System.out.println("Taking seats Again");
jp.proceed();
System.out.println("CLAP CLAP CLAP Again");
}
catch (Throwable e){
System.out.println("Demanding a refund Again");
}
}
}
關(guān)于環(huán)繞通知敏晤,我們首先注意到它接受ProceedingJoinPoint作為參數(shù)。這個(gè)對(duì)象是必須要有的缅茉,因?yàn)橐谕ㄖ型ㄟ^它來調(diào)用被通知的方法嘴脾。
需要注意的是,一般情況下蔬墩,別忘記調(diào)用proceed()方法译打。如果不調(diào)用,那么通知實(shí)際上會(huì)阻塞對(duì)被通知方法的調(diào)用拇颅,也許這是所期望的效果奏司。當(dāng)然,也可以多次調(diào)用樟插,比如要實(shí)現(xiàn)一個(gè)場(chǎng)景是實(shí)現(xiàn)重試邏輯韵洋。
除了注解和沒有實(shí)際操作的performance()方法,Audience類依然是一個(gè)POJO黄锤,可以裝配為Spring中的bean
@Bean
public Audience audience(){ // 聲明Audience
return new Audience();
}
除了定義切面外搪缨,還需要啟動(dòng)自動(dòng)代理,才能使這些注解解析鸵熟。
如果使用JavaConfig的話副编,需要如下配置
package com.springinaction.perf;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@ComponentScan
@EnableAspectJAutoProxy //啟動(dòng)AspectJ自動(dòng)代理
public class AppConfig {
@Bean
public Audience audience(){ // 聲明Audience
return new Audience();
}
}
假如在Spring中要使用XML來裝配bean的話,那么需要使用Spring aop命名空間中的<aop:aspectj-autoproxy>元素流强。
<?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:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.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">
<context:component-scan base-package="com.springinaction.perf" />
<aop:aspectj-autoproxy />
<bean id="audience" class="com.springinaction.perf.Audience" />
</beans>
無論使用JavaConfig還是XML齿桃,Aspect自動(dòng)代理都會(huì)使用@Aspect注解的bean創(chuàng)建一個(gè)代理惑惶,這個(gè)代理會(huì)圍繞著所有該切面的切點(diǎn)所匹配的bean了嚎。這種情況下前翎,將會(huì)為Concert的bean創(chuàng)建一個(gè)代理,Audience類中的通知方法將會(huì)在perform()調(diào)用前后執(zhí)行痕钢。
我們需要記住的是香到,Spring的AspectJ自動(dòng)代理僅僅使用@AspectJ作為創(chuàng)建切面的指導(dǎo)鱼冀,切面依然是基于代理的。本質(zhì)上悠就,它依然是Spring基于代理的切面千绪。
處理通知中的參數(shù)
目前為止,除了環(huán)繞通知梗脾,其他通知都沒有參數(shù)荸型。如果切面所通知的方法確實(shí)有參數(shù)該怎么辦呢?切面能訪問和使用傳遞給被通知方法的參數(shù)嗎炸茧?
為了闡述這個(gè)問題瑞妇,我們來重新看一下BlankDisc樣例。
假設(shè)你想記錄每個(gè)磁道被播放的次數(shù)梭冠。為了記錄次數(shù)辕狰,我們創(chuàng)建了TrackCounter類,它是通知playTrack()方法的一個(gè)切面控漠。
package com.springinaction.disc;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import java.util.HashMap;
import java.util.Map;
@Aspect
public class TrackCounter {
private Map<Integer, Integer> trackCounts = new HashMap<>();
@Pointcut("execution(* com.springinaction.disc.CompactDisc.playTrack(int))" +
"&& args(trackNumber)") // 通知playTrack()方法
public void trackPlayed(int trackNumber){}
@Before("trackPlayed(trackNumber)") // 在播放前蔓倍,為該磁道計(jì)數(shù)
public void countTrack(int trackNumber){
int currentCount = getPlayCount(trackNumber);
trackCounts.put(trackNumber, currentCount + 1);
}
public int getPlayCount(int trackNumber){
return trackCounts.containsKey(trackNumber) ? trackCounts.get(trackNumber) : 0;
}
}
以下為切點(diǎn)表達(dá)式分解
其中args(trackNumber)限定符表明傳遞給playTrack()方法的int類型參數(shù)也會(huì)傳遞到通知中去盐捷。trackNumber也與切點(diǎn)方法簽名中的參數(shù)相匹配偶翅。切點(diǎn)定義中的參數(shù)與切點(diǎn)方法中的參數(shù)名稱是一樣的。
下面我們啟動(dòng)AspectJ自動(dòng)代理以及定義bean
package com.springinaction.disc;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import java.util.ArrayList;
import java.util.List;
/**
* Created by Shangnan on 2017/8/29.
*/
@Configuration
@EnableAspectJAutoProxy
public class TrackCounterConfig {
@Bean
public CompactDisc sgtPeppers(){
BlankDisc cd = new BlankDisc();
cd.setTitle("Sgt. Pepper's Lonely Hearts Club Band");
cd.setArtist("The Beatles");
List<String> tracks = new ArrayList<>();
tracks.add("Sgt. Pepper's Lonely Hearts Club Band");
tracks.add("With a Little Help from My Friends");
tracks.add("Luck in the Sky with Diamonds");
tracks.add("Getting Better");
tracks.add("Fixing a Hole");
tracks.add("Feel My Heart");
tracks.add("L O V E");
cd.setTracks(tracks);
return cd;
}
@Bean
public TrackCounter trackCounter(){
return new TrackCounter();
}
}
最后的簡單測(cè)試
package com.springinaction;
import static org.junit.Assert.*;
import com.springinaction.disc.CompactDisc;
import com.springinaction.disc.TrackCounter;
import com.springinaction.disc.TrackCounterConfig;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.contrib.java.lang.system.StandardOutputStreamLog;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* Created by Shangnan on 2017/8/29.
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = TrackCounterConfig.class)
public class TrackCounterTest {
@Rule
public final StandardOutputStreamLog log = new StandardOutputStreamLog();
@Autowired
private CompactDisc cd;
@Autowired
private TrackCounter counter;
@Test
public void testTrackCounter(){
cd.playTrack(1);
cd.playTrack(2);
cd.playTrack(3);
cd.playTrack(3);
cd.playTrack(3);
cd.playTrack(3);
cd.playTrack(7);
cd.playTrack(7);
assertEquals(1,counter.getPlayCount(1));
assertEquals(1,counter.getPlayCount(2));
assertEquals(4,counter.getPlayCount(3));
assertEquals(0,counter.getPlayCount(4));
assertEquals(0,counter.getPlayCount(5));
assertEquals(0,counter.getPlayCount(6));
assertEquals(2,counter.getPlayCount(7));
}
}
通過注解引入新功能
我們除了給已有的方法添加新功能外碉渡,還可以添加一些額外的功能倒堕。
回顧一下,在Spring中爆价,切面只是實(shí)現(xiàn)了它們所包裝bean相同的接口代理。如果除了實(shí)現(xiàn)這些接口媳搪,代理也能暴露新接口铭段。即便底層實(shí)現(xiàn)類并沒有實(shí)現(xiàn)這些接口,切面所通知的bean也能實(shí)現(xiàn)新的接口秦爆。下圖展示了它們是如何工作的序愚。
需要注意的是等限,當(dāng)引入接口的方法被調(diào)用時(shí)爸吮,代理會(huì)把此調(diào)用委托給實(shí)現(xiàn)了新接口的某個(gè)其他對(duì)象芬膝。實(shí)際上,一個(gè)bean的實(shí)現(xiàn)被拆分到了多個(gè)類中形娇。
為了驗(yàn)證能行得通锰霜,我們?yōu)樗械腜erformance實(shí)現(xiàn)引入Encoreable接口
package com.springinaction.perf;
public interface Encoreable {
void performEncore();
}
借助于AOP,我們創(chuàng)建一個(gè)新的切面
package com.springinaction.perf;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.DeclareParents;
@Aspect
public class EncoreableIntroducer { // 需要給Performance和其實(shí)現(xiàn)類額外添加方法的實(shí)現(xiàn)
@DeclareParents(value = "com.springinaction.perf.Performance+",
defaultImpl = DefaultEncoreable.class)
public static Encoreable encoreable;
}
其中@DeclareParents注解桐早,將Encoreable接口引入到Performance bean中癣缅。
@DeclareParents注解有三個(gè)部門組成:
- value屬性指定了哪種類型的bean要引入該接口。(本例中哄酝,就是Performance友存,加號(hào)表示Performance的所有子類型)
- defaultImpl屬性指定了為引入功能提供實(shí)現(xiàn)的類。
- @DeclareParents注解所標(biāo)注的靜態(tài)屬性指明了要引入的接口陶衅。
然后需要在配置中聲明EncoreableIntroducer的bean
@Bean
public EncoreableIntroducer encoreableIntroducer(){
return new EncoreableIntroducer();
}
當(dāng)調(diào)用委托給被代理的bean或被引入的實(shí)現(xiàn)屡立,取決于調(diào)用的方法屬性被代理的bean還是屬性被引入的接口。
在Spring中搀军,注解和自動(dòng)代理提供了一種很便利的方式來創(chuàng)建切面膨俐。但有一個(gè)劣勢(shì):必須能夠?yàn)橥ㄖ愄砑幼⒔猓性创a奕巍。
如果沒有源碼或者不想注解到你的代碼中吟策,能可選擇Spring XML配置文件中聲明切面。
在XML中聲明切面
如果聲明切面的止,但不能為通知類添加注解時(shí)檩坚,需要轉(zhuǎn)向XML配置了。
在Spring的aop命名空間中诅福,提供了多個(gè)元素用來在XML中聲明切面匾委,如下表所示
AOP配置元素 | 用途 |
---|---|
<aop:advisor> | 定義AOP通知器 |
<aop:after> | 定義AOP后置通知(不管被通知的方法是否執(zhí)行成功) |
<aop:after-returning> | 定義AOP返回通知 |
<aop:after-throwing> | 定義AOP異常通知 |
<aop:around> | 定義AOP環(huán)繞通知 |
<aop:aspect> | 定義一個(gè)切面 |
<aop:aspectj-autoproxy> | 啟用@AspectJ注解驅(qū)動(dòng)的切面 |
<aop:before> | 定義AOP前置通知 |
<aop:config> | 頂層的AOP配置元素。大多數(shù)的<aop:*>元素必須包含在<aop:config>元素內(nèi) |
<aop:declare-parents> | 以透明的方式為被通知的對(duì)象引入額外的接口 |
<aop:pointcut> | 定義一個(gè)切點(diǎn) |
我們重新看一下Audience類氓润,這一次我們將它所有的AspectJ注解全部移除掉:
package com.springinaction.perf;
public class Audience {
public void silenceCellPhones(){
System.out.println("Silencing cell phones");
}
public void takeSeats(){
System.out.println("Taking seats");
}
public void applause(){
System.out.println("CLAP CLAP CLAP");
}
public void demandRefund(){
System.out.println("Demanding a refund");
}
public void watchPerformance(ProceedingJoinPoint jp){
try {
System.out.println("Silencing cell phones Again");
System.out.println("Taking seats Again");
jp.proceed();
System.out.println("CLAP CLAP CLAP Again");
}
catch (Throwable e){
System.out.println("Demanding a refund Again");
}
}
}
聲明前置赂乐、后置以及環(huán)繞通知
下面展示了所需要的XML
<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.2.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="audience" class="com.springinaction.perf.Audience" />
<bean id="performance" class="com.springinaction.perf.Concert"/>
<aop:config>
<aop:aspect ref="audience">
<aop:pointcut id="perf" expression="execution(* com.springinaction.perf.Performance.perform(..))" />
<aop:before pointcut-ref="perf" method="silenceCellPhones" />
<aop:before pointcut-ref="perf" method="takeSeats" />
<aop:after-returning pointcut-ref="perf" method="applause" />
<aop:after-throwing pointcut-ref="perf" method="demandRefund"/>
<aop:around pointcut-ref="perf" method="watchPerformance"/>
</aop:aspect>
</aop:config>
</beans>
為通知傳遞參數(shù)
我們使用XML來配置BlankDisc。
首先咖气,移除掉TrackCounter上所有的@AspectJ注解挨措。
package com.springinaction.disc;
import java.util.HashMap;
import java.util.Map;
public class TrackCounter {
private Map<Integer, Integer> trackCounts = new HashMap<>();
// 在播放前,為該磁道計(jì)數(shù)
public void countTrack(int trackNumber){
int currentCount = getPlayCount(trackNumber);
trackCounts.put(trackNumber, currentCount + 1);
}
public int getPlayCount(int trackNumber){
return trackCounts.containsKey(trackNumber) ? trackCounts.get(trackNumber) : 0;
}
}
下面展示了在XML中將TrackCounter配置為參數(shù)化的切面
<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.2.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="trackCounter" class="com.springinaction.disc.TrackCounter" />
<bean id="cd" class="com.springinaction.disc.BlankDisc" >
<property name="title" value="Sgt. Pepper's Lonely Hearts Club Band" />
<property name="artist" value="The Beatles" />
<property name="tracks">
<list>
<value>Sgt. Pepper's Lonely Hearts Club Band</value>
<value>With a Little Help from My Friends</value>
<value>Lucy in the Sky with Diamonds</value>
<value>Getting Better</value>
<value>Fixing a Hole</value>
<value>Feel My Heart</value>
<value>L O V E</value>
</list>
</property>
</bean>
<aop:config>
<aop:aspect ref="trackCounter">
<aop:pointcut id="trackPlayed" expression="
execution(* com.springinaction.disc.CompactDisc.playTrack(int))
and args(trackNumber)" />
<aop:before pointcut-ref="trackPlayed" method="countTrack"/>
</aop:aspect>
</aop:config>
</beans>
注意:在XML中崩溪,“&”符號(hào)會(huì)被解析為實(shí)體的開始浅役,所用“and”關(guān)鍵字。
通過切面引入新的功能
使用Spring aop命名空間中的<aop:declare-parents>元素伶唯,我們可以實(shí)現(xiàn)相同的功能觉既。
<aop:aspect>
<aop:declare-parents types-matching="com.springinaction.perf.Performance+"
implement-interface="com.springinaction.perf.Encoreable"
default-impl="com.springinaction.perf.DefaultEncoreable" />
</aop:aspect>
我們還可以使用delegate-ref屬性來標(biāo)識(shí)
<aop:aspect>
<aop:declare-parents types-matching="com.springinaction.perf.Performance+"
implement-interface="com.springinaction.perf.Encoreable"
delegate-ref="defaultEncoreable" />
</aop:aspect>
delegate-ref屬性引用了一個(gè)Spring bean作為引入的委托。
<bean id="defaultEncoreable" class="com.springinaction.perf.DefaultEncoreable" />
注入AspectJ切面
AspectJ提供了Spring AOP所不能支持的許多類型的切點(diǎn)。例如:構(gòu)造器切點(diǎn)就非常方便瞪讼。
為了演示钧椰,我們新創(chuàng)建一個(gè)切面,我們以切面的方式創(chuàng)建一個(gè)評(píng)論員的角色符欠,演出后提一些批評(píng)意見嫡霞。
首先創(chuàng)建這樣的一個(gè)切面
package com.springinaction.perf;
public aspect CriticAspect {
public CriticAspect(){}
pointcut performance() : execution(* perform(..));
after() returning : performance() {
System.out.println(criticismEngine.getCriticism());
}
private CriticismEngine criticismEngine;
public CriticismEngine getCriticismEngine() {
return criticismEngine;
}
public void setCriticismEngine(CriticismEngine criticismEngine) {
this.criticismEngine = criticismEngine;
}
}
然后是CriticismEngine的接口
package com.springinaction.perf;
public interface CriticismEngine {
String getCriticism();
}
CriticismEngine的實(shí)現(xiàn)類
package com.springinaction.perf;
public class CriticismEngineImpl implements CriticismEngine {
public CriticismEngineImpl(){}
@Override
public String getCriticism() {
int i = (int) (Math.random() * criticismPool.length);
return criticismPool[i];
}
private String[] criticismPool;
public void setCriticismPool(String[] criticismPool){
this.criticismPool = criticismPool;
}
}
為CriticismEngineImpl注入list
<bean id="criticismEngine" class="com.springinaction.perf.CriticismEngineImpl">
<property name="criticismPool">
<list>
<value>Worst performance ever!</value>
<value>I laughed, I cried, then I realized I was at the wrong show.</value>
<value>A must see show!</value>
</list>
</property>
</bean>
在展示如何實(shí)現(xiàn)注入之前,我們必須清楚AspectJ切面根本不需要Spring就可以織入到我們的應(yīng)用中背亥。如果想使用Spring的依賴注入秒际,那就需要把切面聲明為一個(gè)Spring配置中的<bean>。
<bean class="com.springinaction.perf.CriticAspect" factory-method="aspectOf">
<property name="criticismEngine" ref="criticismEngine"/>
</bean>
通常情況下狡汉,Spring bean由Spring容器初始化娄徊,但是AspectJ切面AspectJ在運(yùn)行期創(chuàng)建的。等到Spring有機(jī)會(huì)為CriticAspect注入CriticismEngine時(shí)盾戴,CriticAspect已經(jīng)被實(shí)例化了寄锐。
因?yàn)镾pring不能負(fù)責(zé)創(chuàng)建CriticAspect,那就不能在Spring中簡單地把CriticAspect聲明為一個(gè)bean尖啡。相反橄仆,我們需要一種方式為Spring獲得已經(jīng)有AspectJ創(chuàng)建的CriticAspect實(shí)例的句柄,從而可以注入CriticismEngine衅斩。幸好盆顾,所有AspectJ切面都提供了一個(gè)靜態(tài)的aspectOf()方法,該方法返回切面的一個(gè)單例畏梆。所有為了獲得切面的實(shí)例您宪,我們必須使用factory-method來調(diào)用aspectOf()而不是調(diào)用CriticAspect的構(gòu)造器方法。
簡而言之奠涌,Spring不能像之前那樣使用<bean>聲明來創(chuàng)建一個(gè)CriticAspect實(shí)例-它已經(jīng)在運(yùn)行時(shí)有AspectJ創(chuàng)建完成了宪巨。Spring需要通過aspectOf()工廠方法獲得切面的引用,然后像<bean>元素規(guī)定的那樣在該對(duì)象上執(zhí)行依賴注入溜畅。