前面兩篇文章記錄了 Spring IOC 的相關知識,本文記錄 Spring 中的另外一特性 AOP 相關知識。html
部分參考資料:
《Spring實戰(第4版)》
《輕量級 JavaEE 企業應用實戰(第四版)》
Spring 官方文檔
W3CSchool Spring教程
易百教程 Spring教程java
AOP (Aspect Orient Programming),直譯過來就是 面向切面編程。AOP 是一種編程思想,是面向對象編程(OOP)的一種補充。面向對象編程將程序抽象成各個層次的對象,而面向切面編程是將程序抽象成各個切面。
從《Spring實戰(第4版)》圖書中扒了一張圖: spring
從該圖能夠很形象地看出,所謂切面,至關於應用對象間的橫切點,咱們能夠將其單獨抽象爲單獨的模塊。express
想象下面的場景,開發中在多個模塊間有某段重複的代碼,咱們一般是怎麼處理的?顯然,沒有人會靠「複製粘貼」吧。在傳統的面向過程編程中,咱們也會將這段代碼,抽象成一個方法,而後在須要的地方分別調用這個方法,這樣當這段代碼須要修改時,咱們只須要改變這個方法就能夠了。然而需求老是變化的,有一天,新增了一個需求,須要再多出作修改,咱們須要再抽象出一個方法,而後再在須要的地方分別調用這個方法,又或者咱們不須要這個方法了,咱們仍是得刪除掉每一處調用該方法的地方。實際上涉及到多個地方具備相同的修改的問題咱們均可以經過 AOP 來解決。編程
AOP 要達到的效果是,保證開發者不修改源代碼的前提下,去爲系統中的業務組件添加某種通用功能。AOP 的本質是由 AOP 框架修改業務組件的多個方法的源代碼,看到這其實應該明白了,AOP 其實就是前面一篇文章講的代理模式的典型應用。
按照 AOP 框架修改源代碼的時機,能夠將其分爲兩類:框架
下面給出經常使用 AOP 實現比較
yii
如不清楚動態代理的,可參考我前面的一篇文章,有講解靜態代理、JDK動態代理和 CGlib 動態代理。
靜態代理和動態代理 https://www.cnblogs.com/joy99/p/10865391.htmlide
AOP 領域中的特性術語:測試
概念看起來老是有點懵,而且上述術語,不一樣的參考書籍上翻譯還不同,因此須要慢慢在應用中理解。gradle
AOP 框架有不少種,1.3節中介紹了 AOP 框架的實現方式有可能不一樣, Spring 中的 AOP 是經過動態代理實現的。不一樣的 AOP 框架支持的鏈接點也有所區別,例如,AspectJ 和 JBoss,除了支持方法切點,它們還支持字段和構造器的鏈接點。而 Spring AOP 不能攔截對對象字段的修改,也不支持構造器鏈接點,咱們沒法在 Bean 建立時應用通知。
下面先上代碼,對着代碼說比較好說,看下面這個例子: 這個例子是基於gradle建立的,首先 build.gradle 文件添加依賴:
dependencies { compile 'org.springframework:spring-context:5.0.6.RELEASE' }
首先建立一個接口 IBuy.java
package com.sharpcj.aopdemo.test1; public interface IBuy { String buy(); }
Boy 和 Gril 兩個類分別實現了這個接口: Boy.java
package com.sharpcj.aopdemo.test1; import org.springframework.stereotype.Component; @Component public class Boy implements IBuy { @Override public String buy() { System.out.println("男孩買了一個遊戲機"); return "遊戲機"; } }
Girl.java
package com.sharpcj.aopdemo.test1; import org.springframework.stereotype.Component; @Component public class Girl implements IBuy { @Override public String buy() { System.out.println("女孩買了一件漂亮的衣服"); return "衣服"; } }
配置文件, AppConfig.java
package com.sharpcj.aopdemo; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @Configuration @ComponentScan(basePackageClasses = {com.sharpcj.aopdemo.test1.IBuy.class}) public class AppConfig { }
測試類, AppTest.java
package com.sharpcj.aopdemo; import com.sharpcj.aopdemo.test1.Boy; import com.sharpcj.aopdemo.test1.Girl; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class AppTest { public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); Boy boy = context.getBean("boy",Boy.class); Girl girl = (Girl) context.getBean("girl"); boy.buy(); girl.buy(); } }
運行結果:
這裏運用SpringIOC裏的自動部署。如今需求改變了,咱們須要在男孩和女孩的 buy 方法以前,須要打印出「男孩女孩都買了本身喜歡的東西」。用 Spring AOP 來實現這個需求只需下面幾個步驟:
一、 既然用到 Spring AOP, 首先在 build.gralde
文件中引入相關依賴:
dependencies { compile 'org.springframework:spring-context:5.0.6.RELEASE' compile 'org.springframework:spring-aspects:5.0.6.RELEASE' }
二、 定義一個切面類,BuyAspectJ.java
package com.sharpcj.aopdemo.test1; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; @Aspect @Component public class BuyAspectJ { @Before("execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..))") public void haha(){ System.out.println("男孩女孩都買本身喜歡的東西"); } }
這個類,咱們使用了註解 @Component
代表它將做爲一個Spring Bean 被裝配,使用註解 @Aspect
表示它是一個切面。
類中只有一個方法 haha
咱們使用 @Before
這個註解,表示他將在方法執行以前執行。關於這個註解後文再做解釋。
參數("execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..))")
聲明瞭切點,代表在該切面的切點是com.sharpcj.aopdemo.test1.Ibuy
這個接口中的buy
方法。至於爲何這麼寫,下文再解釋。 三、 在配置文件中啓用AOP切面功能
package com.sharpcj.aopdemo; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; @Configuration @ComponentScan(basePackageClasses = {com.sharpcj.aopdemo.test1.IBuy.class}) @EnableAspectJAutoProxy(proxyTargetClass = true) public class AppConfig { }
咱們在配置文件類增長了@EnableAspectJAutoProxy
註解,啓用了 AOP 功能,參數proxyTargetClass
的值設爲了 true 。默認值是 false,二者的區別下文再解釋。
OK,下面只需測試代碼,運行結果以下:
咱們看到,結果與咱們需求一致,咱們並無修改 Boy 和 Girl 類的 Buy 方法,也沒有修改測試類的代碼,幾乎是徹底無侵入式地實現了需求。這就是 AOP 的「神奇」之處。
Spring AOP 所支持的 AspectJ 切點指示器
在spring中嘗試使用AspectJ其餘指示器時,將會拋出IllegalArgumentException異常。
當咱們查看上面展現的這些spring支持的指示器時,注意只有execution指示器是惟一的執行匹配,而其餘的指示器都是用於限制匹配的。這說明execution指示器是咱們在編寫切點定義時最主要使用的指示器,在此基礎上,咱們使用其餘指示器來限制所匹配的切點。
下圖的切點表達式表示當Instrument的play方法執行時會觸發通知。
咱們使用execution指示器選擇Instrument的play方法,方法表達式以 *
號開始,標識咱們不關心方法的返回值類型。而後咱們指定了全限定類名和方法名。對於方法參數列表,咱們使用 ..
標識切點選擇任意的play方法,不管該方法的入參是什麼。
多個匹配之間咱們可使用連接符 &&
、||
、!
來表示 「且」、「或」、「非」的關係。可是在使用 XML 文件配置時,這些符號有特殊的含義,因此咱們使用 「and」、「or」、「not」來表示。
舉例:
限定該切點僅匹配的包是 com.sharpcj.aopdemo.test1
,可使用 execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..)) && within(com.sharpcj.aopdemo.test1.*)
在切點中選擇 bean,可使用
execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..)) && bean(girl)
修改 BuyAspectJ.java
package com.sharpcj.aopdemo.test1; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; @Aspect @Component public class BuyAspectJ { @Before("execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..)) && within(com.sharpcj.aopdemo.test1.*) && bean(girl)") public void hehe(){ System.out.println("男孩女孩都買本身喜歡的東西"); } }
此時,切面只會對 Girl.java
這個類生效,執行結果:
細心的你,可能發現了,切面中的方法名,已經被我悄悄地從haha
改爲了hehe
,絲毫沒有影響結果,說明方法名沒有影響。和 Spring IOC 中用 java 配置文件裝配 Bean 時,用@Bean
註解修飾的方法名同樣,沒有影響。
Spring AOP 中有 5 中通知類型,分別以下:
下面修改切面類:
package com.sharpcj.aopdemo.test1; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; @Aspect @Component public class BuyAspectJ { @Before("execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..))") public void hehe() { System.out.println("before ..."); } @After("execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..))") public void haha() { System.out.println("After ..."); } @AfterReturning("execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..))") public void xixi() { System.out.println("AfterReturning ..."); } @Around("execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..))") public void xxx(ProceedingJoinPoint pj) { try { System.out.println("Around aaa ..."); pj.proceed(); System.out.println("Around bbb ..."); } catch (Throwable throwable) { throwable.printStackTrace(); } } }
爲了方便看效果,咱們測試類中,只要 Boy 類:
package com.sharpcj.aopdemo; import com.sharpcj.aopdemo.test1.Boy; import com.sharpcj.aopdemo.test1.Girl; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class AppTest { public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); Boy boy = context.getBean("boy",Boy.class); Girl girl = (Girl) context.getBean("girl"); boy.buy(); // girl.buy(); } }
執行結果以下:
結果顯而易見。指的注意的是 @Around
修飾的環繞通知類型,是將整個目標方法封裝起來了,在使用時,咱們傳入了 ProceedingJoinPoint
類型的參數,這個對象是必需要有的,而且須要調用 ProceedingJoinPoint
的 proceed()
方法。 若是沒有調用 該方法,執行結果爲 :
Around aaa ... Around bbb ... After ... AfterReturning ...
可見,若是不調用該對象的 proceed() 方法,表示原目標方法被阻塞調用,固然也有可能你的實際需求就是這樣。
如你看到的,上面咱們寫的多個通知使用了相同的切點表達式,對於像這樣頻繁出現的相同的表達式,咱們可使用 @Pointcut
註解聲明切點表達式,而後使用表達式,修改代碼以下:
BuyAspectJ.java
package com.sharpcj.aopdemo.test1; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; @Aspect @Component public class BuyAspectJ { @Pointcut("execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..))") public void point(){} @Before("point()") public void hehe() { System.out.println("before ..."); } @After("point()") public void haha() { System.out.println("After ..."); } @AfterReturning("point()") public void xixi() { System.out.println("AfterReturning ..."); } @Around("point()") public void xxx(ProceedingJoinPoint pj) { try { System.out.println("Around aaa ..."); pj.proceed(); System.out.println("Around bbb ..."); } catch (Throwable throwable) { throwable.printStackTrace(); } } }
程序運行結果沒有變化。
這裏,咱們使用
@Pointcut("execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..))") public void point(){}
聲明瞭一個切點表達式,該方法 point 的內容並不重要,方法名也不重要,實際上它只是做爲一個標識,供通知使用。
上面的例子,咱們要進行加強處理的目標方法沒有參數,下面咱們來講說有參數的狀況,而且在加強處理中使用該參數。 下面咱們給接口增長一個參數,表示購買所花的金錢。經過AOP 加強處理,若是女孩買衣服超過了 68 元,就能夠贈送一雙襪子。 更改代碼以下: IBuy.java
package com.sharpcj.aopdemo.test1; public interface IBuy { String buy(double price); }
Girl.java
package com.sharpcj.aopdemo.test1; import org.springframework.stereotype.Component; @Component public class Girl implements IBuy { @Override public String buy(double price) { System.out.println(String.format("女孩花了%s元買了一件漂亮的衣服", price)); return "衣服"; } }
Boy.java
package com.sharpcj.aopdemo.test1; import org.springframework.stereotype.Component; @Component public class Boy implements IBuy { @Override public String buy(double price) { System.out.println(String.format("男孩花了%s元買了一個遊戲機", price)); return "遊戲機"; } }
再看 BuyAspectJ 類,咱們將以前的通知都註釋掉。用一個環繞通知來實現這個功能:
package com.sharpcj.aopdemo.test1; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; @Aspect @Component public class BuyAspectJ { /* @Pointcut("execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..))") public void point(){} @Before("point()") public void hehe() { System.out.println("before ..."); } @After("point()") public void haha() { System.out.println("After ..."); } @AfterReturning("point()") public void xixi() { System.out.println("AfterReturning ..."); } @Around("point()") public void xxx(ProceedingJoinPoint pj) { try { System.out.println("Around aaa ..."); pj.proceed(); System.out.println("Around bbb ..."); } catch (Throwable throwable) { throwable.printStackTrace(); } } */ @Pointcut("execution(String com.sharpcj.aopdemo.test1.IBuy.buy(double)) && args(price) && bean(girl)") public void gif(double price) { } @Around("gif(price)") public String hehe(ProceedingJoinPoint pj, double price){ try { pj.proceed(); if (price > 68) { System.out.println("女孩買衣服超過了68元,贈送一雙襪子"); return "衣服和襪子"; } } catch (Throwable throwable) { throwable.printStackTrace(); } return "衣服"; } }
前文提到,當不關心方法返回值的時候,咱們在編寫切點指示器的時候使用了 *
, 當不關心方法參數的時候,咱們使用了 ..
。如今若是咱們須要傳入參數,而且有返回值的時候,則須要使用對應的類型。在編寫通知的時候,咱們也須要聲明對應的返回值類型和參數類型。
測試類:AppTest.java
package com.sharpcj.aopdemo; import com.sharpcj.aopdemo.test1.Boy; import com.sharpcj.aopdemo.test1.Girl; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class AppTest { public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); Boy boy = context.getBean("boy",Boy.class); Girl girl = (Girl) context.getBean("girl"); String boyBought = boy.buy(35); String girlBought = girl.buy(99.8); System.out.println("男孩買到了:" + boyBought); System.out.println("女孩買到了:" + girlBought); } }
測試結果:
能夠看到,咱們成功經過 AOP 實現了需求,並將結果打印了出來。
前面還有一個遺留問題,在配置文件中,咱們用註解 @EnableAspectJAutoProxy()
啓用Spring AOP 的時候,咱們給參數 proxyTargetClass
賦值爲 true
,若是咱們不寫參數,默認爲 false。這個時候運行程序,程序拋出異常
這是一個強制類型轉換異常。爲何會拋出這個異常呢?或許已經可以想到,這跟Spring AOP 動態代理的機制有關,這個 proxyTargetClass
參數決定了代理的機制。當這個參數爲 false 時, 經過jdk的基於接口的方式進行織入,這時候代理生成的是一個接口對象,將這個接口對象強制轉換爲實現該接口的一個類,天然就拋出了上述類型轉換異常。 反之,proxyTargetClass
爲 true
,則會使用 cglib 的動態代理方式。這種方式的缺點是拓展類的方法被final
修飾時,沒法進行織入。
測試一下,咱們將 proxyTargetClass
參數設爲 true
,同時將 Girl.java 的 Buy 方法用 final
修飾:
AppConfig.java
package com.sharpcj.aopdemo; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; @Configuration @ComponentScan(basePackageClasses = {com.sharpcj.aopdemo.test1.IBuy.class}) @EnableAspectJAutoProxy(proxyTargetClass = true) public class AppConfig { }
Girl.java
package com.sharpcj.aopdemo.test1; import org.springframework.stereotype.Component; @Component public class Girl implements IBuy { @Override public final String buy(double price) { System.out.println(String.format("女孩花了%s元買了一件漂亮的衣服", price)); return "衣服"; } }
此時運行結果:
能夠看到,咱們的切面並無織入生效。
前面的示例中,咱們已經展現瞭如何經過註解配置去聲明切面,下面咱們看看如何在 XML 文件中聲明切面。下面先列出 XML 中聲明 AOP 的經常使用元素:
咱們依然可使用 <aop:aspectj-autoproxy>
元素,他可以自動代理AspectJ註解的通知類。
在XML配置文件中,切點指示器表達式與經過註解配置的寫法基本一致,區別前面有提到,即XML文件中須要使用 「and」、「or」、「not」來表示 「且」、「或」、「非」的關係。
下面咱們不使用任何註解改造上面的例子:
BuyAspectJ.java
package com.sharpcj.aopdemo.test2; import org.aspectj.lang.ProceedingJoinPoint; public class BuyAspectJ { public void hehe() { System.out.println("before ..."); } public void haha() { System.out.println("After ..."); } public void xixi() { System.out.println("AfterReturning ..."); } public void xxx(ProceedingJoinPoint pj) { try { System.out.println("Around aaa ..."); pj.proceed(); System.out.println("Around bbb ..."); } catch (Throwable throwable) { throwable.printStackTrace(); } } }
在 Resource 目錄下新建一個配置文件 aopdemo.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/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <bean id="boy" class="com.sharpcj.aopdemo.test2.Boy"></bean> <bean id="girl" class="com.sharpcj.aopdemo.test2.Girl"></bean> <bean id="buyAspectJ" class="com.sharpcj.aopdemo.test2.BuyAspectJ"></bean> <aop:config proxy-target-class="true"> <aop:aspect id="qiemian" ref="buyAspectJ"> <aop:before pointcut="execution(* com.sharpcj.aopdemo.test2.IBuy.buy(..))" method="hehe"/> <aop:after pointcut="execution(* com.sharpcj.aopdemo.test2.IBuy.buy(..))" method="haha"/> <aop:after-returning pointcut="execution(* com.sharpcj.aopdemo.test2.IBuy.buy(..))" method="xixi"/> <aop:around pointcut="execution(* com.sharpcj.aopdemo.test2.IBuy.buy(..))" method="xxx"/> </aop:aspect> </aop:config> </beans>
這裏分別定義了一個切面,裏面包含四種類型的通知。 測試文件中,使用
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("aopdemo.xml");
來獲取 ApplicationContext,其它代碼不變。
對於頻繁重複使用的切點表達式,咱們也能夠聲明成切點。 配置文件以下:aopdemo.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/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <bean id="boy" class="com.sharpcj.aopdemo.test2.Boy"></bean> <bean id="girl" class="com.sharpcj.aopdemo.test2.Girl"></bean> <bean id="buyAspectJ" class="com.sharpcj.aopdemo.test2.BuyAspectJ"></bean> <aop:config proxy-target-class="true"> <aop:pointcut id="apoint" expression="execution(* com.sharpcj.aopdemo.test2.IBuy.buy(..))"/> <aop:aspect id="qiemian" ref="buyAspectJ"> <aop:before pointcut-ref="apoint" method="hehe"/> <aop:after pointcut-ref="apoint" method="haha"/> <aop:after-returning pointcut-ref="apoint" method="xixi"/> <aop:around pointcut-ref="apoint" method="xxx"/> </aop:aspect> </aop:config> </beans>
BuyAspectJ.java
package com.sharpcj.aopdemo.test2; import org.aspectj.lang.ProceedingJoinPoint; public class BuyAspectJ { public String hehe(ProceedingJoinPoint pj, double price){ try { pj.proceed(); if (price > 68) { System.out.println("女孩買衣服超過了68元,贈送一雙襪子"); return "衣服和襪子"; } } catch (Throwable throwable) { throwable.printStackTrace(); } return "衣服"; } }
aopdemo.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/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <bean id="boy" class="com.sharpcj.aopdemo.test2.Boy"></bean> <bean id="girl" class="com.sharpcj.aopdemo.test2.Girl"></bean> <bean id="buyAspectJ" class="com.sharpcj.aopdemo.test2.BuyAspectJ"></bean> <aop:config proxy-target-class="true"> <aop:pointcut id="apoint" expression="execution(String com.sharpcj.aopdemo.test2.IBuy.buy(double)) and args(price) and bean(girl)"/> <aop:aspect id="qiemian" ref="buyAspectJ"> <aop:around pointcut-ref="apoint" method="hehe"/> </aop:aspect> </aop:config> </beans>
同註解配置相似,
CGlib 代理方式:
<aop:config proxy-target-class="true"> </aop:config>
JDK 代理方式:
<aop:config proxy-target-class="false"> </aop:config>
本文簡單記錄了 AOP 的編程思想,而後介紹了 Spring 中 AOP 的相關概念,以及經過註解方式和XML配置文件兩種方式使用 Spring AOP進行編程。 相比於 AspectJ 的面向切面編程,Spring AOP 也有一些侷限性,可是已經能夠解決開發中的絕大多數問題了,若是確實遇到了 Spring AOP 解決不了的場景,咱們依然能夠在 Spring 中使用 AspectJ 來解決。
原文出處:https://www.cnblogs.com/joy99/p/10941543.html