一.面向切面程式設計
Spring的基礎是IOC和AOP,前面兩節對IOC和DI做了簡單總結,這裡再對AOP進行一個學習總結,Spring基礎就算有一個初步了解了。
在軟體開發中,我們可能需要一些跟業務無關但是又必須做的東西,比如日志,事務等,這些分布于應用中多處的功能被稱為橫切關注點,通常橫切關注點從概念上是與應用的業務邏輯相分離的。如何将這些橫切關注點與業務邏輯在代碼層面進行分離,是面向切面程式設計(AOP)所要解決的。
橫切關注點可以被描述為影響應用多處的功能,切面能夠幫助我們子產品化橫切關注點。下圖直覺呈現了橫切關注點的概念:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsICMyYTMvw1dvwlMvwlM3VWaWV2Zh1Wa-cmbw5CZyEmcn5Wd41mdvwVO4UDOzAjMtUGall3LcVmdhNXLwRHdo9CXt92YucWbpRWdvx2Yx5yazF2Lc9CX6MHc0RHaiojIsJye.png)
途中CourseService,StudentService,MiscService都需要類似安全、事務這樣的輔助功能,這些輔助功能就被稱為橫切關注點。
繼承和委托是最常見的實作重用通用功能的面向對象技術。但是如果在整個程式中使用相同的基類繼承往往會導緻一個脆弱的對象體系;而使用委托可能需要對委托對象進行複雜的調用。
切面提供了取代繼承和委托的另一種選擇,而且更加清晰簡潔。在面向切面程式設計時,我們任然在一個地方定義通用功能,但是我們可以通過聲明的方式定義這個功能以何種方式在何處應用,而無需修改受影響的類,受影響類完全感受不到切面的存在。
二.AOP常用術語
下面是AOP中常用的名詞。
1. 通知(Advice)
通知定義了切面是什麼以及何時使用。出了描述切面要完成的工作,通知還解決了何時執行這個工作的問題。Sping切面可以應用以下5種類型的通知。
- Before 在方法被調用之前調用通知
- After 在方法完成之後調用通知,無論方法執行是否成功
- After-returning 在方法成功執行後調用通知
- After-throwing 在方法抛出異常後調用通知
- Around 通知包裹了被通知的方法,在被通知的方法調用前和調用後執行
2.連接配接點(Joinpoint)
應用可能有很多個時機應用通知,這些時機被稱為連接配接點。連接配接點是應用在執行過程中能夠插入切面的一個點,這個點可以是調用方法時、抛出異常時、甚至是修改字段時。切面代碼可以利用這些切入到應用的正常流程中,并添加新的行為。
3.切點(Pointcut)
切點定義了通知所要織入的一個或多個連接配接點。如果說通知定義了切面的“什麼”和“何時”,那麼切點就定義了“何處”。通常使用明确的類和方法名稱來指定切點,或者利用正規表達式定義比對的類和方法來指定這些切點。有些AOP架構允許我們建立動态的切點,可以更具運作時的政策來決定是否應用通知。
4.切面(Aspect)
切面是通知和切點的結合。通知和切點定義了關于切面的全部内容,是什麼,在何時、何處完成其功能。
5.引入
引入允許我們想現有的類添加新方法或屬性。即在無需修改現有類的情況下讓它們具有新的行為和狀态。
6.織入
織入是将切面應用到目标對象來建立新的代理對象的過程。切面在指定的連接配接點被織入到目标對象中,在目标對象的生命周期裡有多個點可以進行織入。
- 編譯期:切面在目标類編譯時被織入。這種方式需要特殊的編譯期,比如AspectJ的織入編譯期
- 類加載期:切面在目标類加載到JVM時被織入。這種方式需要特殊的加載器,它可以在目标類被引入應用之前增強該目标類的位元組碼,例如AspectJ5的LTW(load-time weaving)
- 運作期:切面在應用運作的某個時刻被織入。一般情況下AOP容器會為目标對象動态建立一個代理對象
三.Spring AOP
Spring在運作期通知對象,通過在代理類中包裹切面,Spring在運作期将切面織入到Spring管理的Bean中。代理類封裝了目标類,并攔截被通知的方法的調用,再将調用轉發給真正的目标Bean。由于Spring是基于動态代理,所有Spring隻支援方法連接配接點,如果需要方法攔截之外的連接配接點攔截,我們可以利用Aspect來協助SpringAOP。
Spring在運作期通知對象,通過在代理類中包裹切面,Spring在運作期将切面織入到Spring管理的Bean中。代理類封裝了目标類,并攔截被通知的方法的調用,再将調用轉發給真正的目标Bean。由于Spring是基于動态代理,所有Spring隻支援方法連接配接點,如果需要方法攔截之外的連接配接點攔截,我們可以利用Aspect來協助SpringAOP。
1、定義切點
在SpringAOP中,需要使用AspectJ的切點表達式語言來定義切點。Spring隻支援AspectJ的部分切點訓示器,如下表所示:
AspectJ訓示器 | 描述 |
---|---|
arg() | 限制連接配接點比對參數為指定類型的執行方法 |
@args() | 限制連接配接點比對參數由指定注解标注的執行方法 |
execution() | 用于比對是連接配接點的執行方法 |
this() | 限制連接配接點比對AOP代理的Bean引用為指導類型的類 |
target() | 限制連接配接點比對目标對象為指定類型的類 |
@target() | 限制連接配接點比對特定的執行對象,這些對象對應的類要具備指定類型的注解 |
within() | 限制連接配接點比對指定的類型 |
@within() | 限制連接配接點比對指定注解所标注的類型(當使用SpringAOP時,方法定義在由指定的注解所标注的類裡) |
@annotation | 限制比對帶有指定注解連接配接點 |
bean() | 使用Bean ID或Bean名稱作為參數來限制切點隻比對特定的Bean |
其中隻有execution訓示器是唯一的執行比對,其他都是限制比對。是以execution訓示器是
其中隻有execution訓示器是唯一的執行比對,其他都是限制比對。是以execution訓示器是我們在編寫切點定義時最主要使用的訓示器。
2、編寫切點
假設我們要使用execution()訓示器選擇Hello類的sayHello()方法,表達式如下:
execution(* com.test.Hello.sayHello(..))
複制
方法表達式以*** 号開始,說明不管方法傳回值的類型。然後指定全限定類名和方法名。對于方法參數清單,我們使用()辨別切點選擇任意的sayHello()方法,無論方法入參是什麼。
同時我們可以使用&&(and),||(or),!(not)來連接配接訓示器,如下所示:
execution(* com.test.Hello.sayHello(..)) and !bean(xiaobu)
複制
3、申明切面
在經典Spring AOP中使用ProxyFactoryBean非常複雜,是以提供了申明式切面的選擇,在Spring的AOP配置命名空間中有如下配置元素:
AOP配置元素 | 描述 |
---|---|
<aop:advisor > | 定義AOP通知器 |
<aop:after > | 定義AOP後置通知(無論被通知方法是否執行成功) |
<aop:after-returning > | 定義AOP after-returning通知 |
<aop:after-throwing > | 定義after-throwing |
<aop:around > | 定義AOP環繞通知 |
<aop:aspect > | 定義切面 |
<aop:aspectj-autoproxy > | 啟用@AspectJ注解驅動的切面 |
<aop:before > | 定義AOP前置通知 |
<aop:config > | 頂層的AOP配置元素。大多數的<aop:* >元素必須包含在其中 |
<aop:declare-parents > | 為被通知的對象引入額外的接口,并透明的實作 |
<aop:pointcut > | 定義切點 |
4、實作
假設有一個演員類
Actor
,演員類中有一個表演方法
perform()
,然後還有一個觀衆類
Audience
,這兩個類都在包
com.example.springtest
下,Audience類主要方法如下:
public class Audience{
//搬凳子
public void takeSeats(){}
//歡呼
public void applaud(){}
//計時,環繞通知需要一個ProceedingJoinPoint參數
public void timing(ProceedingJoinPoint joinPoint){
joinPoint.proceed();
}
//演砸了
public void demandRefund(){}
//測試帶參數
public void dealString(String word){}
}
複制
a、xml配置實作
首先将Audience配置到springIOC中:
<bean id="audience" class="com.example.springtest.Audience"/>
複制
然後申明通知:
<aop:config>
<aop:aspect ref="audience">
<!-- 申明切點 -->
<aop:pointcut id="performance" expression="execution(* com.example.springtest.Performer.perform(..))"/>
<!-- 聲明傳遞參數切點 -->
<aop:pointcut id="performanceStr" expression="execution(* com.example.springtest.Performer.performArg(String) and args(word))"/>
<!-- 前置通知 -->
<aop:before pointcut-ref="performance" method="takeSeats"/>
<!-- 執行成功通知 -->
<aop:after-returning pointcout-ref="performance" method="applaud"/>
<!-- 執行異常通知 -->
<aop:after-throwing pointcut-ref="performance" method="demandRefund"/>
<!-- 環繞通知 -->
<aop:around pointcut-ref="performance" method="timing"/>
<!-- 傳遞參數 -->
<aop:before pointcut-ref="performanceStr" arg-names="word" method="dealString"/>
</aop:aspect>
</aop:config>
複制
b、注解實作
直接在Audience類上加注解(Aspect注解并不能被spring自動發現并注冊,要麼寫到xml中,要麼使用@Aspectj注解或者加一個@Component注解),如下所示:
@Aspect
public class Audience{
//定義切點
@Pointcut(execution(* com.example.springtest.Performer.perform(..)))
public void perform(){}
//定義帶參數切點
@Pointcut(execution(* com.example.springtest.Performer.performArg(String) and args(word)))
public void performStr(String word){}
//搬凳子
@Before("perform()")
public void takeSeats(){}
//歡呼
@AfterReturning("perform()")
public void applaud(){}
//計時,環繞通知需要一個ProceedingJoinPoint參數
@Around("perform()")
public void timing(ProceedingJoinPoint joinPoint){
joinPoint.proceed();
}
//演砸了
@AfterThrowing("perform()")
public void demandRefund(){}
//帶參數
@Before("performStr(word)")
public void dealString(String word){}
}
複制
c、通過切面引入新功能
既然可以用AOP為對象擁有的方法添加新功能,那為什麼不能為對象增加新的方法呢?利用被稱為引入的AOP概念,切面可以為Spring Bean添加新的方法,示例圖如下:
當引入接口的方法被調用時,代理将此調用委托給實作了新接口的某個其他對象。實際上,Bean的實作被拆分到了多個類。
-
xml引入需要使用<aop:declare-parents >元素:
<aop:aspect> <aop:declare-parents types-matching="com.fxb.springtest.Performer+" implement-interface="com.fxb.springtest.AddTestInterface" default-impl="com.fxb.springtest.AddTestImpl"/> </aop:aspect>
顧名思義<declare-parents>聲明了此切面所通知的Bean在它的對象層次結構中有了新的父類型。其中types-matching指定增強的類;implement-interface指定實作新方法的接口;default-imple指定實作了implement-interface接口的實作類,也可以用delegate-ref來指定一個Bean的引用。
- 注解引入,通過
@DeclareParents
注解
@DeclareParents(value="com.fxb.springtest.Performer+", defaultImpl=AddTestImpl.class) public static AddTestInterface addTestInterface;
同xml實作一樣,注解也由三部分組成:1、value屬性相當于tpes-matching屬性,辨別被增強的類;2、defaultImpl等同于default-imple,指定接口的實作類;3、有@DeclareParents注解所标注的static屬性指定了将被引入的接口。