本篇內容說說Spring對切麵的支持,如何把普通類聲明為一個切麵,以及如何使用註解創建切麵,主要有以下幾點內容: 什麼是面向切麵編程 選擇連接點 使用註解創建切麵 在XML中聲明切麵 ...
本篇內容說說Spring對切麵的支持,如何把普通類聲明為一個切麵,以及如何使用註解創建切麵,主要有以下幾點內容:
- 什麼是面向切麵編程
- 選擇連接點
- 使用註解創建切麵
- 在XML中聲明切麵
什麼是面向切麵編程
切麵能幫助模塊化橫切關註點,橫切關註點可以被描述為影響應用多處的功能。如圖,直觀呈現橫切關註點的概念:
上圖展現了一個被劃分為模塊的典型應用。每個模塊的核心功能都是為特定業務領域提供服務,但是這些模塊都需要類似的輔助功能,例如安全、事務管理、監控日誌等。
使用面向切麵編程時,在一個地方定義通用功能,可以通過聲明的方式定義這個功能要以何種方式在何處應用,而無需修改受影響的類。橫切關註點可以被模塊化為特殊的類,這些類被稱為切麵。這樣做有兩個好處:首先,每個關註點都集中於一個地方,而不是分散到多處代碼中;其次,服務模塊更簡潔,因為它們只包含主要關註點的代碼,而次要關註點的代碼被轉移到切麵中。
定義AOP術語
描述切麵的常用術語有通知(advice)、切點(pointcut)和連接點(join point),這幾個概念之間的關聯如圖所示:
通知(Advice)
通知定義了切麵是什麼以及何時使用。Spring切麵可應用的通知有5種類型:
- 前置通知(Before):在目標方法被調用之前調用通知;
- 後置通知(After):在目標方法完成之後調用通知;
- 返回通知(After-returning):在目標方法成功執行之後調用通知;
- 異常通知(After-throwing):在目標方法拋出異常後調用通知;
- 環繞通知(Around):通知包裹了被通知的方法,在被通知的方法調用之前和調用之後執行自定義的行為。
連接點(Join point)
連接點是在應用執行過程中能夠插入切麵的一個點,這個點可以是調用方法時、拋出異常時、甚至修改一個欄位時。切麵代碼可以利用這些點插入到應用的正常流程之中,並添加新的行為。
切點(Pointcut)
切點的定義會匹配通知所要織入的一個或多個連接點。通常是使用明確的類和方法名稱,或是利用正則表達式定義所匹配的類和方法名稱來指定這些切點。
切麵(Aspect)
切麵是通知和切點的結合,通知和切點共同定義了切麵的全部內容 —— 是什麼,在何時、何處完成其功能。
引入(Introduction)
引入允許我們向現有的類添加新方法和屬性。
織入(Weaving)
織入是把切麵應用到目標對象並創建新的代理對象的過程。在目標對象的生命周期里有多個點可以進行織入:
- 編譯期:切麵在目標類編譯時被織入,這種方式需要特殊的編譯器。AspectJ的織入編譯器就是以這種方式織入切麵的。
- 類載入期:切麵在目標類載入到JVM時被織入。這種方式需要特殊的類載入器(ClassLoader),它可以在目標類被引入應用之前增強目標類的位元組碼。AspectJ 5的載入時織入(load-time weaving,LTW)就支持這種方式織入切麵。
- 運行期:切麵在應用運行的某個時刻被織入。一般情況下,在織入切麵時,AOP容器會為目標對象動態地創建一個代理對象。Spring AOP就是以這種方式織入切麵的。
Spring對AOP的支持
不是所有的AOP框架都是相同的,它們在連接點模型上可能有強弱之分,有些允許在欄位修飾符級別應用通知,有些只支持方法調用相關的連接點。它們織入切麵的方式和時機也有所不同。總的來說,創建切點來定義切麵所織入的連接點是AOP框架的基本功能。
Spring提供了4種類型的AOP支持:
- 基於代理的經典Spring AOP;
- 純POJO切麵;
- @AspectJ註解驅動的切麵;
- 註入式AspectJ切麵
前三種都是Spring AOP實現的變體,Spring AOP構建在動態代理基礎之上,因此,Spring對AOP的支持局限方法攔截。
Spring的經典AOP編程模型曾經非常棒,但現在Spring提供了更簡潔和乾凈的面向切麵編程方式。引入了簡單的聲明式AOP和基於註解的AOP,Spring經典的AOP看超來顯得非常笨重和過於複雜,使用ProxyFactory Bean會讓人感到繁瑣。所以這裡只寫簡單的聲明式AOP和基於註解的AOP。
藉助Spring的aop命名空間,可以將純POJO轉換為切麵。這些POJO只是提供了滿足切點條件時所要調用的方法,它需要XML配置。
Spring借鑒了AspectJ的切麵,提供註解驅動的AOP。它不需要XML配置,它雖然是基於代理的AOP,但編程模型與AspectJ註解切麵幾乎一致。
選擇連接點
Spring AOP的AspectJ切點,最重要的一點就是Spring僅支持AspectJ切點指示器(pointcut designator)的一個子集。Spring AOP支持的AspectJ切點指示器如下表:
AspectJ指示器 |
描述 |
arg() |
限制連接點匹配參數為指定類型的執行方法 |
@args() |
限制連接點匹配參數由指定註解標註的執行方法 |
execution() |
用於匹配是連接點的執行方法 |
this() |
限制連接點匹配AOP代理的bean引用為指定類型的類 |
target() |
限制連接點匹配目標對象為指定類型的類 |
@target() |
限制連接點匹配特定的執行對象,這些對象對應的類要具有指定類型的註解 |
within() |
限制連接點匹配指定的類型 |
@within() |
限制連接點匹配指定註解所標註的類型(當使用Spring AOP時,方法定義在由指定的註解所標註的類里) |
@annotation |
限定匹配帶有指定註解的連接點 |
只有execution指示器是實際執行匹配的,而其它的指示器都是用來限制匹配的。execution指示器是編寫切點定義時使用最多的指示器。
編寫切點
定義Performance介面作為切麵的切點:
package concert;
public interface Performance {
public void perform();
}
現在展示一個切點表達式,這個表達式能夠設置當perform()方法執行時觸發通知的調用:
execution(* concert.Performance.perform(..))
使用execution()指示器選擇Performance的perform()方法。方法表達式以“*”號開始,表明不關心方法返回值的類型。還指定了全限定類名和方法名,對於方法參數列表,使用兩個點號(..)表明切點要選擇任意的perform()方法,無論該方法的入參是什麼。
假設需要配置的切點僅匹配concert包。在此場景下,可以使用within()指示器來限制匹配:
execution(* concert.Performance.perform(..)) && within(concert.*)
在切點中選擇bean
除了上面所列的指示器外,Spring還引入了一個新的bean()指示器,它允許在切點表達式中使用bean的ID來標識bean。bean()使用bean ID或bean名稱作為參數來限制切點只匹配的bean。
execution(* concert.Performance.perform(..)) and bean('dancer')
還可以使用非操作為除了特定ID以外的其他bean應用通知:
execution(* concert.Performance.perform(..)) and !bean('dancer')
使用註解創建切麵
Aspect提供了五個註解來定義通知:
- @Before: 通知方法會在目標方法調用之前執行
- @After: 通知方法會在目標方法返回或拋出異常後調用
- @AfterReturning: 通知方法會在目標方法返回後調用
- @Around: 通知方法會將目標方法封裝起來
- @AfterThrowing: 通知方法會在目標方法拋出異常後調用
示例代碼
package asp; import com.alibaba.fastjson.JSONObject; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; @Aspect @Component public class RecordAspect { @Pointcut("execution(* asp.P30Phone.call(..))") public void excute() { } @Before("excute()") public void before(JoinPoint joinPoint) { // 列印請求入參 System.out.println("parameter:" + JSONObject.toJSONString(joinPoint.getArgs())); } @Around("excute()") public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { long beginTime = System.currentTimeMillis(); Object result = proceedingJoinPoint.proceed(); long endTime = System.currentTimeMillis(); System.out.println("result:"+ result+", execute times:" + (endTime - beginTime) + " ms"); return result; } @After("excute()") public void after(JoinPoint joinPoint) throws Throwable { System.out.println("after:" + joinPoint.toString()); } }
@Aspect定義一個切麵,@Pointcut 定義命名的切點
package asp; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; @Configuration @ComponentScan @EnableAspectJAutoProxy public class AspConfig { }
@EnableAspectJAutoProxy 啟動AspectJ自動代理
package asp; public interface HWPhone { String call(String user); }
package asp; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; @Component("p30Phone") @Qualifier("p30") public class P30Phone implements HWPhone { @Override public String call(String user) { System.out.println("使用 P30 手機呼叫..."+user); return "p30"; } }
package asp; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class Main { public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AspConfig.class); HWPhone hwPhone = (HWPhone) context.getBean("p30Phone"); hwPhone.call("xiaoming"); } }
運行結果:
parameter:["xiaoming"] 使用 P30 手機呼叫...xiaoming result:p30, execute times:51 ms after:execution(String asp.HWPhone.call(String))
在XML中聲明切麵
在Spring的aop命名空間中, 提供了多個元素用來在XML中聲明切麵
AOP配置元素 |
用途 |
<aop:advisor> |
定義AOP通知器 |
<aop:after> |
定義AOP後置通知 |
<aop:after-returning> |
定義AOP返回通知 |
<aop:after-throwing> |
定義AOP異常通知 |
<aop:around> |
定義AOP環繞通知 |
<aop:aspect> |
定義一個切麵 |
<aop:aspectj-autoproxy> |
啟用@AspectJ註解驅動的切麵 |
<aop:before> |
定義一個AOP前置通知 |
<aop:config> |
頂層的AOP配置元素。大多數的<aop:*>元素必須包含在<aop:config>元素內 |
<aop:declare-parents> |
以透明的方式為被通知的對象引入額外的介面 |
<aop:pointcut> |
定義一個切點 |
示例代碼
<?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/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd"> <bean id="p30Phone" class="asp.P30Phone"/> <bean id="recordAspect" class="asp.RecordAspect" /> <aop:config> <aop:aspect ref="recordAspect"> <aop:pointcut id="execute" expression="execution(* asp.P30Phone.call(..))"/> <aop:before pointcut-ref="execute" method="before" /> <aop:after pointcut-ref="execute" method="after" /> <aop:around pointcut-ref="execute" method="around" /> </aop:aspect> </aop:config> </beans>
package asp; import com.alibaba.fastjson.JSONObject; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; public class RecordAspect { public void before(JoinPoint joinPoint) { // 列印請求入參 System.out.println("parameter:" + JSONObject.toJSONString(joinPoint.getArgs())); } public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { long beginTime = System.currentTimeMillis(); Object result = proceedingJoinPoint.proceed(); long endTime = System.currentTimeMillis(); System.out.println("result:"+ result+", execute times:" + (endTime - beginTime) + " ms"); return result; } public void after(JoinPoint joinPoint) throws Throwable { System.out.println("after:" + joinPoint.toString()); } }
package asp; public interface HWPhone { String call(String user); }
package asp; public class P30Phone implements HWPhone { @Override public String call(String user) { System.out.println("使用 P30 手機呼叫..."+user); return "p30"; } }
package asp; import org.springframework.context.support.ClassPathXmlApplicationContext; public class Main { public static void main(String[] args) { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("asp/application.xml"); HWPhone hwPhone = (HWPhone) context.getBean("p30Phone"); hwPhone.call("xiaoming"); } }
運行結果:
parameter:["xiaoming"] 使用 P30 手機呼叫...xiaoming result:p30, execute times:0 ms after:execution(String asp.HWPhone.call(String))
參考《Spring實戰(第4版)》