目錄
一、基於Schema風格配置AOP增強處理;
二、基於Schema風格配置全局切入點;
三、Spring AOP與AspectJ AOP的關係;
四、使用@Aspect定義切面;
五、基於Annotation風格配置AOP增強處理;
六、基於Annotation風格配置全局切入點;
前言
筆者在上一章節中,爲大家詳細且深入的講解了有關AOP的一些基礎知識。那麼從本章開始,咱們要開始接着學習有關Spring AOP的後續知識,這些知識包括:增強處理、切入點配置、以及如何使用基於AspectJ的方式配置Spring AOP。但是在學習這些技術之前,筆者還是要提醒各位,掌握基礎相對於進階來說是必經之路,沒有一步登天,所以耐下心學習,才能夠讓你成長的更快。
其次,有些朋友在閱讀筆者博文的時候,往往容易誤解筆者博文的標題。總是覺得筆者在講解的時候少了些許後續章節應該包含的東西,在此筆者有必要澄清一些事實。筆者打算對每一章的知識點,逐步分析、逐步講解,這是需要極其漫長的過程和時間的。所以並不會在某一章節就對所有的知識點進行全方位概括,當然如果是因爲博文的標題誤導了你,筆者只能對你說聲抱歉。
最後還要提及一點的是,由於筆者會經常維護、更新早期博文的內容,所以如果大家有興趣還是可以再次進行閱讀,或許你會有意想不到的收穫。
一、詳解AOP之增強處理
上一章中,筆者爲大家簡單的介紹了增強處理的一些概念。還記得什麼是增強處理嗎?增強處理無非就是定義了通知的執行時機、執行順序。如果你無法理解什麼是執行時機與執行順序,那麼請你回想下咱們在上一章編寫JDK動態代理時,是否顯式的指定過代理業務的執行?其實增強處理無法就是隱式的幫你做了這些事情而已。在Spring AOP中,增強處理一共包含如下5種類型:
1、前置通知(Before advice):代理業務執行於被攔截的方法之前;
2、後置通知(After advice):代理業務執行於被攔截的方法之後;
3、環繞通知(Around advice):代理業務執行於被攔截的方法之前或之後;
4、異常通知(After throwing advice):代理業務執行於被攔截的方法拋出異常後;
5、返回時通知(After returning advice):代理業務執行於被攔截的方法返回之前;
在基於Schema的AOP中配置中,我們可以通過使用如下標籤配置Spring AOP的增強處理:
1、<aop:before/>:用於配置前置通知(Before advice);
2、<aop:after/>:用於配置後置通知(After advice);
3、<aop:around/>:用於配置環繞通知(Around advice);
4、<aop:after-throwing/>:用於配置異常通知(After throwing advice);
5、<aop:after-returning/>:用於配置返回通知(After returning advice);
上述標籤我們可以將其稱之爲增強處理標籤,因爲這些標籤均帶有部分相同屬性。分別爲:method、pointcut、pointcut-ref、throwing和returning。其中屬性“method”用於指定作爲增強處理類型的切面類的指定方法。屬性“pointcut”允許定義一個表達式,該表達式的作用就是攔截指定切入點,並且該表達式支持通配符“*”和“..”。其中通配符“*”代表了任意命名模式的匹配,而“..”則代表了接受0—N個任意類型參數的方法。屬性“pointcut-ref”用於引用一個已經存在的切入點名稱,換句話來說,該屬性可以用於引用一個全局攔截切入點表達式。屬性“throwing”僅針對<aop:after-throwing/>標籤有效,該屬性用於指定一個參數,異常通知(After throwing advice)便可以通過這個參數訪問委託對象的指定方法拋出的異常。屬性“returning”同樣僅針對<aop:after-returning/>標籤有效,該屬性用於指定一個參數,返回時通知(After returning advice)便可以通過這個參數訪問委託對象的指定方法的返回值。
筆者打算先從前置通知(Before advice)和後置通知(After advice)開始講起,因爲這兩個通知是Spring AOP增強處理中最爲簡單,同時也是最容易理解的。前置通知(Before advice)無非就是在切入點執行之前首先執行代理業務,而後置通知(After advice)則反之。
基於Schema的風格配置Before and After增強處理:
- <aop:config>
- <aop:aspect id="logAspect" ref="logBean" order="1">
- <!-- 前置通知 -->
- <aop:before method="logTest"
- pointcut="execution(* org.johngao.bean.LoginBean.*(..))" />
- <!-- 後置通知 -->
- <aop:after method="logTest"
- pointcut="execution(* org.johngao.bean.LoginBean.*(..))" />
- </aop:aspect>
- </aop:config>
- <bean name="logBean" class="org.johngao.bean.LogBean" />
在上述配置文件中,筆者爲切入點定義了2個通知,分別爲前置通知(Before advice)和後置通知(After advice)。在切入點執行之前首先執行前置通知(Before advice),然後再執行切入點,最後執行後置通知(After advice)。其中屬性“pointcut”的值爲:"execution(* org.johngao.bean.LoginBean.*(..))" 。也就是說指定通知將攔截org.johngao.bean包下LoginBean類型的所有帶參或無參方法。當你看到這裏的時候,或許你已經明白,在一個切入點內部,允許同時定義多個通知。
Before and After通知實現:
- /**
- * 模擬前置通知、後置通知
- *
- * @author JohnGao
- *
- * @param JoinPoint: 連接點
- *
- * @return void
- */
- public void logTest(JoinPoint joinPoint)
- {
- /* 獲取委託對象指定方法參數 */
- Object[] params = joinPoint.getArgs();
- for(Object param: params)
- System.out.println(param);
- /* 獲取委託對象指定方法名稱 */
- System.out.println(joinPoint.getSignature().getName());
- /* 獲取委託對象實例 */
- joinPoint.getTarget();
- /* 獲取代理對象實例 */
- joinPoint.getThis();
- System.out.println("日誌記錄...");
- }
在上述程序示例中,筆者使用到了AspectJ提供的JoinPoint接口。開發人員使用JoinPoint接口可以很方便的訪問到委託對象的上下文信息,並且在實際開發過程中,這些上下文信息對於開發人員而言至關重要。JoinPoint接口的常用方法如下:
方法名稱 | 方法返回值 | 方法描述 |
getArgs() | Object[] | 獲取委託對象的目標方法參數列表,注意:由於返回數組所以需要拆分 |
getSignature() | Signature | 獲取委託對象的方法簽名 |
getTarget() | Object | 獲取委託對象實例 |
getThis() | Object | 獲取代理對象實例 |
當然JoinPoint接口還派生有另一個常用的擴展接口,那便是ProceedingJoinPoint接口。該接口僅限用於環繞通知(Around advice),並且相對JoinPoint接口而言,ProceedingJoinPoint接口爲開發人員提供有2個新增用於執行委託對象的方法,分別爲:
方法名稱 | 方法返回值 | 方法描述 |
proceed() | Object | 用於執行委託對象的目標方法 |
proceed(java.lang.Object[] args) | Object | 用於執行委託對象的目標方法,用新參數替換原先入參 |
至於如何使用ProceedingJoinPoint接口,大家目前不必着急,筆者在後續章節自然會進行講解。爲了更加形象的描述不同通知的執行時機與執行順序,筆者接下來會爲大家展示各種通知執行時序圖。
Before and After增強處理執行時序圖:
當大家理解前置通知(Before advice)和後置通知(After advice)後,筆者接下來將會爲大家講解異常通知(After throwing advice)的配置和使用。所謂異常通知(After throwing advice)無非就是在切入點拋出異常之後執行代理業務,Spring的增強處理爲開發人員提供了多種環境下執行通知的機會,在此筆者不得不佩服Rod Johnson豐富的想象力及“創造力”。
基於Schema的風格配置After throwing增強處理:
- <aop:config>
- <aop:aspect id="logAspect" ref="logBean" order="1">
- <!-- 異常通知 -->
- <aop:after-throwing method="logTest"
- pointcut="execution(* org.johngao.bean.LoginBean.*(..))" />
- </aop:aspect>
- </aop:config>
- <bean name="logBean" class="org.johngao.bean.LogBean" />
如果你定義的切入點並沒有往外拋出異常,通知是無法執行的。所以使用異常通知(After throwing advice)的時候,我們務必需要將爲切入點throws異常。
還記得筆者在上一章節提到過的throwing屬性嗎?其僅針對<aop:after-throwing/>標籤有效,該屬性用於指定一個參數,異常通知(After throwing advice)便可以通過這個參數訪問委託對象的指定方法拋出的異常。使用throwing屬性指定參數:
- <aop:after-throwing method="logTest"
- pointcut="execution(* org.johngao.bean.LoginBean.*(..))"
- throwing="exception" />
捕獲委託對象的指定方法拋出的異常:
- /**
- * 模擬異常通知
- *
- * @author JohnGao
- *
- * @param JoinPoint: 連接點, Exception: 異常信息
- *
- * @return void
- */
- public void logTest(JoinPoint joinPoint, Exception exception)
- {
- System.out.println(exception);
- }
通過上述程序示例我們可以看出,一旦委託對象的目標方法拋出異常後,我們便可以在通知中通過屬性“exception”訪問所拋出的異常信息。當然定義在通知中的方法參數,Spring的IOC容器自然會負責其初始化工作,所以你無須關心這些“瑣事”,儘可能的關注於你的業務既可。
After throwing advice增強處理執行時序圖:
返回時通知(After returning advice)其實也比較簡單,該通知執行於被攔截的方法返回之前。也就是說當返回時通知(After returning advice)攔截切入點後,應當首先執行切入點,最後切入點即將返回時再執行通知。
基於Schema的風格配置After returning增強處理:
- <aop:config>
- <aop:aspect id="logAspect" ref="logBean" order="2">
- <!-- 返回時通知 -->
- <aop:after-returning method="logTest"
- pointcut="execution(* org.johngao.bean.LoginBean.*(..))" />
- </aop:aspect>
- </aop:config>
- <bean name="logBean" class="org.johngao.bean.LogBean" />
還記得筆者在上一章節提到過的returning屬性嗎?其僅針對<aop:after-returning/>標籤有效,該屬性用於指定一個參數,返回時通知(After returning advice)便可以通過這個參數訪問委託對象的指定方法的返回值。使用returning屬性指定參數:
- <aop:after-returning method="logTest"
- pointcut="execution(* org.johngao.bean.LoginBean.*(..))"
- returning="rvt" />
獲取委託對象的目標方法返回值:
- /**
- * 模擬返回時通知
- *
- * @author JohnGao
- *
- * @param JoinPoint: 連接點, Object: 返回值信息
- *
- * @return void
- */
- public void logTest(JoinPoint joinPoint, Object rvt)
- {
- System.out.println(rvt);
- }
通過上述程序示例我們可以看出,如果需要獲取委託對象的目標方法的返回值時,我們可以通過定義<aop:after-returning/>標籤中的“returning”屬性即可。但這裏需要注意的是,returning屬性的屬性名稱務必和通知方法中的屬性名稱保持一致。
After returning advice增強處理執行時序圖:
筆者至此已經爲大家講解了前置(Before advice)、後置(After advice)、異常(After throwing advice)、返回時(After returning advice)等4種通知的配置和使用,那麼接下來筆者就開始講解環繞通知(Around advice)。至於爲什麼需要將環繞通知(Around advice)放在最後一個講解?並不是因爲它相對其他幾個通知更復雜,而是因爲這個通知更特殊。
但從字面上的理解,很多開發人員會錯誤的將環繞通知(Around advice)理解爲是前置通知(Before advice)和後置通知(After advice)的結合體。以爲該通知無非就是在切入點的執行前後各執行一次,其實這是錯誤的,筆者希望大家不要因爲字面的顯淺理解就妄下定論,免得貽笑大方。
環繞通知(Around advice)適用的場景更多的基於併發環境,並且該通知可以滿足於切入點之前或者之後執行,甚至可以根據實際業務來判斷是否執行。這纔是使用環繞通知(Around advice)真正的目的。
基於Schema的風格配置Around增強處理:
- <aop:config>
- <aop:aspect id="logAspect" ref="logBean" order="1">
- <!-- 環繞通知 -->
- <aop:around method="logTest"
- pointcut="execution(* org.johngao.bean.LoginBean.*(..))" />
- </aop:aspect>
- </aop:config>
- <bean name="logBean" class="org.johngao.bean.LogBean" />
ProceedingJoinPoint接口派生於JoinPoint接口,該接口僅限於環繞通知(Around advice)獲取委託對象的上下文信息使用。筆者剛纔也提到過,在實際開發過程中,我們可以使用環繞通知(Around advice)根據實際業務,決定通知是否執行,以及何時執行。
Around通知實現:
- /**
- * 模擬環繞通知
- *
- * @author JohnGao
- *
- * @param ProceedingJoinPoint: 連接點
- *
- * @return void
- */
- public void logTest(ProceedingJoinPoint proceedingJoinPoint)
- throws Throwable
- {
- System.out.println("日誌記錄...");
- /* 執行委託對象的目標方法 */
- proceedingJoinPoint.proceed();
- System.out.println("日誌記錄...");
- }
ProceedingJoinPoint接口的proceed()方法用於執行委託對象的目標方法。然而ProceedingJoinPoint接口還提供有另外一個帶參的proceed(java.lang.Object[] args)方法,該方法除了可以用於執行委託對象的目標方法外,還支持使用新參數替換原先入參。
使用proceed(java.lang.Object[] args)方法替換原先入參:
- /* 執行委託對象的目標方法 */
- proceedingJoinPoint.proceed(new Object[]{"新入參,替換原先入參"});
一旦使用proceed(java.lang.Object[] args)方法替換原先入參後,新參數將會覆蓋之前傳遞給委託對象的目標方法參數。當然如果你不希望替換原先入參,筆者建議你還是使用proceed()方法即可。
二、基於Schema風格配置全局切入點
在5種通知標籤中,我們可以通過使用pointcut屬性定義一個表達式,該表達式的作用就是攔截指定切入點。只不過所配置的切入點的作用域僅針對對應的切面有效,換句話來說使用pointcut屬性定義的攔截切入點表達式作用域是局部的。但是在實際開發過程中,我們往往需要定義一些通用的全局攔截切入點表達式,該如何實現呢?值得慶幸的是Spring爲咱們提供有<aop:pointcut/>標籤以滿足開發人員的需求。
使用<aop:pointcut/>標籤定義全局攔截切入點表達式相當簡單,你僅僅只需要聲明,然後在通知標籤中使用屬性“pointcut-ref”引用即可。這樣一來,我們就可以定義一些通用的攔截切入點表達式,而不必每次都在通知標籤中重複定義。
使用<aop:pointcut/>標籤定義全局攔截切入點表達式:
- <aop:config>
- <aop:pointcut expression="execution(* org.johngao.bean.LoginBean.*(..))"
- id="pointcutTest" />
- <aop:aspect id="logAspect" ref="logBean" order="1">
- <!-- 環繞通知 -->
- <aop:around method="logTest" pointcut-ref="pointcutTest" />
- </aop:aspect>
- </aop:config>
- <bean name="logBean" class="org.johngao.bean.LogBean" />
三、Spring AOP與AspectJ AOP的關係
AspectJ是Java平臺誕生的第一個AOP Framework,可以毫不客氣的說AspectJ已經成爲AOP領域的規範制定者。遵循AspectJ規範,也就是在遵循AOP的標準。目前市面上諸多AOP Framework都在借鑑AspectJ的一些思想,其中就包括Spring。值得慶幸的是AspectJ完全是開源的,並且完全採用Java語言編寫的,開發人員可以自由下載AspectJ的源碼進行研究和學習。
Spring針對AspectJ進行了很好的集成支持,並且Spring允許開發人員直接在Spring中使用AspectJ進行AOP編程。當然筆者並不打算對AspectJ進行深入講解,感興趣的朋友可以自行下載AspectJ的依賴構件及API。
或許談到現在,有很多朋友還是不明白Spring AOP與AspectJ到底存在什麼關係,難道僅僅只是遵循了AspectJ的規範進行自定義編制嗎?其實不是的,Spring AOP與AspectJ從嚴格意義上來說完全是兩碼事,Spring AOP更像是一個粘合劑。除了允許你使用Spring的原生AOP實現,同時還支持你使用AspectJ作爲Spring的AOP實現。早在Spring1.x的時代,由於那時候Spring還並未集成AspectJ,所以那時候的開發人員都只能使用Spring的原生AOP。但現在不同了,你完全可以使用AspectJ作爲你的AOP實現,並且脫離Spring,AspectJ同樣也能夠單獨使用。對於目前的開發團隊而言,已經很少有人繼續使用Spring的原生AOP來滿足項目需要,更多的均是採用集成AspectJ的方式。因爲這不僅僅是遵循一種規範,更重要的是使用AspectJ可以簡化編碼量。所謂開發實惠,就是這個道理,選擇解耦的同時,注重開發效率也是一個優秀團隊應該考慮的首要問題。
在上一章中,筆者跟大家提及過JDK動態代理和cglib動態代理。至於爲什麼要學習這2種代理,那是因爲幾乎所有的AOP Framework的底層實現均是使用這2種代理方式。JDK動態代理相對於cglib是有侷限性的,因爲JDK自身只支持基於接口的代理,而不支持類型的代理。當遇到代理類型是類類型的時候,我們或許可以考慮使用cglib。Spring AOP同樣也是這麼做的,Spring缺省使用JDK動態代理來作爲AOP底層實現,那是爲了實現高內聚、低耦合,Spring遵循了面向接口編程而已。只有當需要代理的爲非接口類型時,Spring纔會自動切換cglib作爲AOP的底層實現。
最後你只需要明白一旦在Spring中使用AspectJ進行AOP編程,Spring AOP則依賴於AspectJ,而AspectJ的底層實現仍然是採用JDK動態代理和cglib動態代理。
Spring AOP與AspectJ AOP的依賴關係圖:
提示:
筆者在此還要補充一點的是,AspectJ採用的是編譯時增強的解決方案。這與Spring原生的AOP實現是不同的,Spring的原生AOP實現採用的是代理實現方式,這2種底層實現方式的不同,並不代表着性能差距會很大,你只需關注你的業務即可。
四、使用@Aspect定義切面
AspectJ允許使用Annotation的方式來定義切面、增強類型以及切入點。當然你不用去關注Spring與AspectJ底層依賴的一些“瑣事”,因爲你根本沒有必要理解這些東西,你只需要明白接下來咱們要做的事情就是在Spring中使用AspectJ提供的Annotation進行AOP編程即可。
我們可以使用AspectJ提供的@Aspect來定義一個切面類,但是在正式使用之前,我們必須要在IOC配置文件中添加AspectJ的支持,因爲只有這樣才能正常使用AspectJ AOP。
啓動AspectJ AOP支持:
- <!-- 啓動AspectJ支持 -->
- <aop:aspectj-autoproxy />
在第四章的時候,筆者爲大家講解了<context:component-scan/>標籤的使用。既然我們使用的是AspectJ定義切面,那麼必然我們還需要在自動掃包的時候,能夠自動掃描定義的所有切面組件。<context:component-scan/>標籤中包含了一個<context:include-filter/>子標籤,該標籤的作用就是IOC容器啓動時,除了會自動掃描項目中所有的Bean組件外,還會掃描定義好的所有切面組件。
使用<context:include-filter/>標籤自動掃描切面組件:
- <context:component-scan base-package="*">
- <context:include-filter type="annotation"
- expression="org.aspectj.lang.annotation.Aspect" />
- </context:component-scan>
如果使用Annotation的方式定義切面,咱們就可以將以前以Schema風格定義在配置文件中的切面信息完全移除。開發人員只需在切面類上方加上@Aspect標註即可。
使用@Aspect定義切面類:
- @Aspect
- public class LogBean
- {
- //...
- }
通過上述程序示例我們可以看出,使用@Aspect可成功定義一個切面類。相對於以Schema風格定義切面類來說確實方便了不少,同時也爲IOC配置文件進行了極大的瘦身。
五、基於Annotation風格配置AOP增強處理
使用Annotation的方式配置AOP增強處理,主要使用到的Annotation爲如下5種:
1、@Before:用於配置前置通知(Before advice);
2、@After:用於配置後置通知(After advice);
3、@Around:用於配置環繞通知(Around advice);
4、@AfterThrowing:用於配置異常通知(After throwing advice);
5、@afterReturning:用於配置返回通知(After returning advice);
使用@Before配置前置通知(Before advice):
- @Aspect
- public class LogBean
- {
- @Before("execution(* org.johngao.bean.*.*(..))")
- public void logTest(JoinPoint joinPoint)
- {
- System.out.println("日誌記錄...");
- }
- }
使用@After用於配置後置通知(After advice):
- @Aspect
- public class LogBean
- {
- @After("execution(* org.johngao.bean.*.*(..))")
- public void logTest(JoinPoint joinPoint)
- {
- System.out.println("日誌記錄...");
- }
- }
使用@Around配置環繞通知(Around advice):
- @Aspect
- public class LogBean
- {
- @Around("execution(* org.johngao.bean.*.*(..))")
- public void logTest(ProceedingJoinPoint proceedingJoinPoint)
- throws Throwable
- {
- System.out.println("日誌記錄...");
- /* 執行委託對象的目標方法 */
- proceedingJoinPoint.proceed();
- System.out.println("日誌記錄...");
- }
- }
使用@AfterThrowing配置異常通知(After throwing advice):
- @Aspect
- public class LogBean
- {
- @AfterThrowing(pointcut = "execution(* org.johngao.bean.*.*(..))",
- throwing="exception")
- public void logTest(JoinPoint joinPoint, Exception exception)
- {
- System.out.println("異常: " + exception);
- System.out.println("日誌記錄...");
- }
- }
使用@AfterReturning:用於配置返回通知(After returning advice):
- @Aspect
- public class LogBean
- {
- @AfterReturning(pointcut = "execution(* org.johngao.bean.*.*(..))",
- returning ="rvt")
- public void logTest(JoinPoint joinPoint, Object rvt)
- {
- System.out.println("返回值: " + rvt);
- System.out.println("日誌記錄...");
- }
- }
在@AfterThrowing和@AfterReturning的內部,都包含一個叫做pointcut的屬性。在基於Schema風格的配置中,我們都是使用這個屬性來定義攔截切入點表達式。但是其內部缺省的value屬性也可以用於定義攔截切入點表達式,那麼我們應該如何選擇呢?來看看Spring的官方解釋:
通過Spring的官方解釋我們可以看出,缺省情況下以value屬性定義攔截切入點表達式,如果顯示指定pointcut作爲攔截切入點表達式後,pointcut將重寫value屬性的定義。
六、基於Annotation風格配置全局切入點
在基於Schema的配置風格中,我們可以使用<aop:pointcut/>標籤來配置全局攔截點表達式。而一旦我們使用基於Annotation的方式後,則可以使用@Pointcut的方式配置全局攔截點表達式。@Pointcut可以適用於委託對象的目標方法之上,同樣也可以適用於通知上。當然具體怎麼使用就根據項目需要或者你的個人喜好而定。
使用@Pointcut配置全局切入點:
- @Aspect
- public class LogBean {
- @Before(value ="testPointcut()")
- public void logTest() {
- System.out.println("日誌記錄...");
- }
- @Pointcut("execution(* org.johngao.bean.*.*(..))")
- public void testPointcut() {
- //...
- }
- }
如果其他切面類也需要使用這個全局的攔截點表達式,則可以使用類名.方法名的方式進行引用:
- @Before(value ="LogBean.testPointcut()")
本章內容到此結束,由於時間倉庫,本文或許有很多不盡人意的地方,希望各位能夠理解和體諒。關於下一章的內容,筆者打算講解Spring3.x MVC相關的內容。