基於注解的Spring AOP開發
簡單案例快速入門
定義目標類接口和實現類
/** * Created by zejian on 2017/2/19.*/ //接口類 public interface UserDao { int addUser(); void updateUser(); void deleteUser(); void findUser(); } //實現類 import com.zejian.spring.springAop.dao.UserDao; import org.springframework.stereotype.Repository; /** * Created by zejian on 2017/2/19.*/ @Repository public class UserDaoImp implements UserDao { @Override public int addUser() { System.out.println("add user ......"); return 6666; } @Override public void updateUser() { System.out.println("update user ......"); } @Override public void deleteUser() { System.out.println("delete user ......"); } @Override public void findUser() { System.out.println("find user ......"); } }
使用Spring 2.0引入的注解方式,編寫Spring AOP的aspect 類:
@Aspect public class MyAspect { /** * 前置通知 */ @Before("execution(* com.zejian.spring.springAop.dao.UserDao.addUser(..))") public void before(){ System.out.println("前置通知...."); } /** * 后置通知 * returnVal,切點方法執行后的返回值 */ @AfterReturning(value="execution(* com.zejian.spring.springAop.dao.UserDao.addUser(..))",returning = "returnVal") public void AfterReturning(Object returnVal){ System.out.println("后置通知...."+returnVal); } /** * 環繞通知 * @param joinPoint 可用於執行切點的類 * @return * @throws Throwable */ @Around("execution(* com.zejian.spring.springAop.dao.UserDao.addUser(..))") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("環繞通知前...."); Object obj= (Object) joinPoint.proceed(); System.out.println("環繞通知后...."); return obj; } /** * 拋出通知 * @param e */ @AfterThrowing(value="execution(* com.zejian.spring.springAop.dao.UserDao.addUser(..))",throwing = "e") public void afterThrowable(Throwable e){ System.out.println("出現異常:msg="+e.getMessage()); } /** * 無論什么情況下都會執行的方法 */ @After(value="execution(* com.zejian.spring.springAop.dao.UserDao.addUser(..))") public void after(){ System.out.println("最終通知...."); } }
編寫配置文件交由Spring IOC容器管理
<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" 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/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!-- 啟動@aspectj的自動代理支持--> <aop:aspectj-autoproxy /> <!-- 定義目標對象 --> <bean id="userDaos" class="com.zejian.spring.springAop.dao.daoimp.UserDaoImp" /> <!-- 定義aspect類 --> <bean name="myAspectJ" class="com.zejian.spring.springAop.AspectJ.MyAspect"/> </beans>
編寫測試類
/** * Created by zejian on 2017/2/19.*/ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations= "classpath:spring/spring-aspectj.xml") public class UserDaoAspectJ { @Autowired UserDao userDao; @Test public void aspectJTest(){ userDao.addUser(); } }
簡單說明一下,定義了一個目標類UserDaoImpl,利用Spring2.0引入的aspect注解開發功能定義aspect類即MyAspect,在該aspect類中,編寫了5種注解類型的通知函數,分別是前置通知@Before、后置通知@AfterReturning、環繞通知@Around、異常通知@AfterThrowing、最終通知@After,在注解通知上使用execution關鍵字定義的切點表達式,即指明該通知要應用的目標函數,當只有一個execution參數時,value屬性可以省略,當含兩個以上的參數,value必須注明,例如存在返回值時。當然除了把切點表達式直接傳遞給通知注解類型外,還可以使用@pointcut來定義切點匹配表達式。目標類和aspect類定義完成后,所有類的創建都交由SpringIOC容器處理,注意,使用Spring AOP 的aspectJ功能時,需要引用 aspectJ 的 jar 包: aspectjweaver.jar aspectjrt.jar aspectj.jar aopalliance.jar,另外需要使用以下代碼啟動aspect的注解功能支持:
<aop:aspectj-autoproxy />
ok~,運行程序,結果符合預期:
定義切入點函數
在案例中,定義過濾切入點函數時,是直接把execution以定義匹配表達式作為值傳遞給通知類型的如下:
@After(value="execution(* com.zejian.spring.springAop.dao.UserDao.addUser(..))") public void after(){ System.out.println("最終通知...."); }
除了上述方式外,還可采用與ApectJ中使用pointcut關鍵字類似的方式定義切入點表達式如下,使用@Pointcut注解:
/** * 使用Pointcut定義切點 */ @Pointcut("execution(* com.zejian.spring.springAop.dao.UserDao.addUser(..))") private void myPointcut(){} /** * 應用切入點函數 */ @After(value="myPointcut()") public void afterDemo(){ System.out.println("最終通知...."); }
使用@Pointcut注解進行定義,應用到通知函數afterDemo()時直接傳遞切點表達式的函數名稱myPointcut()即可。
切入點指示符
為了方法通知應用到相應過濾的目標方法上,SpringAOP提供了匹配表達式,這些表達式也叫切入點指示符,在前面的案例中,它們已多次出現。
通配符
在定義匹配表達式時,通配符幾乎隨處可見,如*、.. 、+ ,它們的含義如下:
-
.. :匹配方法定義中的任意數量的參數,此外還匹配類定義中的任意數量包
//任意返回值,任意名稱,任意參數的公共方法 execution(public * *(..)) //匹配com.zejian.dao包及其子包中所有類中的所有方法 within(com.zejian.dao..*)
-
+ :匹配給定類的任意子類
//匹配實現了DaoUser接口的所有子類的方法 within(com.zejian.dao.DaoUser+)
-
* :匹配任意數量的字符
//匹配com.zejian.service包及其子包中所有類的所有方法 within(com.zejian.service..*) //匹配以set開頭,參數為int類型,任意返回值的方法 execution(* set*(int))
類型簽名表達式
為了方便類型(如接口、類名、包名)過濾方法,Spring AOP 提供了within關鍵字。其語法格式如下:
within(<type name>)
type name 則使用包名或者類名替換即可,來點案例吧。
//匹配com.zejian.dao包及其子包中所有類中的所有方法
@Pointcut("within(com.zejian.dao..*)")
//匹配UserDaoImpl類中所有方法
@Pointcut("within(com.zejian.dao.UserDaoImpl)")
//匹配UserDaoImpl類及其子類中所有方法
@Pointcut("within(com.zejian.dao.UserDaoImpl+)")
//匹配所有實現UserDao接口的類的所有方法
@Pointcut("within(com.zejian.dao.UserDao+)")
方法簽名表達式
如果想根據方法簽名進行過濾,關鍵字execution可以幫到我們,語法表達式如下
//scope :方法作用域,如public,private,protect //returnt-type:方法返回值類型 //fully-qualified-class-name:方法所在類的完全限定名稱 //parameters 方法參數 execution(<scope> <return-type> <fully-qualified-class-name>.*(parameters))
對於給定的作用域、返回值類型、完全限定類名以及參數匹配的方法將會應用切點函數指定的通知,這里給出模型案例:
//匹配UserDaoImpl類中的所有方法
@Pointcut("execution(* com.zejian.dao.UserDaoImpl.*(..))")
//匹配UserDaoImpl類中的所有公共的方法
@Pointcut("execution(public * com.zejian.dao.UserDaoImpl.*(..))")
//匹配UserDaoImpl類中的所有公共方法並且返回值為int類型
@Pointcut("execution(public int com.zejian.dao.UserDaoImpl.*(..))")
//匹配UserDaoImpl類中第一個參數為int類型的所有公共的方法
@Pointcut("execution(public * com.zejian.dao.UserDaoImpl.*(int , ..))")
其他指示符
-
bean:Spring AOP擴展的,AspectJ沒有對於指示符,用於匹配特定名稱的Bean對象的執行方法;
//匹配名稱中帶有后綴Service的Bean。 @Pointcut("bean(*Service)") private void myPointcut1(){}
-
this :用於匹配當前AOP代理對象類型的執行方法;請注意是AOP代理對象的類型匹配,這樣就可能包括引入接口也類型匹配
//匹配了任意實現了UserDao接口的代理對象的方法進行過濾 @Pointcut("this(com.zejian.spring.springAop.dao.UserDao)") private void myPointcut2(){}
-
target :用於匹配當前目標對象類型的執行方法;
//匹配了任意實現了UserDao接口的目標對象的方法進行過濾 @Pointcut("target(com.zejian.spring.springAop.dao.UserDao)") private void myPointcut3(){}
-
@within:用於匹配所以持有指定注解類型內的方法;請注意與within是有區別的, within是用於匹配指定類型內的方法執行;
//匹配使用了MarkerAnnotation注解的類(注意是類) @Pointcut("@within(com.zejian.spring.annotation.MarkerAnnotation)") private void myPointcut4(){}
-
@annotation(com.zejian.spring.MarkerMethodAnnotation) : 根據所應用的注解進行方法過濾
//匹配使用了MarkerAnnotation注解的方法(注意是方法) @Pointcut("@annotation(com.zejian.spring.annotation.MarkerAnnotation)") private void myPointcut5(){}
ok~,關於表達式指示符就介紹到這,我們主要關心前面幾個常用的即可,不常用過印象即可。這里最后說明一點,切點指示符可以使用運算符語法進行表達式的混編,如and、or、not(或者&&、||、!),如下一個簡單例子:
//匹配了任意實現了UserDao接口的目標對象的方法並且該接口不在com.zejian.dao包及其子包下 @Pointcut("target(com.zejian.spring.springAop.dao.UserDao) !within(com.zejian.dao..*)") private void myPointcut6(){} //匹配了任意實現了UserDao接口的目標對象的方法並且該方法名稱為addUser @Pointcut("target(com.zejian.spring.springAop.dao.UserDao)&&execution(* com.zejian.spring.springAop.dao.UserDao.addUser(..))") private void myPointcut7(){}
通知函數以及傳遞參數
5種通知函數
通知在前面的Spring AOP的入門案例已見過面,在Spring中,通知主要分5種類型,分別是前置通知、后置通知、異常通知、最終通知以及環繞通知,下面分別介紹。
-
前置通知@Before
前置通知通過@Before注解進行標注,並可直接傳入切點表達式的值,該通知在目標函數執行前執行,注意JoinPoint,是Spring提供的靜態變量,通過joinPoint 參數,可以獲取目標對象的信息,如類名稱,方法參數,方法名稱等,,該參數是可選的。
/**
* 前置通知
* @param joinPoint 該參數可以獲取目標對象的信息,如類名稱,方法參數,方法名稱等
*/
@Before("execution(* com.zejian.spring.springAop.dao.UserDao.addUser(..))")
public void before(JoinPoint joinPoint){
System.out.println("我是前置通知");
}
- 后置通知@AfterReturning
通過@AfterReturning注解進行標注,該函數在目標函數執行完成后執行,並可以獲取到目標函數最終的返回值returnVal,當目標函數沒有返回值時,returnVal將返回null,必須通過returning = “returnVal”注明參數的名稱而且必須與通知函數的參數名稱相同。請注意,在任何通知中這些參數都是可選的,需要使用時直接填寫即可,不需要使用時,可以完成不用聲明出來。如下
/**
* 后置通知,不需要參數時可以不提供
*/
@AfterReturning(value="execution(* com.zejian.spring.springAop.dao.UserDao.*User(..))")
public void AfterReturning(){
System.out.println("我是后置通知...");
}
/**
* 后置通知
* returnVal,切點方法執行后的返回值
*/
@AfterReturning(value="execution(* com.zejian.spring.springAop.dao.UserDao.*User(..))",returning = "returnVal")
public void AfterReturning(JoinPoint joinPoint,Object returnVal){
System.out.println("我是后置通知...returnVal+"+returnVal);
}
- 異常通知 @AfterThrowing
該通知只有在異常時才會被觸發,並由throwing來聲明一個接收異常信息的變量,同樣異常通知也用於Joinpoint參數,需要時加上即可,如下:
/** * 拋出通知 * @param e 拋出異常的信息 */ @AfterThrowing(value="execution(* com.zejian.spring.springAop.dao.UserDao.addUser(..))",throwing = "e") public void afterThrowable(Throwable e){ System.out.println("出現異常:msg="+e.getMessage()); }
- 最終通知 @After
該通知有點類似於finally代碼塊,只要應用了無論什么情況下都會執行。
/** * 無論什么情況下都會執行的方法 * joinPoint 參數 */ @After("execution(* com.zejian.spring.springAop.dao.UserDao.*User(..))") public void after(JoinPoint joinPoint) { System.out.println("最終通知...."); }
- 環繞通知@Around
環繞通知既可以在目標方法前執行也可在目標方法之后執行,更重要的是環繞通知可以控制目標方法是否指向執行,但即使如此,我們應該盡量以最簡單的方式滿足需求,在僅需在目標方法前執行時,應該采用前置通知而非環繞通知。案例代碼如下第一個參數必須是ProceedingJoinPoint,通過該對象的proceed()方法來執行目標函數,proceed()的返回值就是環繞通知的返回值。同樣的,ProceedingJoinPoint對象也是可以獲取目標對象的信息,如類名稱,方法參數,方法名稱等等。
@Around("execution(* com.zejian.spring.springAop.dao.UserDao.*User(..))") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("我是環繞通知前...."); //執行目標函數 Object obj= (Object) joinPoint.proceed(); System.out.println("我是環繞通知后...."); return obj; }
通知傳遞參數
在Spring AOP中,除了execution和bean指示符不能傳遞參數給通知方法,其他指示符都可以將匹配的方法相應參數或對象自動傳遞給通知方法。獲取到匹配的方法參數后通過”argNames”屬性指定參數名。如下,需要注意的是args(指示符)、argNames的參數名與before()方法中參數名 必須保持一致即param。
@Before(value="args(param)", argNames="param") //明確指定了 public void before(int param) { System.out.println("param:" + param); }
當然也可以直接使用args指示符不帶argNames聲明參數,如下:
@Before("execution(public * com.zejian..*.addUser(..)) && args(userId,..)") public void before(int userId) { //調用addUser的方法時如果與addUser的參數匹配則會傳遞進來會傳遞進來 System.out.println("userId:" + userId); }
args(userId,..)該表達式會保證只匹配那些至少接收一個參數而且傳入的類型必須與userId一致的方法,記住傳遞的參數可以簡單類型或者對象,而且只有參數和目標方法也匹配時才會有值傳遞進來。
Aspect優先級
在不同的切面中,如果有多個通知需要在同一個切點函數指定的過濾目標方法上執行,那些在目標方法前執行(”進入”)的通知函數,最高優先級的通知將會先執行,在執行在目標方法后執行(“退出”)的通知函數,最高優先級會最后執行。而對於在同一個切面定義的通知函數將會根據在類中的聲明順序執行。如下:
package com.zejian.spring.springAop.AspectJ; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; /** * Created by zejian on 2017/2/20.*/ @Aspect public class AspectOne { /** * Pointcut定義切點函數 */ @Pointcut("execution(* com.zejian.spring.springAop.dao.UserDao.deleteUser(..))") private void myPointcut(){} @Before("myPointcut()") public void beforeOne(){ System.out.println("前置通知....執行順序1"); } @Before("myPointcut()") public void beforeTwo(){ System.out.println("前置通知....執行順序2"); } @AfterReturning(value = "myPointcut()") public void AfterReturningThree(){ System.out.println("后置通知....執行順序3"); } @AfterReturning(value = "myPointcut()") public void AfterReturningFour(){ System.out.println("后置通知....執行順序4"); } }
在同一個切面中定義多個通知響應同一個切點函數,執行順序為聲明順序:
如果在不同的切面中定義多個通知響應同一個切點,進入時則優先級高的切面類中的通知函數優先執行,退出時則最后執行,如下定義AspectOne類和AspectTwo類並實現org.springframework.core.Ordered 接口,該接口用於控制切面類的優先級,同時重寫getOrder方法,定制返回值,返回值(int 類型)越小優先級越大。其中AspectOne返回值為0,AspectTwo的返回值為3,顯然AspectOne優先級高於AspectTwo。
/** * Created by zejian on 2017/2/20.*/ @Aspect public class AspectOne implements Ordered { /** * Pointcut定義切點函數 */ @Pointcut("execution(* com.zejian.spring.springAop.dao.UserDao.deleteUser(..))") private void myPointcut(){} @Before("myPointcut()") public void beforeOne(){ System.out.println("前置通知..AspectOne..執行順序1"); } @Before("myPointcut()") public void beforeTwo(){ System.out.println("前置通知..AspectOne..執行順序2"); } @AfterReturning(value = "myPointcut()") public void AfterReturningThree(){ System.out.println("后置通知..AspectOne..執行順序3"); } @AfterReturning(value = "myPointcut()") public void AfterReturningFour(){ System.out.println("后置通知..AspectOne..執行順序4"); } /** * 定義優先級,值越低,優先級越高 * @return */ @Override public int getOrder() { return 0; } } //切面類 AspectTwo.java @Aspect public class AspectTwo implements Ordered { /** * Pointcut定義切點函數 */ @Pointcut("execution(* com.zejian.spring.springAop.dao.UserDao.deleteUser(..))") private void myPointcut(){} @Before("myPointcut()") public void beforeOne(){ System.out.println("前置通知....執行順序1--AspectTwo"); } @Before("myPointcut()") public void beforeTwo(){ System.out.println("前置通知....執行順序2--AspectTwo"); } @AfterReturning(value = "myPointcut()") public void AfterReturningThree(){ System.out.println("后置通知....執行順序3--AspectTwo"); } @AfterReturning(value = "myPointcut()") public void AfterReturningFour(){ System.out.println("后置通知....執行順序4--AspectTwo"); } /** * 定義優先級,值越低,優先級越高 * @return */ @Override public int getOrder() { return 2; } }
運行結果如下:
案例中雖然只演示了前置通知和后置通知,但其他通知也遵循相同的規則,有興趣可自行測試。到此基於注解的Spring AOP 分析就結束了,但請注意,在配置文件中啟動@Aspect支持后,Spring容器只會嘗試自動識別帶@Aspect的Bean,前提是任何定義的切面類都必須已在spring容器中聲明。
<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" 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/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!--<context:component-scan base-package=""--> <!-- 啟動@aspectj的自動代理支持--> <aop:aspectj-autoproxy /> <!-- 定義目標對象 --> <bean id="userDaos" class="com.zejian.spring.springAop.dao.daoimp.UserDaoImp" /> <!-- 定義aspect類 --> <bean name="myAspectJ" class="com.zejian.spring.springAop.AspectJ.MyAspect"/> <bean name="aspectOne" class="com.zejian.spring.springAop.AspectJ.AspectOne" /> <bean name="aspectTwo" class="com.zejian.spring.springAop.AspectJ.AspectTwo" /> </beans>
基於XML的開發
前面分析完基於注解支持的開發是日常應用中最常見的,即使如此我們還是有必要了解一下基於xml形式的Spring AOP開發,這里會以一個案例的形式對xml的開發形式進行簡要分析,定義一個切面類
/** * Created by zejian on 2017/2/20.*/ public class MyAspectXML { public void before(){ System.out.println("MyAspectXML====前置通知"); } public void afterReturn(Object returnVal){ System.out.println("后置通知-->返回值:"+returnVal); } public Object around(ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("MyAspectXML=====環繞通知前"); Object object= joinPoint.proceed(); System.out.println("MyAspectXML=====環繞通知后"); return object; } public void afterThrowing(Throwable throwable){ System.out.println("MyAspectXML======異常通知:"+ throwable.getMessage()); } public void after(){ System.out.println("MyAspectXML=====最終通知..來了"); } }
通過配置文件的方式聲明如下(spring-aspectj-xml.xml):
<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" 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/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!--<context:component-scan base-package=""--> <!-- 定義目標對象 --> <bean name="productDao" class="com.zejian.spring.springAop.dao.daoimp.ProductDaoImpl" /> <!-- 定義切面 --> <bean name="myAspectXML" class="com.zejian.spring.springAop.AspectJ.MyAspectXML" /> <!-- 配置AOP 切面 --> <aop:config> <!-- 定義切點函數 --> <aop:pointcut id="pointcut" expression="execution(* com.zejian.spring.springAop.dao.ProductDao.add(..))" /> <!-- 定義其他切點函數 --> <aop:pointcut id="delPointcut" expression="execution(* com.zejian.spring.springAop.dao.ProductDao.delete(..))" /> <!-- 定義通知 order 定義優先級,值越小優先級越大--> <aop:aspect ref="myAspectXML" order="0"> <!-- 定義通知 method 指定通知方法名,必須與MyAspectXML中的相同 pointcut 指定切點函數 --> <aop:before method="before" pointcut-ref="pointcut" /> <!-- 后置通知 returning="returnVal" 定義返回值 必須與類中聲明的名稱一樣--> <aop:after-returning method="afterReturn" pointcut-ref="pointcut" returning="returnVal" /> <!-- 環繞通知 --> <aop:around method="around" pointcut-ref="pointcut" /> <!--異常通知 throwing="throwable" 指定異常通知錯誤信息變量,必須與類中聲明的名稱一樣--> <aop:after-throwing method="afterThrowing" pointcut-ref="pointcut" throwing="throwable"/> <!-- method : 通知的方法(最終通知) pointcut-ref : 通知應用到的切點方法 --> <aop:after method="after" pointcut-ref="pointcut"/> </aop:aspect> </aop:config> </beans>
聲明方式和定義方式在代碼中已很清晰了,了解一下即可,在實際開發中,會更傾向與使用注解的方式開發,畢竟更簡單更簡潔。
參考資料
http://blog.csdn.net/javazejian/article/details/56267036#基於aspect-spring-aop-開發
http://shouce.jb51.net/spring/aop.html