Spring(8)使用@AspectJ註解開發Spring AOP(一)
1.選擇連線點
Spring是方法級別的AOP框架,我們主要是以某個類的某個方法作為連線點,用動態代理的理論來說,就是要攔截哪個方法織入對應的AOP通知。
(1)建立介面
package com.xhbjava.service; import com.xhbjava.pojo.Account; /** * 賬戶的業務層介面 * * @author mr.wang * */ public interface IAccountService { public void printAccount(Account account); }
(2)定義實現類
packagecom.xhbjava.service.impl; import org.springframework.stereotype.Component; import com.xhbjava.pojo.Account; import com.xhbjava.service.IAccountService; /** * 賬戶業務層介面實現類 * * @author mr.wang * */ @Component public class AccountServiceImpl implements IAccountService { @Override public voidprintAccount(Account account) { System.out.println("{id:" + account.getId() + "," + "accountName:" + account.getName() + "," + "accountMoney:" + account.getMoney() + "}"); } }
(3)選擇連線點
我們在上面的實現類中把printAccount作為AOP的連線點,那麼在用動態代理的時候就是要為類AccountServiceImpl生成代理物件,然後攔截printAccount方法,於是可以產生AOP的通知方法。
2.建立切面
(1)建立切面
我們在選擇好了連線點後就可以建立切面了,對於動態代理的概念而言,它如同一個攔截器,在Spring中只要使用@Aspect註解一個類就表示Spring IoC容器就會認為這是一個切面,切面定義如下:
pom.xml新增:
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.4</version> </dependency>
package com.xhbjava.aspect; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; @Aspect public class AccountAspect { @Before("execution(*com.xhbjava.service.impl.AccountServiceImpl.printAccount(..))") public void before() { System.out.println("Before...."); } @After("execution(*com.xhbjava.service.impl.AccountServiceImpl.printAccount(..))") public void after() { System.out.println("After...."); } @AfterReturning("execution(*com.xhbjava.service.impl.AccountServiceImpl.printAccount(..))") public void afterReturning() { System.out.println("afterReturning...."); } @AfterThrowing("execution(*com.xhbjava.service.impl.AccountServiceImpl.printAccount(..))") public void afterThrowing() { System.out.println("afterThrowing..."); } }
(2)AspectJ註解
註解 | 通知 | 備註 |
@Before | 在被代理的物件方法前呼叫 | 前置通知 |
@Around | 將被代理物件的方法封裝起來,並用環繞通知取代它 | 環繞通知,它將覆蓋原有方法,但是允許通過反射呼叫原有方法 |
@After | 在被代理物件的方法後呼叫 | 後置通知 |
@AfterReturning | 在被代理物件的方法正常返回後呼叫 | 返回通知,要求被代理物件的方法執行過程中沒有發生異常 |
@AfterThrowing | 在被代理物件的方法丟擲異常後呼叫 | 異常通知,要求被代理物件的方法執行過程中產生異常 |
根據該表我們可以知道各個方法執行順序,上面建立切面我們使用了註解對應的正則表示式,這些正則式是切點的問題,也就是我們需要攔截什麼物件的什麼方法。
3.定義切點
(1)學習Spring AOP明確的事
a 、開發階段(我們做的)
編寫核心業務程式碼(開發主線):大部分程式設計師來做,要求熟悉業務需求。
把公用程式碼抽取出來,製作成通知。(開發階段最後再做):AOP 程式設計人員來做。
在配置檔案中,宣告切入點與通知間的關係,即切面。:AOP 程式設計人員來做。
b 、執行階段(Spring 框架完成的)
Spring 框架監控切入點方法的執行。一旦監控到切入點方法被執行,使用代理機制,動態建立目標物件的代理物件,根據通知類別,在代理物件的對應位置,將通知對應的功能織入,完成完整的程式碼邏輯執行。
(2)execution的正則表示式
上面我們沒有詳細討論Spring如何判斷那些方法需要進行攔截,並不是所有方法都需要使用AOP程式設計,這就是連線點的問題。前面的建立切面我們使用正則表示式,Spring通過這個正則表示式判斷是否需要攔截我們的方法。
execution(*com.xhbjava.service.impl.AccountServiceImpl.printAccount(..))
表示式分析:
- execution:代表執行方法的時候會觸發
- *:代表任意返回型別的方法
-
com.xhbjava.service.impl.AccountServiceImpl:代表限定類名
-
printAccount:被攔截的方法名稱
- (..):任意的引數
通過對錶達式分析我們可以看出限定類名為com.xhbjava.service.impl.AccountServiceImpl的類的printAccount方法被攔截了,這樣就按照AOP通知的規則將該方法織入流程中了,我們這只是簡單的描述,詳細配置分析如下:
AspectJ指示器 | 描述 |
arg() | 限制連線點匹配引數為指定型別的方法 |
@args() | 限定連線點匹配引數為制定註解標註的執行方法 |
execution | 用於匹配連線點的執行方法,這是最常見的匹配,可以通過正則表示式進行匹配 |
this() | 限制連線點匹配AOP代理的Bean,引用為指定型別的類 |
target | 限制連線點匹配被代理物件為指定的型別 |
@target() | 限制連線點匹配特定的執行物件,這些物件要符合指定的註解型別 |
within | 限制連線點匹配指定的包 |
@within() | 限制連線點匹配指定的型別 |
@annotation | 限定匹配帶有指定註解的連線點 |
注意:Spring只能支援上面列出的AspectJ的指示器,如果使用了非上面的指示器,那麼會丟擲IllegalArgumentException異常。
在上面的程式碼中我們正則表示式重複寫了多次,比較麻煩,下面我們引入@Pointcut定義一個切點就可以解決問題。程式碼如下:
package com.xhbjava.aop.aspect; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.DeclareParents; import org.aspectj.lang.annotation.Pointcut; import com.xhbjava.service.IAccountService; import com.xhbjava.service.impl.AccountServiceImpl; @Aspect public class AccountAspect { @DeclareParents(value= "com.xhbjava.service.impl.AccountServiceImpl+", defaultImpl=AccountServiceImpl.class) public IAccountService accountService; @Pointcut ("execution(* com.xhbjava.service.impl.AccountServiceImpl.printAccount(..))") public void print(){} @Before("printAccount") public void before() { System.out.println("Before...."); } @After("printAccount") public void after() { System.out.println("After...."); } @AfterReturning("printAccount") public void afterReturning() { System.out.println("afterReturning...."); } @AfterThrowing("printAccount") public void afterThrowing() { System.out.println("afterThrowing..."); } }
4.測試AOP
程式碼如下:
package com.xhbjava.test; 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 com.xhbjava.aop.aspect.AccountAspect; @Configuration @EnableAspectJAutoProxy @ComponentScan("com.xhbjava.aop") public class AopConfig { @Bean public AccountAspect getAccountAspect() { return new AccountAspect(); } }
@EnableAspectJAutoProxy表示啟動AspectJ框架的自動代理,這時Spring自動生成代理物件,進而使用AOP,getAccountAspect方法生成一個切面例項。
測試類:
package com.xhbjava.test; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import com.xhbjava.pojo.Account; import com.xhbjava.service.IAccountService; public class testAOP { private static ApplicationContext contxt; public static void main(String[] args) { contxt = new AnnotationConfigApplicationContext(AopConfig.class); IAccountService accountService = (IAccountService)contxt.getBean(IAccountService.class); Account ac = new Account(); ac.setId(6); ac.setName("aop"); ac.setMoney(12345.90f); accountService.printAccount(ac); System.out.println("-----------------"); ac = null; accountService.printAccount(ac); } }
5.環繞通知
環繞通知是Spring AOP中最強大的通知,它可以同時實現前置通知和後置通知,由於保留了排程代理物件的原有方法的功能,因此它即強大又靈活。由於強大,環繞同時的可控性不那麼強,如果需要大量改變業務邏輯,一般就不需要使用環繞通知了。
我們在上面的切面中加入環繞通知,程式碼示例如下:
@Around("print()") public void around(ProceedingJoinPoint jp) { System.out.println("around before..."); try { jp.proceed(); } catch (Throwable e) { e.printStackTrace(); } System.out.println("around after...."); }
@Around註解表示加入了切面的環繞通知,通知裡面的引數ProceedingJoinPoint由Spring提供,通過這個引數我們可以反射到連線點的方法,我們加入反射連線點後測試結果如下:
6.織入
織入是生成代理物件的過程,Spring AOP中,使用類org.springframework.aop.framework.ProxyFactory作為織入器。Spring AOP是基於代理模式的AOP實現,織入過程完成後,會返回織入了橫切邏輯的目標物件的代理物件。
使用ProxyFactory只需要指定如下兩個最基本的東西:
1,要對其進行織入的目標物件。可以通過構造方法直接傳入,也可以構造完後,通過setter方法設定。
2,將要應用到目標物件的Advisor。
Spring AOP在使用代理模式實現AOP的過程中採用了JDK動態代理和CGLIB兩種機制,分別對實現了某些介面的目標類和沒有實現任何介面的目標類進行代理,所以,在使用ProxyFactory對目標類進行代理的時候,會通過ProxyFactory的某些行為控制屬性對這兩種情況進行區分。如果滿足以下列出的三種情況中的任何一種,ProxyFactory將對目標類進行基於類的代理。
1,如果目標類沒有實現任何介面,不管proxyTargetClass的值是什麼,ProxyFactory會採用基於類的代理。
2,如果ProxyFactory的proxyTargetClass屬性值被設定為true,ProxyFactory會採用基於類的代理。
3,如果ProxyFactory的optimize屬性設定為true,ProxyFactory會採用基於類的代理。
動態代理物件是由Spring IOC容器根據描述生成的,一般不需要修改,在實際程式設計中建議使用介面程式設計,這樣便於使定義和實現向分離,有利於實現變化和替代,更為靈活。
7.給通知傳遞引數
在Spring AOP各類通知中,除了環繞通知外,並沒有討論引數的傳遞,有時候需要進行引數傳遞,在這我們學習引數傳遞。
@Override public void printAccount(Account account, int sort) { System.out.println("{id:" + account.getId() + "," + "accountName:" + account.getName() + "," + "accountMoney:" + account.getMoney() + "}"); System.out.println("sort:"+sort); }
@Pointcut ("execution(* com.xhbjava.aop.service.impl.AccountServiceImpl.printAccount(..))" +"&& args(account,sort)") public void print(){}
package com.xhbjava.aop.aspect; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.DeclareParents; import org.aspectj.lang.annotation.Pointcut; import com.xhbjava.aop.verifier.AccountVerifier; import com.xhbjava.aop.verifier.impl.AccountVerifierImpl; import com.xhbjava.pojo.Account; @Aspect public class AccountAspect { @DeclareParents(value= "com.xhbjava.aop.service.impl.AccountServiceImpl+", defaultImpl=AccountVerifierImpl.class) public AccountVerifier accountVerifier; @Pointcut ("execution(* com.xhbjava.aop.service.impl.AccountServiceImpl.printAccount(..))") public void print(){} @Before("execution(* com.xhbjava.aop.service.impl.AccountServiceImpl.printAccount(..))" +"&& args(account,sort)") public void before(Account account,int sort) { System.out.println("Before...."); } @After("print()") public void after() { System.out.println("After...."); } @AfterReturning("print()") public void afterReturning() { System.out.println("afterReturning...."); } @AfterThrowing("print()") public void afterThrowing() { System.out.println("afterThrowing..."); } @Around("print()") public void around(ProceedingJoinPoint jp) { System.out.println("around before..."); try { jp.proceed(); } catch (Throwable e) { e.printStackTrace(); } System.out.println("around after...."); } }
從測試結果看,引數傳遞是成功的,對於其他通知我們也可以像前置通知那樣去傳遞。
8.引入
Spring AOP只是通過JDK動態代理技術把各類通知織入到它約定的流程中,但是有時我們需要引入其他型別的方法得到更好的實現,比如在printAccount方法中,如果要求賬戶為空我們不進行列印,那麼我們需要引入一個新的檢測器對其進行檢查,程式碼示例如下:
package com.xhbjava.aop.verifier; import com.xhbjava.pojo.Account; public interface AccountVerifier { public boolean verify(Account account); }
package com.xhbjava.aop.verifier.impl; import com.xhbjava.aop.verifier.AccountVerifier; import com.xhbjava.pojo.Account; public class AccountVerifierImpl implements AccountVerifier { @Override public boolean verify(Account account) { System.out.println("------------"); return account != null; } }
在AccountAspect切面中我們加入如下程式碼:
@DeclareParents(value= "com.xhbjava.aop.service.impl.AccountServiceImpl+", defaultImpl=AccountVerifierImpl.class) public AccountVerifier accountVerifier;
@DeclareParents中,value= "com.xhbjava.aop.service.impl.AccountServiceImpl+",表示對AccountServiceImpl類進行增強,也就是在AccountServiceImpl中引入一個新的介面。
defaultImpl:表示預設實現的類,這裡是AccountVerifierImpl,下面我們就可以使用這個方法了。
package com.xhbjava.aop.test; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import com.xhbjava.aop.service.IAccountService; import com.xhbjava.aop.verifier.AccountVerifier; import com.xhbjava.pojo.Account; public class testAOP { private static ApplicationContext contxt; public static void main(String[] args) { contxt = new AnnotationConfigApplicationContext(AopConfig.class); IAccountService accountService = (IAccountService)contxt.getBean(IAccountService.class); AccountVerifier accountVerifier = (AccountVerifier)accountService; Account ac = new Account(); ac.setId(6); ac.setName("aop"); ac.setMoney(12345.90f); if(accountVerifier.verify(ac)) { accountService.printAccount(ac,6); } System.out.println("-----------------"); ac = null; if(accountVerifier.verify(ac)) { accountService.printAccount(ac,6); } //accountService.printAccount(ac); } }