天天看點

Spring面向切面程式設計

      第1章主要介紹了Spring管理實體對象的應用,通過ApplicationContext容器來了解Spring管理實體對象的原理以及設值注入、構造方法及自動注入等不同的注入方式。本章先介紹為什麼需要AOP以及使用AOP的好處,然後采用手動代理的方式介紹什麼是代理及代理的必要性,最後結合商場手機進貨和收獲的案例分别介紹前置通知、後置通知、環繞通知和異常通知。在介紹前置通知的時候,分别采用Spring1.x和Spring2.x的方式進行配置,為後續課程的學習打下鋪墊。

核心技能部分

第1章 

通過依賴注入,在編寫程式的時候,我們不必關心依賴的元件如何實作,然而在實際開發過程中我們還需要将程式中涉及的公共問題集中解決,如圖2.1.1所示。

圖2.1.1 Spring的兩個重要子產品

看下面的一個應用:

這是一個典型的業務處理方法。日志、參數合法性驗證、異常處理、事務控制等都是一個健壯的業務系統所必需的;否則系統出現問題或者有錯誤的業務操作時沒有日志可查;傳入的出庫參數為負數,出庫反而導緻庫存增加;轉賬時,打款方的錢已經被扣了,方法卻異常退出,而收款方還沒有收到,已經進行的交易沒有復原。這樣的系統顯然是沒有人敢用的。

為了保證系統健壯可用,就要在每個業務方法裡都反複編寫這些代碼,如果需要修改,則每個業務方法都需要修改,這樣的代碼品質很難保障。

我們怎樣才能把心用在真正的業務邏輯上呢?這就是AOP要解決的問題。

AOP 是Aspect-Oriented Programming的簡稱,意思是面向切面程式設計。AOP是對OOP的補充和完善。比如剛才的問題,程式中所有的業務方法都需要日志記錄、參數驗證、事務處理,這些公共的處理如果放在每個業務方法裡,系統會變的臃腫,而且很難去維護。散布在系統各處的需要在實作業務系統時關注的事情就被成為“切面”,也稱為關注點,AOP的思想就是把這些公共部分從業務方法中提取出來,集中處理。

編寫代碼的時候,“切面”代碼并不放在業務方法中,但是程式運作的時候,Spring會攔截到方法的執行,并運作這些“切面”代碼。

OOP 引人了封裝、繼承及多态性等概念來建立對象層次結構,用于模拟公共行為的集合。在OOP思想中,代碼的重複可以被提取出來放在父類中被複用。但是方法中的重複代碼,OOP卻無能為力。例如,在日志資訊記錄中,日志代碼常水準地散布在所有對象層次中,與對象的核心功能毫無關系。對于其他類型的代碼,如安全性、異常處理以及透明的持續性也同樣如此。散布在各處且無關的代碼稱為橫切 (Cross-cutting) 代碼,這種代碼在OOP 的設計中導緻了大量重複且不利于各個子產品重用。

AOP技術則剛好相反,它代表橫向關系,通過利用“橫切”技術解剖封裝的對象并獲得其内部,将影響多個類的公共行為封裝至可重用子產品并命名為“Aspect”(切面)。簡而言之,即 AOP技術将與業務無關卻由業務子產品共同調用的邏輯進行封裝,減少系統的重複代碼,降低子產品間的耦合度,提高系統的可操作性及可維護性。若将“對象”視為空心的圓柱體,将屬性與行為都封裝于柱體中,則 AOP技術的作用類似于刀,将空心圓柱體剖開并獲得内部的消息,剖開的面即“切面”。

通過使用“橫切”技術,AOP将軟體系統分為兩個部分:核心關注點與橫切關注點。業務處理的主要流程即核心關注點,與之關系較小的部分就是橫切關注點。橫切關注點常發生在核心關注點的多處且各處基本相似,如權限認證、日志、事務處理等。AOP 的作用在于分離系統中的各種關注點,将核心關注點與橫切關注點進行分離,其核心思想是将應用程式中的商業邏輯與向其提供支援的通用服務進行分離。

AOP 中包含許多新的概念與術語,說明如下:

(1)切面 (Aspect):切面是系統中抽象出來的的某一個功能子產品。

(2)連接配接點 (Joinpoint):程式執行過程中某個特定的點,如調用某方法或者處理異常時。在SpringAOP中,連接配接點總是代表某個方法的執行。

(3)通知 (Advice):通知是切面的具體實作,是放置“切面代碼”的類。

(4)切入點 (Pointcut):多個連接配接點組成一個切入點,可以使用切入點表達式來表示。

(5)目标對象 (Target Object):包含一個連接配接點的對象,即被攔截的對象。

(6)AOP代理 (AOP Proxy):AOP架構産生的對象,是通知和目标對象的結合體。

(7)織入 (Weaving):将切入點和通知結合的過程稱為織入。

在生活中,“代理”一詞出現的頻率較高,并且現實事物能夠形象、直覺地反映代理模式的抽象過程及本質。下面我們以購買手機和出售房屋為例,初步分析“代理”的概念。

衆所周知,手機或電腦都可以直接從廠家購買,但是這種方式卻很少使用。因為一般情況下,顧客都希望在購買時獲得額外的儲值卡、滑鼠等,廠家卻很少提供,但是顧客可以在代理商處獲得此類額外物件。房屋買賣中同樣存在類似情況:房屋待售時,屋主可以通過網際網路釋出出售資訊尋找買家,同咨詢者看房、洽談、交易、過戶以實作成交,需要占用賣家大量時間;此外還可以交給中介管理,由其處理瑣碎的交易過程。實際上,中介即賣家的代理。

接下來我們看一個需求,某手機商店需要購買手機和銷售手機,現要求購買和銷售手機時記錄日志。

根據需求我們知道,本系統有兩個業務操作:購買手機和銷售手機。兩個業務方法都用着相同的日志操作,是以日志操作為該系統的“切面”代碼,我們可以将這些代碼從業務方法中分離出來。我們可以定義一個手機業務接口、一個手機業務接口實作和一個日志操作類。手機接口和手機接口的實作如示例2.1所示。

示例2.1

日志操作類如示例2.2所示。

示例2.2

接下來我們編寫一個測試類,對系統的業務方法進行測試,測試代碼如示例2.3所示。

示例2.3

上面的測試結果可以實作手機的進貨和銷售,但是日志記錄卻無法實作,原因是目标對象的方法沒有日志相關的代碼(日志代碼從業務方法中提取出去了),那麼如何能在程式運作過程中加入“切面”代碼(日志)呢?這個時候,代理的作用就展現出來了,我們可以設計一個針對目标對象的代理。手機業務代理類如示例2.4所示。

示例2.4

通過上面的代碼我們知道,該代理類中包含兩個重要屬性:目标對象和“切面對象”。

要想把公共代碼提取出來,又在業務方法運作的時候加上公共代碼,我們可以調用代理對象的方法。測試代碼如示例2.5所示。

示例2.5

綜上,代理通常可以作為目标對象的替代品,且提供更加強大的功能。代理包含目标Bean的所有方法,代理就是目标對象的加強,通過以目标對象為基礎來增加屬性與方法,提供更加強大的功能。AOP代理是由 AOP架構建立的對象。

如果在Spring項目中使用AOP,則需要在Spring項目中添加Spring Libraries類庫。

本章繼續以2.2節中手機商店的進貨和銷售業務為例,以日志管理為需求,講解Spring AOP技術的具體使用方法。

首先我們需要在Spring應用容器配置檔案中引入Spring AOP命名空間,否則無法在Spring容器配置檔案中使用AOP相關xml标簽。如示例2.6所示,将其中加粗部分代碼,添加在你的applicationContext.xml中即可:

示例2.6 

包含業務關注點(本例是需要日志管理的點)的對象,稱為目标對象。在本例中,我們的目标對象是手機業務對象,即s3spring.ch2.biz.impl.PhoneBizImpl類的對象,它實作s3spring.ch2.biz.PhoneBiz接口,我們需要在applicationContext.xml中将其配置成Spring bean,如示例2.7所示:

示例2.7

<bean id="phoneBiz" class="s3spring.ch2.biz.impl.PhoneBizImpl"></bean>

程式執行過程中的某個業務關注點(本例是需要日志管理的點)被稱作連接配接點(JoinPoint)。在Spring AOP中,連接配接點總是代表某個方法的執行。在本例中PhoneBizImpl類的buyPhone(int)方法和salePhone(int)的執行都是連接配接點。

多個連接配接點組成一個切入點。我們用切入點表達式來描述多個連接配接點的集合。為了選擇方法,我們可以使用下面這些條件來構造切入點表達式:

l 切入點訓示符:如execution,最常用的切入點訓示符,表示方法的執行。

l 布爾運算符:AND(&&)、OR(||)和NOT(!),可以将多個表達式組合成一個新的表達式來縮小選擇連接配接點的範圍。

l 通配符:星号(*),用于比對任何方法、類名或者參數類型。雙點号(..),用于表示0個或者多個參數類型。

l 方法可見性修飾符:将選擇連接配接點的範圍縮小到某種可見性的方法。如public的。

l 方法傳回類型:如void、int、String等,也可以使用*,表示所有類型。

l 類名:指定完整的類名(包括完整的包名稱,如java.lang.String),将選擇連接配接點的範圍縮小到某個目标類。

l 方法名稱:可以是全名。也可以是*(任何名稱)。也可以是部分名稱結合通配符,如get*,即所有名稱以get開頭的方法。

l 方法聲明抛出的異常:如 throws java.lang.IOException。

使用execution切入點訓示符描述連接配接點範圍時,除了方法傳回類型、方法名稱和方法參數是必須描述的以外, 其它所有的部分都是可選的(方法可見性修飾符、類名、方法聲明抛出的異常)。

示例2.8表達式描述了PhoneBizImpl類名字以Phone結尾,參數隻有一個且類型為int,傳回類型為void,可見性為public的方法的執行,當然也包括了buyPhone(int)方法和salePhone(int)方法。

示例2.8  

execution( public void s3spring.ch2.biz.impl.PhoneBizImpl.*Phone(int) )

如果想描述所有業務類的所有業務方法,可以象示例2.9這樣描述:

示例2.9

execution(* s3spring.ch2.biz.impl.*.* (..) )

示例2.9中第一個*表示任意傳回類型。第二個*表示任意類名。第三個*表示任意方法名稱。..表示0或多個任意類型的方法參數。

示例2.9也可以用within切入點訓示符進行簡化。within表示某一範圍内的連接配接點,如某包内,某類内。示例2.10表示s3spring.ch2.biz.impl包中所有的連接配接點:

示例2.10

within( s3spring.ch2.biz.impl.* )

示例2.11表示s3spring.ch2.biz.impl.PhoneBizImpl類中所有的連接配接點:

示例2.11

within( s3spring.ch2.biz.impl.PhoneBizImpl )

示例2.12表示s3spring.ch2.biz.impl 包及其所有子孫包中所有的連接配接點:

示例2.12

within( s3spring.ch2.biz.impl.PhoneBizImpl..* )

切面是系統中抽象出來的的某一個系統服務功能子產品。在本例中是日志管理子產品,它可以保含多個實作日志管理操作的通知。我們用一個POJO (Plain Old Java Ojbect,簡單普通的Java對象)類來表示抽象的切面,用方法表示通知,并把切面類配置成一個Spring bean。示例2.13 是日志管理切面類,目前它還沒有包含任何通知:

示例2.13

package s3spring.ch2.log;

public class LogAspect {

}

此時,LogAspect僅僅是一個類,還不能代表一個切面,我們接着需要把它配置成一個Spring bean,請看示例:

示例2.14

<bean id="logAspectBean" class="s3spring.ch2.log.LogAspect"></bean>

然後,我們可以利用示例2.6 引入的Spring AOP命名空間将“logAspectBean” bean 配置成一個切面。請看示例2.15:

示例2.15

<aop:config>

     <!-- 定義一個可以被多個切面共享的切入點 -->

<aop:pointcut id="p1" expression="execution( void *Phone(int))"/>

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

<aop:aspect id="logAspect" ref="logAspectBean"></aop:aspect>

</aop:config>

如示例2.15所示,切面、切入點等AOP相關内容必須定義在<aop:config>元素内部,且可以配置多個切面、多個切入點。

我們首先使用<aop:pointcut>标簽定義了一個id為p1的切入點,這個切入點的表達式描述了日志管理所關注的連接配接點——所有傳回類型為void,名稱以Phone結尾,參數隻有一個且類型為int的方法。

将切入點直接定義在<aop:config>元素内部,而不是<aop:aspect>标簽内部的好處是它可以被多個切面共享。例如該切入點即被日志管理子產品關注,又被事務管理子產品關注。當然,你也可以将關注點定義在某個<aop:aspect>标簽内部,這時該切入點就成為這個切面内部獨享的私有切入點了。

最後,我們用<aop:aspect>标簽,通過ref屬性指定id為 “logAspectBean”的bean為一個切面,并為該切面設定id為“logAspect”。

通知(Advice)是在切面的某個特定的連接配接點上執行的具體操作。按照執行的時機可以分為下面幾種:

l 前置通知(Before):在某連接配接點之前執行的通知,它可以阻止連接配接點的執行。例如檢查權限,決定是否執行連接配接點。

l 後置通知(AfterReturning):在某連接配接點正常完成後執行的通知:例如,一個方法沒有抛出任何異常,正常傳回。它可以通路方法傳回值。

l 異常通知(AfterThrowing):在方法抛出異常退出時執行的通知。它可以通路抛出的異常。

l 最終通知(After ):當某連接配接點退出的時候執行的通知(不論是正常傳回還是異常退出)。 這是傳統spring AOP中沒有的新的通知類型。它不能通路傳回值或抛出的異常。可以用來執行釋放資源,日志記錄等操作。

l 環繞通知(Around):也叫方法攔截器。包圍一個連接配接點的通知。這是最強大的一種通知類型。環繞通知可以在方法調用前後完成自定義的行為。它也會選擇是否繼續執行連接配接點或直接傳回它自己的傳回值或抛出異常來結束執行。 可用于實作性能測試,事物管理等。

前置通知是在目标方法被調用之前織入的通知。即當目标方法被調用且執行之前,先執行的系統服務業務邏輯,如執行日志記錄,參數檢查,權限限制等。

示例2.16将展示利用Spring的前置通知實作在手機進貨和銷售手機執行之前記錄記錄檔。示例2.1中的業務接口PhoneBiz和實作類PhoneBizImpl的代碼不需要做任何修改,代理對象由Spring自動産生。我們隻需要在示例2.13中建立的LogAspect切面類内部定義一個方法實作目标業務方法執行之前要進行的日志記錄操作,如示例2.16所示。

示例2.16 

如示例2.16所示,before方法擁有一個參數叫做jp, 類型為JoinPoint,即連接配接點。這個參數是可選的。JoinPoint對象提供了如下方法以獲得連接配接點(在spring中一般是方法的執行)的一些有用資訊:

l Object[] getArgs():以對象數組的形式傳回所有方法參數

l Signature getSignature():傳回方法簽名,通過Signature的getName()方法可以得到方法名稱;getModifiers()方法可以得到方法修飾符。

l String getKind():傳回目前連接配接點的類型,如:“method-execution”,方法執行。

l Object getTarget():傳回連接配接點所在的目标對象。

l Object getThis():傳回AOP自動建立的代理對象。

此時before方法雖然實作了一個日志管理前置通知的業務邏輯,但是它還是一個普通的方法,我們需要把這個方法定義成一個前置通知,并和示例2.15中定義的切入點p1關聯起來,請看示例2.17中<aop:aspect>标簽内的粗體部分代碼:

示例2.17

<aop:before>标簽用于定義一個前置通知。method屬性指定實作通知的方法。pointcut-ref屬性指定該通知關聯的切入點。一個切面(<aop:aspect>)内可以包含多個不同類型的通知。

到這裡,我們的前置通知的示例就完成了。示例2.18是完整的配置:

示例2.18

接下來我們來測試一下使用前置通知實作的日志管理操作的效果,示例2.19是測試代碼:

示例2.19

示例2.19執行輸出結果:

2012年07月18日 18:02:45 即将執行進貨操作,數量為100

手機進貨,進貨數量 為100部

2012年07月18日 18:02:45 即将執行銷售操作,數量為88

銷售手機,銷售數量為88部

觀察輸出結果我們可以看出,在業務方法執行之前,前置通知成功織入連接配接點,先進行了日志輸出。實際上,我們通過Spring應用容器獲得的并不是真正的PhoneBizImpl類型的對象,而是Spring自動根據PhoneBizImpl 類實作的PhoneBiz接口建立的代理對象。示例2.20可以證明着一點:

示例2.20

示例2.20粗體部分代碼調用了pBiz的getClass()方法傳回該對象的類型描述對象(一個Class類型的對象),并進一步調用Class對象的getName方法擷取類名稱。輸出結果是:

$Proxy4

而不是bean對象的類型:

s3spring.ch2.biz.impl.PhoneBizImpl

這足以說明我們獲得的并不是一個 PhoneBizImpl 類的對象,而是一個代理對象,而且該代理對象實作了PhoneBiz接口。所有此時你不能使用PhoneBizImpl類型的變量引用代理對象,請看示例2.21:

示例2.21

ApplicationContext ac =

new ClassPathXmlApplicationContext("applicationContext.xml");

//建立代理對象

PhoneBizImpl pBiz = (PhoneBizImpl)ac.getBean("phoneBiz");

//輸出bean類型名稱

System.out.println(pBiz.getClass().getName());

示例2.21将引用代理對象的變量類型從PhoneBiz改成PhoneBizImpl,執行時将抛出以下異常資訊:

java.lang.ClassCastException: $Proxy4 cannot be cast to s3spring.ch2.biz.impl.PhoneBizImpl

示例2.21抛出的異常資訊描述的很清楚:$Proxy4不能被轉換成PhoneBizImpl。那麼如果我們的目标對象沒有實作接口怎麼辦呢?讓我們修改PhoneBizImpl 類的定義,使其不實作任何接口,請看示例2.22:

示例2.22

public class PhoneBizImpl

此時再次運作示例2.20,程式能夠正常運作,輸出代理對象類型資訊如下:

s3spring.ch2.biz.impl.PhoneBizImpl$$EnhancerByCGLIB$$ffeaaf7b

實際上,Spring AOP 建立代理分為兩種情況:

l 如果被代理的目标對象實作了至少一個接口,Spring會使用針對接口産生代理的Java SE動态代理(Java直接提供的動态代理API)。目标對象類型實作的所有接口都将被代理,包括業務接口之外的接口。 注意,Java SE 動态代理隻能針對接口産生代理,是以目标對象必須至少實作了一個接口。建議優先使用Java SE 動态代理。

l 對于需要直接代理類而不是代理接口的時候,Spring也可以使用CGLIB(Code Generation Library)代理。如果一個業務對象并沒有實作任何接口,Spring就會使用CGLIB在運作時動态生成目标對象的子類對象來作為代理對象。就算目标對象實作了接口,你也可以強制使用CGLIB代理,例如:希望代理目标對象的所有方法,而不隻是實作自接口的方法;或當目标對象沒有實作有用的業務接口,而隻是實作了一些輔助工具接口(如果此時針對接口産生代理,則代理對象僅擁有這些輔助工具接口定義的方法,沒有業務方法)。我們隻需要将<aop:config>标簽的proxy-target-class屬性置為"true"即可強制使用CGLIB針對類産生代理,請看示例:

示例2.23

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

         ……

後置通知是在目标方法調用之後織入通知,即在方法正常退出傳回值之後且傳回調用地點之前進行織入。

示例2.24是一個後置通知實作,也位于示例2.13的LogAspect切面類中,作用是在業務方法調用結束之後進行日志記錄。

示例2.24

在配置檔案中<aop:aspect>标簽内增加如示例2.25粗體部配置設定置内容,将後置通知織入切入點p1。

示例2.25

不修改示例2.19測試代碼,測試結果如下所示,粗體部分為後置通知輸出的日志資訊:

2012年07月20日 15:04:56即将執行進貨操作,數量為100

2012年07月20日 15:04:56進貨操作執行完畢...

2012年07月20日 15:04:56即将執行銷售操作,數量為88

2012年07月20日 15:04:56銷售操作執行完畢...

如果連接配接點抛出異常,異常通知(throws advice)将在連接配接點異常退出後被調用。示例2.26是一個自定義業務異常,表示缺貨。

示例2.26

package s3spring.ch2.exception;

/** 缺貨異常*/

public class OutOfStockException extends Exception{

public OutOfStockException(String msg) {

super(msg);

修改示例2.1,為 PhoneBizImpl 類添加一個num屬性代表庫存。在salePhone()方法中判斷如果銷售量高于庫存量,則抛出OutOfStockException,請看示例2.27粗體部分代碼:

示例2.27

在LogAspect切面類添加一個實作異常通知的方法,作用是在業務方法異常退出之後進行日志記錄。OutOfStockException類型的參數e用于接收目标方法抛出的OutOfStockException類型異常,以便在通知内處理該異常。如果目标方法抛出的是其它異常,比如空指針異常,則示例2.28中異常通知方法不會被執行:

示例2.28

在配置檔案中<aop:aspect>标簽内增加如示例2.29粗體部配置設定置内容,将afterThrowing方法作為異常通知織入切入點p1,将throwing屬性的值指定為afterThrowing方法OutOfStockException類型參數的名稱“e”:

示例2.29

2012年07月20日 21:19:10即将執行進貨操作,數量為100

2012年07月20日 21:19:10進貨操作執行完畢...

2012年07月20日 21:19:10即将執行銷售操作,數量為120

2012年07月20日 21:19:10salePhone方法執行,發生缺貨異常:貨存不足,客戶需要120部手機,庫存隻有100部

觀察執行結果你會發現,當目标方法發生缺貨異常時異常通知正确執行,但是後置通知沒有執行。因為後置通知隻在目标方法正常執行結束時執行。如果我們希望能夠在目标方法抛出異常之後,像try、catch、finally結構一樣,除了是用異常通知處理異常外,還可以通過某種通知去完成類似finally的任務,即無論目标方法是否抛出異常,該通知都一定會執行。這就是我們繼續要學習的最終通知。

最終通知是無論目标方法異常退出,還是正常退出都一定會執行的通知。在LogAspect切面類中添加如示例2.30所示方法。

示例2.30 最終通知

在配置檔案中<aop:aspect>标簽内增加如示例2.31粗體部配置設定置内容,将after方法作為最終通知織入切入點p1。

示例2.31最終通知配置

2012年07月25日 15:55:32即将執行進貨操作,數量為100

2012年07月25日 15:55:32進貨操作執行完畢...

2012年07月25日 15:55:32進貨操作執行完畢,發生異常也要執行的最終通知...

2012年07月25日 15:55:32即将執行銷售操作,數量為120

銷售手機,銷售數量為120部

2012年07月25日 15:55:32銷售操作執行完畢...

2012年07月25日 15:55:32銷售操作執行完畢,發生異常也要執行的最終通知...

環繞 (Around)通知是最強大的通知類型,它能夠代替之前所有通知類型,在連接配接點的前後執行,擷取方法入參、傳回值,捕捉并處理異常。下面我們就以性能測試為需求,針對業務層編寫環繞通知,計算業務方法執行耗費的時間長短。在LogAspect切面類中添加如示例2.32所示性能測試環繞通知方法實作。

示例2.32 

環繞通知的實作方法必須包含一個連接配接點入參,但是其類型與其它類型的通知不同,為ProceedingJoinPoint。ProceedingJoinPoint對象代表了通知織入的目前連接配接點,調用其proceed()方法就會執行目标方法,proceed()方法的傳回值就是目标方法的傳回值,proceed()方法抛出的異常就是目标方法抛出的異常;如果你不想執行目标方法,隻要不執行proceed()方法即可。

在配置檔案中<aop:aspect>标簽内增加如示例2.33粗體部配置設定置内容,将aroundTest方法作為環繞通知織入切入點p1。

示例2.33 

……省略部分日志

2012年07月25日 10:58:41:buyPhone方法開始執行,計時開始!

2012年07月25日 10:58:41:buyPhone方法執行完畢執行完畢,耗時31毫秒

2012年07月25日 10:58:41:salePhone方法開始執行,計時開始!

2012年07月25日 10:58:41:salePhone方法執行完畢執行完畢,耗時0毫秒

我們也可以使用注解來進行AOP程式設計的配置。兩種方式原理和概念不變,僅僅是配置形式不同罷了。下面我們就在2.3小節示例項目的基礎之上将xml配置方式改為注解配置方式。

首先建立一個新包s3spring.ch2.log.annotation,然後将預設包下的applicationContext.xml和s3spring.ch2.log.LogAspect類複制到該包下。

修改s3spring.ch2.log.annotation.applicationContext.xml中代碼為示例2.34所示:

示例2.34

注解配置方式和XML配置方式相同的是也需要在Spring應用容器的配置檔案applicationContext.xml中引入AOP命名空間,不同的是我們采用注解來代替<aop:config >、<aop:pointcut>、<aop:aspect>、<aop:before>等等這些标簽。

另外,我們需要用<aop:aspectj-autoproxy />标簽來告訴spring應用容器啟用AOP注解配置,Spring應用容器就會在初始化時掃描所有的bean,以找出應用AOP注解配置的切面bean。

id為“phoneBiz”的bean是目标業務對象,其實作類是示例2.1中的s3spring.ch2.biz.impl.PhoneBizImpl,代碼和配置無需任何修改。

s3spring.ch2.log.annotation.LogAspect類作為日志管理切面的實作類被配置成bean,在類聲明上方添加@Aspect注解即可将它聲明為切面。請看示例:

示例2.35 

package s3spring.ch2.log.annotation;

@Aspect

public class LogAspect {……}

spring為我們提供了如下幾個注解來幫助我們配置不同類型的通知:

l @Before,前置通知

l @AfterReturning,後置通知

l @AfterThrowing,異常通知

l @After,最終通知

l @Around,環繞通知

在方法聲明上方使用以上注解即可将方法聲明為相應類型的通知,再為注解指定切入點表達式即可将通知織入該切入點。示例2.36示範了@Before、@AfterReturning、 @After和@Around注解的使用,請看示例2.36中粗體部分代碼:

示例2.36

/** 前置通知 在目标方法執行之前執行日志記錄 */

@Before("execution( void *Phone(int))")

public void before(JoinPoint jp) throws Throwable {  ……  }

/** 後置通知 在目标方法正常退出時執行日志記錄 */

@AfterReturning("execution( void *Phone(int))")

public void afterReturning(JoinPoint jp) throws Throwable { …… }

/** 最終通知 無論目标方法正常退出還是異常退出都執行日志記錄 */

@After("execution( void *Phone(int))")

    public void after(JoinPoint jp) throws Throwable { …… }

/** 環繞通知 */

@Around("execution( void *Phone(int))")

     ……省略異常通知

用@AfterThrowing注解配置異常通知除了需要指定切入點外還需要根據方法參數名稱綁定異常對象,請看示例2.37中粗體部分代碼:

示例2.37

     ……

/** 異常通知 在目标方法抛出參數指定類型異常時執行 */

@AfterThrowing(pointcut="execution( void *Phone(int))",throwing="e")

public void afterThrowing(JoinPoint jp,OutOfStockException e) {

……

@AfterThrowing注解的pointcut配置項用于指定切入點,Throwing配置項用于指定方法中表示異常對象的參數名稱,這樣可以将目标方法抛出的異常對象綁定到同類型叫e的方法參數。

在示例2.36小節中所有通知織入的其實都是同一個切入點,但是卻多次重複編寫相同的切入點表達式。下面讓我們用@Pointcut注解結合切入點表達式在LogAspect類中定義一個切入點,并将before通知織入該切入點,請看示例2.38:

示例2.38

/** 切入點 */

@Pointcut("execution( void *Phone(int))")

public void p1(){}

@Before("s3spring.ch2.log.annotation.LogAspect.p1()")

上例中用一個叫做p1的空方法來表示一個切入點,當我們希望将通知織入該切入點時,在注解中用方法簽名來代替切入點表達式即可。由于切入點的聲明和通知的聲明在同一個類中,可以省略包路徑和類名,示例可以簡寫為示例2.39粗體部分代碼:

示例2.39

@Before("p1()")

訓練技能點

Ø Spring AOP前置通知

Ø AOP的配置

需求說明

某商場進行電冰箱促銷活動,規定每位顧客隻能購買一台特價電冰箱。顧客在購買電冰箱之前輸出歡迎資訊,顧客如果購買多台特價電冰箱,請給出錯誤提示,顧客成功購買電冰箱之後輸出歡送資訊。請使用Spring面向切面程式設計實作該需求的顧客歡迎資訊顯示。

實作思路

(1) 定義出售電冰箱的接口和接口實作。

(2) 定義前置通知。

(3) 編寫配置檔案。

關鍵代碼

(1) 定義缺貨異常

public class NoThisFrigException extends Exception {

public NoThisFrigException(String msg) {

(2) 定義顧客隻能購買一台特價電冰箱的異常

public class BuyFrigException extends Exception {

public BuyFrigException(String msg)

{

(3)  定義電冰箱業務接口。

public interface FrigBiz {//出售電冰箱接口

public void buyFrig(String customer,String frig) throws NoThisFrigException;

(4) 定義電冰箱接口的實作類。

public class FrigBizImpl implements FrigBiz {

public void buyFrig(String customer, String frig) throws NoThisFrigException {

  if ("美的".equals(frig)) {

throw new NoThisFrigException("對不起,沒有" + frig + "的貨了");

        }

System.out.println("您好,您已經購買了一台" + frig);

(5) 定義前置通知,實作歡迎顧客的資訊

public class FrigBefore {

public void before(Joinpoint jp) throws Throwable {

// 通過Joinpoint獲得目标方法傳入的參數customer值

   String customer = (String) jp.getArgs()[0];// 取得第一個參數,客戶名稱

   // 顯示歡迎資訊,在buyFrig方法前調用

    System.out.println("歡迎光臨!" + customer + "!");

(6) 編寫配置檔案,使用Spring2.x方式配置

 <!-- 配置出售電冰箱的實作類 -->

<bean id="frigBiz" class="bean.FrigBizImpl" />

<!-- 配置前置通知 -->

<bean id="frigBefore" class="utils.FrigBefore" />

<!-- 配置代理對象 -->

<bean id="frigBizProxy" <!-- AOP配置 -->

<aop:pointcut id="p1"

expression="execution( void buyFrig(String,String))"/>

<aop:aspect id="logAspect" ref="frigBefore">

         <!-- 定義一個前置通知 -->

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

</aop:aspect>

(7) 編寫測試代碼測試。

public static void main(String[] args) throws NoThisFrigException {

ApplicationContext context=new ClassPathXmlApplicationContext("applicationContext.xml");

FrigBiz frigBiz=(FrigBiz)context.getBean("frigBizProxy");

frigBiz.buyFrig("張無忌", "Lg");

Ø Spring AOP後置通知

實訓任務1的基礎上完善系統,當電冰箱賣出之後,顯示“歡迎下次再來”。

(1) 編寫後置通知FrigAfter。

(2) 在配置檔案中增加對後置通知的配置。

(3) 編寫測試代碼。

Ø Spring AOP環繞通知

Ø Spring2.x的AOP配置

每位顧客隻能購買一台特價電冰箱,已經購買電冰箱的顧客如果重複購買,則給出顧客限購的提示。

(1) 定義環繞通知。

(2) 使用Spring2.x方式配置。

(1) 定義環繞通知FrigAround。

public class FrigAround {

private Set<String> customers = new HashSet<String>();

public Object around(ProceedingJoinPoint pjp) throws Throwable {

Object[] args = pjp.getArgs();// 目标方法所有參數

String customer = (String)args[0];

String frig = (String) args[1];

if (customers.contains(customer)) {

throw new BuyFrigException("對不起,一名顧客隻能購買一台特價電冰箱!您已購買一台特價" + frig);

try {

          return pjp.proceed();

       } finally{

customers.add(customer);

       }

(2) 建立配置檔案aop.xml,配置環繞通知。

<!-- 配置出售電冰箱的實作類 -->

<!-- 配置環繞通知 -->

<bean id="frigAround" class="utils.FrigAround" />

<aop:aspect id="frigAspect" ref="frigAround">

         <!-- 定義一個後置通知 -->

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

Ø Spring AOP異常通知

更新商場購物,如果電冰箱庫存不足,給出訂貨提示。

(1) 編寫異常通知FrigThrows。

(2) 在aop.xml中增加異常通知的配置。

(1) 編寫異常通知。

public class FrigThrows{

public void afterThrowing(NoThisFrigException e) throws Throwable {

System.out.println("通知倉庫,趕緊訂貨!");

(2) 在aop.xml中增加FrigThrows的配置。

<bean id="frigThrows" class="utils.FrigThrows" />

<aop:aspect id="frigAspect" ref="frigThrows">

         <!-- 定義一個異常通知 -->

         <aop:after-throwing method="afterThrowing" pointcut-ref="p1" />

Ø Spring AOP注解配置

一.選擇題

1. Spring通知不包括 ()。

    A. 前置通知

    B. 後置通知

    C. 環繞通知

    D. 設定通知

2. 以下對AOP 的說法中,錯誤的是 ()。

    A. AOP将散落在系統中的“方面”代碼集中實作

    B. AOP有助于提高系統的可維護性

    C. AOP可以取代OOP

    D. AOP能大大簡化程式的代碼

3. 下列關于Spring AOP的說法錯誤的是 ()。

    A. 可支援前置通知、後置通知、環繞通知

    B. Spring AOP采用攔截方法調用的方式實作,可以在調用方法前、調用後、抛出異常時攔截

    C. Spring AOP采用代理的方式實作

    D. 采用Spring2.x方式配置的時候,不會産生AOP代理

4. 在 Spring架構中,面向方面程式設計 (AOP)的目标在于 ()。

    A. 編寫程式時無須關注其依賴元件的實作

    B. 封裝JDBC通路資料庫的代碼,簡化資料通路層的重複性代碼

    C. 将程式中涉及的公共問題集中解決

    D. 可以通過Web服務調用

5.下面關于 Spring AOP 錯誤的是 ()。

A. 任何一個通知的實作發那個發都可以以JoinPoint或ProceedingJoinPoint為

參數

B. 隻有環繞通知可以使用ProceedingJoinPoint為入參,其它類型通知隻能使用

JoinPoint為入參

C. JoinPoint和ProceedingJoinPoint都有proceed()方法,用于執行目标發方

D. 僅ProceedingJoinPoint有proceed()方法

6. 以下那一個注腳不是用于定義通知的? ()。

    A. @After

    B. @Before

    C. @Aspect

    D. @AfterThorwing

二.操作題

1.在第一章操作題第1題的基礎上實作以下功能。

現舉行活動,更新指環的話,可以免費将任意指環更新為“紫色夢幻”指環,新的裝備名稱為“紫色夢幻+原指環名”,而且将在原有的基礎上再加6點攻擊,6點防禦。

提示:使用前置通知,判斷要更新的裝備類型是否為指環,如果是則按照要求修改傳入參數的名稱以及攻擊增效和防禦增效的屬性。

2.在電子商務網站購物時,需要對生成訂單的過程進行日志記錄,以便于查詢交易過程。該系統有一個生成訂單的業務,請使用AOP方式實作在交易方法調用時記錄客戶資訊及生成訂單的時間。

3.在銀行管理系統中,轉賬是很重要的操作,如張三向李四的賬戶轉賬1000元,需要經過的步驟是:

(1)修改張三賬戶資訊,從賬戶中扣除1000元。

(2)修改李四賬戶資訊,在李四賬戶中增加1000元錢。

(3)向交易記錄表中增加一條記錄。

以上三步操作必須運作在同一事務,而且任何一步出現異常,事務必須復原。請使用SpringAOP實作事務控制。

提示:

在BankBiz中添加一個轉賬方法,該方法實作以上三步操作,然後編寫一個前置通知、後置通知和異常通知。在前置通知中開啟事務,在後置通知中送出事務,在異常通知中進行事務復原。

開啟事務的方法:

HibernateSessionFactory.getSession().beginTransaction();

送出事務的方法:

HibernateSessionFactory.getSession().beginTransaction().commit();

事務復原的方法:

HibernateSessionFactory.getSession().beginTransaction().rollback();

4.更新“會員賬戶管理系統”,使用Spring的面向切面程式設計為會員狀态的更改、會員充值、會員資訊的删除增加日志,日志要求:

(1)會員狀态的更改,需要記錄被更改狀态的會員的編号和更改時間。

(2)會員充值的時候,如果充值失敗,記錄失敗原因及充值操作時間。如果充值成功,記錄被充值會員的編号,充值時間和充值金額。

(3)會員資訊的删除,需要記錄被删除的會員編号、删除時間。

(4)日志資訊在控制台輸出。

上一篇: 虛構函數

繼續閱讀