@AspectJ相關文章
《spring AOP 之二:@AspectJ注解的3種配置》
《spring AOP 之三:使用@AspectJ定義切入點》
《spring AOP 之四:@AspectJ切入點標識符語法詳解》
與 AspectJ 相同的是,Spring AOP 同樣需要對目標類進行增強,也就是生成新的 AOP 代理類;與 AspectJ 不同的是,Spring AOP 無需使用任何特殊命令對 Java 源代碼進行編譯,它采用運行時動態地、在內存中臨時生成“代理類”的方式來生成 AOP 代理。
Spring 允許使用 AspectJ Annotation 用於定義方面(Aspect)、切入點(Pointcut)和增強處理(Advice),Spring 框架則可識別並根據這些 Annotation 來生成 AOP 代理。Spring 只是使用了和 AspectJ 5 一樣的注解,但並沒有使用 AspectJ 的編譯器或者織入器(Weaver),底層依然使用的是 Spring AOP,依然是在運行時動態生成 AOP 代理,並不依賴於 AspectJ 的編譯器或者織入器。
簡單地說,Spring 依然采用運行時生成動態代理的方式來增強目標對象,所以它不需要增加額外的編譯,也不需要 AspectJ 的織入器支持;而 AspectJ 在采用編譯時增強,所以 AspectJ 需要使用自己的編譯器來編譯 Java 文件,還需要織入器。
為了啟用 Spring 對 @AspectJ 方面配置的支持,並保證 Spring 容器中的目標 Bean 被一個或多個方面自動增強,必須在 Spring 配置文件中配置如下片段,
AOP的作用這里就不再作說明了,下面開始講解一個很簡單的入門級例子。
引用一個猴子偷桃,守護者守護果園抓住猴子的小情節。
1、猴子偷桃類(普通類):
2、守護者類(聲明為Aspect):
3、XML配置文件:
4、測試類:
5、控制台輸出:
解說:
1寫了一個猴子正在偷桃的方法。
2寫了一個標志為@Aspect的類,它是守護者。它會在猴子偷桃之前發現猴子,並在猴子偷桃之后抓住猴子。
原理:
A、@Aspect的聲明表示這是一個切面類。
B、@Pointcut使用這個方法可以將com.samter.common.Monkey.stealPeaches(..)方法聲明為poincut即切入點。作用,在stealPeaches方法被調用的時候執行2的foundMonkey方法。其中execution是匹配方法執行的切入點,也就是spring最常用的切入點定義方式。
C、@Before(value="foundMonkey()"):@Before聲明為在切入點方法執行之前執行,而后面沒有直接聲明切入點,而是value="foundMonkey()",是因為如果@afterReturning等都有所改動的時候都必須全部改動,所以統一用Pointcut的foundMonkey代替,這樣子有改動的時候僅需改動一個地方。其他@AfterReturning類同。
3是xml配置文件,里面有具體的注釋。
特別說明:Guardian類里面的@Pointcut("execution(* com.samter.common.Monkey.stealPeaches(..))"),如果stealPeaches有參數則..表示所有參數,@AfterReturning("foundMonkey() && args(name,..)")的&& args(name,..)可以獲取切入點方法stealPeaches的參數。
總結:這里列舉了一個簡單的例子,但是不難引申到應用中,當你寫一個登陸系統的時候,你或許要記錄誰成功登陸了系統,誰登陸系統密碼錯誤等等的信息,這樣子你用切面是再合適不過的了,總之當你的事務邏輯都設計到日志、安全檢查、事務管理等等共同的內容的時候,用切面是要比你沒有一個事務邏輯類都有相關代碼或者相關引用好得多。
一、@AspectJ的AOP功能的配置方式有三種:
1、通過 Spring 的 XML Schema 配置方式:
<?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:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" <!--schema方式配置--> xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd"> <!-- 啟動 @AspectJ 支持 --> <aop:aspectj-autoproxy /> <bean id="horseman" class="com.dxz.aop.demo4.Horseman" /> <bean id="swordman" class="com.dxz.aop.demo4.Swordman" /> <bean class="com.dxz.aop.demo4.StorageAdvisor" /> </beans>
當然,如果我們希望完全啟動 Spring 的“零配置”功能,則還需要啟用 Spring 的“零配置”支持,讓 Spring 自動搜索指定路徑下 Bean 類。
所謂自動增強,指的是 Spring 會判斷一個或多個方面是否需要對指定 Bean 進行增強,並據此自動生成相應的代理,從而使得增強處理在合適的時候被調用。
如果不打算使用 Spring 的 XML Schema 配置方式,則應該在 Spring 配置文件中增加如下片段來啟用 @AspectJ 支持。
2、通過<bean class="org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator"/>啟動@AspectJ(此時schema中不需要相關的AOP配置)
<?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:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!-- 啟動@AspectJ支持 --> <bean class="org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator"/> <!-- 啟動@AspectJ支持 效果同上,若兩個同時添加即會執行兩次before方法--> <!-- <aop:aspectj-autoproxy/> --> <bean id="horseman" class="com.dxz.aop.demo4.Horseman" /> <bean id="swordman" class="com.dxz.aop.demo4.Swordman" /> <bean class="com.dxz.aop.demo4.StorageAdvisor" /> </beans>
上面配置文件中的 AnnotationAwareAspectJAutoProxyCreator 是一個 Bean 后處理器(BeanPostProcessor),該 Bean 后處理器將會為容器中 Bean 生成 AOP 代理,當啟動了 @AspectJ 支持后,只要我們在 Spring 容器中配置一個帶 @Aspect 注釋的 Bean,Spring 將會自動識別該 Bean,並將該 Bean 作為方面 Bean 處理。
在 Spring 容器中配置方面 Bean(即帶 @Aspect 注釋的 Bean),與配置普通 Bean 沒有任何區別,一樣使用 <bean.../> 元素進行配置,一樣支持使用依賴注入來配置屬性值;如果我們啟動了 Spring 的“零配置”特性,一樣可以讓 Spring 自動搜索,並裝載指定路徑下的方面 Bean。
3、@EnableAspectJAutoProxy(@Configuration注解)spring3.1及以上版本
不用xml配置文件的情況下,通過@Configuration來裝配Spring bean,@EnableAspectJAutoProxy來啟動spring AOP功能。見《Spring 3.1新特性之二:@Enable*注解的源碼,spring源碼分析之定時任務Scheduled注解》
二、示例
2.1、xml schema + @AspectJ實現spring AOP(cglib)
使用 @Aspect 標注一個 Java 類,該 Java 類將會作為方面 Bean,如下面代碼片段所示:
package com.dxz.aop.demo6; import org.aspectj.lang.annotation.Aspect; // 使用 @Aspect 定義一個方面類 @Aspect public class LogAspect { // 定義該類的其他內容 //... }
方面類(用 @Aspect 修飾的類)和其他類一樣可以有方法、屬性定義,還可能包括切入點、增強處理定義。
當我們使用 @Aspect 來修飾一個 Java 類之后,Spring 將不會把該 Bean 當成組件 Bean 處理,因此負責自動增強的后處理 Bean 將會略過該 Bean,不會對該 Bean 進行任何增強處理。
開發時無須擔心使用 @Aspect 定義的方面類被增強處理,當 Spring 容器檢測到某個 Bean 類使用了 @Aspect 標注之后,Spring 容器不會對該 Bean 類進行增強。
下面將會考慮采用 Spring AOP 來改寫前面介紹的例子:
下面例子使用一個簡單的 Chinese 類來模擬業務邏輯組件:
Chinese.java
package com.dxz.aop.demo6; import org.springframework.stereotype.Component; @Component public class Chinese { // 實現 Person 接口的 sayHello() 方法 public String sayHello(String name) { String ret = name + " Hello , Spring AOP"; System.out.println(ret); return ret; } // 定義一個 eat() 方法 public void eat(String food) { System.out.println("我正在吃 :" + food); } }
提供了上面 Chinese 類之后,接下來假設同樣需要為上面 Chinese 類的每個方法增加事務控制、日志記錄,此時可以考慮使用 Around、AfterReturning 兩種增強處理。
先看 AfterReturning 增強處理代碼。
AfterReturningAdviceTest.java
package com.dxz.aop.demo6; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; // 定義一個方面 @Aspect public class AfterReturningAdviceTest { // 匹配 com.dxz.aop.demo6 包下所有類的下的所有方法的執行作為切入點 @AfterReturning(returning = "rvt", pointcut = "execution(* com.dxz.aop.demo6.*.*(..))") public void log(Object rvt) { System.out.println("AfterReturningAdviceTest==獲取目標方法返回值 :" + rvt); } }
上面 Aspect 類使用了 @Aspect 修飾,這樣 Spring 會將它當成一個方面 Bean 進行處理。其中程序中粗體字代碼指定將會在調用 org.crazyit.app.service.impl 包下的所有類的所有方法之后織入 log(Object rvt) 方法。
再看 Around 增強處理代碼:
package com.dxz.aop.demo6; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; // 定義一個方面 @Aspect public class AroundAdviceTest { // 匹配 com.dxz.aop.demo6 包下所有類的下的所有方法的執行作為切入點 @Around("execution(* com.dxz.aop.demo6.*.*(..))") public Object processTx(ProceedingJoinPoint jp) throws java.lang.Throwable { System.out.println("AroundAdviceTest==執行目標方法之前,模擬開始事務 ..."); // 執行目標方法,並保存目標方法執行后的返回值 Object rvt = jp.proceed(new String[] { "被改變的參數" }); System.out.println("AroundAdviceTest==執行目標方法之后,模擬結束事務 ..."); return rvt + " 新增的內容"; } }
與前面的 AfterReturning 增強處理類似的,此處同樣使用了 @Aspect 來修飾前面 Bean,其中粗體字代碼指定在調用com.dxz.aop.demo6 包下的所有類的所有方法的“前后(Around)” 織入 processTx(ProceedingJoinPoint jp) 方法需要指出的是,雖然此處只介紹了 Spring AOP 的 AfterReturning、Around 兩種增強處理,但實際上 Spring 還支持 Before、After、AfterThrowing 等增強處理,關於 Spring AOP 編程更多、更細致的編程細節,可以參考《輕量級 Java EE 企業應用實戰》一書。
本示例采用了 Spring 的零配置來開啟 Spring AOP,因此上面 Chinese 類使用了 @Component 修飾,而方面 Bean 則使用了 @Aspect 修飾,方面 Bean 中的 Advice 則分別使用了 @AfterReturning、@Around 修飾。接下來只要為 Spring 提供如下配置文件即可:
applicationContext-aop6.xml
<?xml version="1.0" encoding="GBK"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd"> <!-- 指定自動搜索 Bean 組件、自動搜索方面類 --> <context:component-scan base-package="com.dxz.aop.demo6"> <context:include-filter type="annotation" expression="org.aspectj.lang.annotation.Aspect" /> </context:component-scan> <!-- 啟動 @AspectJ 支持 --> <aop:aspectj-autoproxy /> </beans>
接下來按傳統方式來獲取 Spring 容器中 chinese Bean、並調用該 Bean 的兩個方法,程序代碼如下:
BeanTest.java
package com.dxz.aop.demo6; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class BeanTest { public static void main(String[] args) { // 創建 Spring 容器 ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext-aop6.xml"); Chinese p = ctx.getBean("chinese", Chinese.class); System.out.println(p.sayHello("張三")); p.eat("西瓜"); } }
從上面開發過程可以看出,對於 Spring AOP 而言,開發者提供的業務組件、方面 Bean 並沒有任何特別的地方。只是方面 Bean 需要使用 @Aspect 修飾即可。程序不需要使用特別的編譯器、織入器進行處理。
運行上面程序,將可以看到如下執行結果:
雖然程序是在調用 Chinese 對象的 sayHello、eat 兩個方法,但從上面運行結果不難看出:實際執行的絕對不是 Chinese 對象的方法,而是 AOP 代理的方法。也就是說,Spring AOP 同樣為 Chinese 類生成了 AOP 代理類。這一點可通過在程序中增加如下代碼看出:
System.out.println(p.getClass());
上面代碼可以輸出 p 變量所引用對象的實現類,再次執行程序將可以看到上面代碼產生class com.dxz.aop.demo6.Chinese$$EnhancerBySpringCGLIB$$7d0b6d20的輸出,這才是 p 變量所引用的對象的實現類,這個類也就是 Spring AOP 動態生成的 AOP 代理類。從 AOP 代理類的類名可以看出,AOP 代理類是由 CGLIB 來生成的。
2.2、xml schema + @AspectJ實現spring AOP(jdk動態代理實現)
如果將上面程序程序稍作修改:只要讓上面業務邏輯類 Chinese 類實現一個任意接口——這種做法更符合 Spring 所倡導的“面向接口編程”的原則。假設程序為 Chinese 類提供如下 Person 接口,並讓 Chinese 類實現該接口:
Person.java
package com.dxz.aop.demo6; public interface Person { String sayHello(String name); void eat(String food); }
Chinese修改實現Person接口:
@Component public class Chinese implements Person {
接下來讓 BeanTest 類面向 Person 接口、而不是 Chinese 類編程。即將 BeanTest 類改為如下形式:
BeanTest.java
package com.dxz.aop.demo6; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class BeanTest { public static void main(String[] args) { // 創建 Spring 容器 ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext-aop6.xml"); Person p = ctx.getBean("chinese", Person.class); System.out.println(p.sayHello("張三")); p.eat("西瓜"); System.out.println(p.getClass()); } }
原來的程序是將面向 Chinese 類編程,現在將該程序改為面向 Person 接口編程,再次運行該程序,程序運行結果沒有發生改變。只是 System.out.println(p.getClass()); 將會輸出 class com.sun.proxy.$Proxy10,這說明此時的 AOP 代理並不是由 CGLIB 生成的,而是由 JDK 動態代理生成的。
Spring AOP 框架對 AOP 代理類的處理原則是:如果目標對象的實現類實現了接口,Spring AOP 將會采用 JDK 動態代理來生成 AOP 代理類;如果目標對象的實現類沒有實現接口,Spring AOP 將會采用 CGLIB 來生成 AOP 代理類——不過這個選擇過程對開發者完全透明、開發者也無需關心。
Spring AOP 會動態選擇使用 JDK 動態代理、CGLIB 來生成 AOP 代理,如果目標類實現了接口,Spring AOP 則無需 CGLIB 的支持,直接使用 JDK 提供的 Proxy 和 InvocationHandler 來生成 AOP 代理即可。關於如何 Proxy 和 InvocationHandler 來生成動態代理不在本文介紹范圍之內,如果讀者對 Proxy 和 InvocationHandler 的用法感興趣則可自行參考 Java API 文檔或《瘋狂 Java 講義》。
2.3、@Configuration+@EnableAspectJAutoProxy實現
在spring3.1及以上,spring可以不用xml配置裝載bean了,在@Configuration注解的環境里,可以通過@EnableAspectJAutoProxy啟動spring AOP功能。微調上面的示例如下:
在@Aspect中
package com.dxz.aop.demo6; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; import org.springframework.context.annotation.Configuration; // 定義一個方面 @Aspect @Configuration public class AfterReturningAdviceTest { // 匹配 com.dxz.aop.demo6 包下所有類的下的所有方法的執行作為切入點 @AfterReturning(returning = "rvt", pointcut = "execution(* com.dxz.aop.demo6.*.*(..))") public void log(Object rvt) { System.out.println("AfterReturningAdviceTest==獲取目標方法返回值 :" + rvt); } }
增加spring配置類
package com.dxz.aop.demo6; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.context.annotation.Import; @Configuration @ComponentScan @EnableAspectJAutoProxy @Import({AfterReturningAdviceTest.class})/*@Aspect可以生效,相當於Configuration類作用,都是配置類*/ public class AppConfig { @Bean(name = "chinese") public Chinese chinese() { return new Chinese(); } }
啟動類:
package com.dxz.aop.demo6; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class Test6 { public static void main(String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); Person outPut = (Person) context.getBean("chinese"); outPut.sayHello("duan"); } }
結果:
信息: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@c4437c4: startup date [Tue Dec 26 17:03:32 CST 2017]; root of context hierarchy AfterReturningAdviceTest==獲取目標方法返回值 :com.dxz.aop.demo6.Chinese@72967906 duan Hello , Spring AOP AfterReturningAdviceTest==獲取目標方法返回值 :duan Hello , Spring AOP
三、注解
2 注解說明
2.1 @Aspect
作用是把當前類標識為一個切面供容器讀取
2.2 @Before
標識一個前置增強方法,相當於BeforeAdvice的功能,相似功能的還有
2.3 @AfterReturning
后置增強,相當於AfterReturningAdvice,方法正常退出時執行
2.4 @AfterThrowing
異常拋出增強,相當於ThrowsAdvice
2.5 @After
final增強,不管是拋出異常或者正常退出都會執行
2.6 @Around
環繞增強,相當於MethodInterceptor
2.7 @DeclareParents
引介增強,相當於IntroductionInterceptor
3 execution切點函數
execution函數用於匹配方法執行的連接點,語法為:
execution(方法修飾符(可選) 返回類型 方法名 參數 異常模式(可選))
參數部分允許使用通配符:
* 匹配任意字符,但只能匹配一個元素
.. 匹配任意字符,可以匹配任意多個元素,表示類時,必須和*聯合使用
+ 必須跟在類名后面,如Horseman+,表示類本身和繼承或擴展指定類的所有類
示例中的* chop(..)解讀為:
方法修飾符 無
返回類型 *匹配任意數量字符,表示返回類型不限
方法名 chop表示匹配名稱為chop的方法
參數 (..)表示匹配任意數量和類型的輸入參數
異常模式 不限
更多示例:
void chop(String,int)
匹配目標類任意修飾符方法、返回void、方法名chop、帶有一個String和一個int型參數的方法
public void chop(*)
匹配目標類public修飾、返回void、方法名chop、帶有一個任意類型參數的方法
public String *o*(..)
匹配目標類public修飾、返回String類型、方法名中帶有一個o字符、帶有任意數量任意類型參數的方法
public void *o*(String,..)
匹配目標類public修飾、返回void、方法名中帶有一個o字符、帶有任意數量任意類型參數,但第一個參數必須有且為String型的方法
也可以指定類:
public void examples.chap03.Horseman.*(..)
匹配Horseman的public修飾、返回void、不限方法名、帶有任意數量任意類型參數的方法
public void examples.chap03.*man.*(..)
匹配以man結尾的類中public修飾、返回void、不限方法名、帶有任意數量任意類型參數的方法
指定包:
public void examples.chap03.*.chop(..)
匹配examples.chap03包下所有類中public修飾、返回void、方法名chop、帶有任意數量任意類型參數的方法
public void examples..*.chop(..)
匹配examples.包下和所有子包中的類中public修飾、返回void、方法名chop、帶有任意數量任意類型參數的方法
可以用這些表達式替換StorageAdvisor中的代碼並觀察效果
4 更多切點函數
除了execution(),Spring中還支持其他多個函數,這里列出名稱和簡單介紹,以方便根據需要進行更詳細的查詢
4.1 @annotation()
表示標注了指定注解的目標類方法
例如 @annotation(org.springframework.transaction.annotation.Transactional) 表示標注了@Transactional的方法
4.2 args()
通過目標類方法的參數類型指定切點
例如 args(String) 表示有且僅有一個String型參數的方法
4.3 @args()
通過目標類參數的對象類型是否標注了指定注解指定切點
如 @args(org.springframework.stereotype.Service) 表示有且僅有一個標注了@Service的類參數的方法
4.4 within()
通過類名指定切點
如 with(examples.chap03.Horseman) 表示Horseman的所有方法
4.5 target()
通過類名指定,同時包含所有子類
如 target(examples.chap03.Horseman) 且Elephantman extends Horseman,則兩個類的所有方法都匹配
4.6 @within()
匹配標注了指定注解的類及其所有子類
如 @within(org.springframework.stereotype.Service) 給Horseman加上@Service標注,則Horseman和Elephantman 的所有方法都匹配
4.7 @target()
所有標注了指定注解的類
如 @target(org.springframework.stereotype.Service) 表示所有標注了@Service的類的所有方法
4.8 this()
大部分時候和target()相同,區別是this是在運行時生成代理類后,才判斷代理類與指定的對象類型是否匹配
5 邏輯運算符
表達式可由多個切點函數通過邏輯運算組成
5.1 &&
與操作,求交集,也可以寫成and
例如 execution(* chop(..)) && target(Horseman) 表示Horseman及其子類的chop方法
5.2 ||
或操作,求並集,也可以寫成or
例如 execution(* chop(..)) || args(String) 表示名稱為chop的方法或者有一個String型參數的方法
5.3 !
非操作,求反集,也可以寫成not
例如 execution(* chop(..)) and !args(String) 表示名稱為chop的方法但是不能是只有一個String型參數的方法