Spring 使用 AspectJ 開發 AOP:基於 XML 和基於 Annotation
AspectJ 是一個基於 Java 語言的 AOP 框架,它擴展了 Java 語言。Spring2.0 以後,新增了對 AspectJ 方式的支持,新版本的 Spring 框架,建議使用 AspectJ 方式開發 AOP。
使用 AspectJ 開發 AOP 通常有兩種方式:
1)基於 XML 的聲明式
2)基於 Annotation 的聲明式
接下來將對這兩種 AOP 的開發方式進行演練。
基於XML的聲明式
基於 XML 的聲明式是指通過 Spring 配置文件的方式定義切面、切入點及聲明通知,而所有的切面和通知都必須定義在 <aop:config> 元素中。下面通過案例演示 Spring 中如何使用基於 XML 的聲明式實現 AOP 的開發。
1. 導入 JAR 包
使用 AspectJ 除了需要導入 Spring AOP 的 JAR 包以外,還需要導入與 AspectJ 相關的 JAR 包,具體如下:
1)spring-aspects-3.2.13.RELEASE.jar:Spring 爲 AspectJ 提供的實現,在 Spring 的包中已經提供
2)om.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar:是 AspectJ 提供的規範,可以在官方網址https://repo.spring.io/webapp/#/search/quick/中搜索並下載:
2. 創建切面類 MyAspect
在 src 目錄下創建一個名爲 com.mengma.aspectj.xml 的包,在該包下創建切面類 MyAspect,編輯後如下所示:
package com.mengma.aspectj.xml;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
public class MyAspect {
// 前置通知
public void myBefore(JoinPoint joinPoint) {
System.out.print("前置通知,目標:");
System.out.print(joinPoint.getTarget() + "方法名稱:");
System.out.println(joinPoint.getSignature().getName());
}
// 後置通知
public void myAfterReturning(JoinPoint joinPoint) {
System.out.print("後置通知,方法名稱:" + joinPoint.getSignature().getName());
}
// 環繞通知
public Object myAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("環繞開始"); // 開始
Object obj = proceedingJoinPoint.proceed(); // 執行當前目標方法
System.out.println("環繞結束"); // 結束
return obj;
}
// 異常通知
public void myAfterThrowing(JoinPoint joinPoint, Throwable e) {
System.out.println("異常通知" + "出錯了" + e.getMessage());
}
// 最終通知
public void myAfter() {
System.out.println("最終通知");
}
}
上述代碼中,分別定義了幾種不同的通知類型方法,在這些方法中,通過 JoinPoint 參數可以獲得目標對象的類名、目標方法名和目標方法參數等。需要注意的是,環繞通知必須接收一個類型爲 ProceedingJoinPoint 的參數,返回值必須是 Object 類型,且必須拋出異常。異常通知中可以傳入 Throwable 類型的參數,用於輸出異常信息。
3. 創建 Spring 配置文件
在 com.mengma.aspectj.xml 包下創建 applicationContext.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-2.5.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">
<!--目標類 -->
<bean id="customerDao" class="com.mengma.dao.CustomerDaoImpl" />
<!--切面類 -->
<bean id="myAspect" class="com.mengma.aspectj.xml.MyAspect"></bean>
<!--AOP 編程 -->
<aop:config>
<aop:aspect ref="myAspect">
<!-- 配置切入點,通知最後增強哪些方法 -->
<aop:pointcut expression="execution ( * com.mengma.dao.*.* (..))"
id="myPointCut" />
<!--前置通知,關聯通知 Advice和切入點PointCut -->
<aop:before method="myBefore" pointcut-ref="myPointCut" />
<!--後置通知,在方法返回之後執行,就可以獲得返回值returning 屬性 -->
<aop:after-returning method="myAfterReturning"
pointcut-ref="myPointCut" returning="returnVal" />
<!--環繞通知 -->
<aop:around method="myAround" pointcut-ref="myPointCut" />
<!--拋出通知:用於處理程序發生異常,可以接收當前方法產生的異常 -->
<!-- *注意:如果程序沒有異常,則不會執行增強 -->
<!-- * throwing屬性:用於設置通知第二個參數的名稱,類型Throwable -->
<aop:after-throwing method="myAfterThrowing"
pointcut-ref="myPointCut" throwing="e" />
<!--最終通知:無論程序發生任何事情,都將執行 -->
<aop:after method="myAfter" pointcut-ref="myPointCut" />
</aop:aspect>
</aop:config>
</beans>
上述代碼中,首先在第 4、7、8 行代碼中分別導入了 AOP 的命名空間。第 12 行代碼指定了切面類。
第 17、18 行代碼配置了切入點,通知需要增強哪些方法,expression="execution(*com.mengma.dao.*.*(..))的意思是增強 com.mengma.dao 包下所有的方法。
第 20~32 行代碼用於關聯通知(Advice)和切入點(PointCut)。以第 20 行代碼前置通知爲例,<aop:before> 標籤的 method 屬性用於指定通知,pointcut-ref 屬性用於指定切入點,也就是要增強的方法,其他幾種通知的配置可以參考代碼註釋。
4. 創建測試類
在 com.mengma.aspectj.xml 包下創建測試類 XMLTest,如下所示:
package com.mengma.aspectj.xml;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.mengma.dao.CustomerDao;
public class XMLTest {
@Test
public void test() {
String xmlPath = "com/mengma/aspectj/xml/applicationContext.xml";
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(xmlPath);
// 從Spring容器獲取實例
CustomerDao customerDao = (CustomerDao) applicationContext.getBean("customerDao");
// 執行方法
customerDao.add();
}
}
5. 運行項目並查看結果
使用 JUnit 測試運行 XMLTest,運行成功後,控制檯的輸出結果如圖:
爲了更好地演示異常通知,接下來在 CustomerDaoImpl 類的 add() 方法中添加一行會拋出異常的代碼,如“int i=1/0;”,重新運行 XMLTest 測試類,可以看到異常通知執行了,此時控制檯的輸出結果如圖:
從上面兩次輸出結果中可以看出,基於 XML 聲明式的 AOP 開發已經成功實現。
基於 Annotation 的聲明式
在 Spring 中,儘管使用 XML 配置文件可以實現 AOP 開發,但是如果所有的相關的配置都集中在配置文件中,勢必會導致 XML 配置文件過於臃腫,從而給維護和升級帶來一定的困難。爲此,AspectJ 框架爲 AOP 開發提供了另一種開發方式——基於 Annotation 的聲明式。AspectJ 允許使用註解定義切面、切入點和增強處理,而 Spring 框架則可以識別並根據這些註解生成 AOP 代理。
關於 Annotation 註解的介紹如表所示:
名稱 | 說明 |
---|---|
@Aspect | 用於定義一個切面 |
@Before | 用於定義前置通知,相當於 BeforeAdvice |
@AfterReturning | 用於定義後置通知,相當於 AfterReturningAdvice |
@Around | 用於定義環繞通知,相當於MethodInterceptor |
@AfterThrowing | 用於定義拋出通知,相當於ThrowAdvice |
@After | 用於定義最終final通知,不管是否異常,該通知都會執行 |
@DeclareParents | 用於定義引介通知,相當於IntroductionInterceptor(不要求掌握) |
下面使用註解的方式重新實現“基於XML的聲明式”部分的功能。
1. 創建切面類 MyAspect
在 src 目錄下創建一個名爲 com.mengma.aspectj.annotation 的包,在該包下創建一個切面類 MyAspect,如下所示:
package com.mengma.aspectj.annotation;
import org.aspectj.lang.JoinPoint;
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.Before;
import org.aspectj.lang.annotation.Pointcut;
public class MyAspect {
// 用於取代:<aop:pointcut expression="execution ( * com.mengma.dao.*.* (..))" id="myPointCut" />
// 要求:方法必須是private,沒有值,名稱自定義,沒有參數
@Pointcut("execution ( * com.mengma.dao.*.* (..))")
private void myPointCut() {
}
// 前置通知
@Before("myPointCut()")
public void myBefore(JoinPoint joinPoint) {
System.out.print("前置通知,目標:");
System.out.print(joinPoint.getTarget() + "方法名稱:");
System.out.println(joinPoint.getSignature().getName());
}
// 後置通知
@AfterReturning(value = "myPointCut()")
public void myAfterReturning(JoinPoint joinPoint) {
System.out.print("後置通知,方法名稱:" + joinPoint.getSignature().getName());
}
// 環繞通知
@Around("myPointCut()")
public Object myAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("環繞開始"); // 開始
Object obj = proceedingJoinPoint.proceed(); // 執行當前目標方法
System.out.println("環繞開始"); // 結束
return obj;
}
// 異常通知
@AfterThrowing(value = "myPointCut()", throwing = "e")
public void myAfterThrowing(JoinPoint joinPoint, Throwable e) {
System.out.println("異常通知,出錯了" + e.getMessage());
}
// 最終通知
@After("myPointCut()")
public void myAfter() {
System.out.println("最終通知");
}
}
上述代碼中,第 13 行 @Aspect 註解用於聲明這是一個切面類,該類作爲組件使用,所以要添加 @Component 註解才能生效。第 19 行中 @Poincut 註解用於配置切入點,取代 XML 文件中配置切入點的代碼。
在每個通知相應的方法上都添加了註解聲明,並且將切入點方法名“myPointCut”作爲參數傳遞給要執行的方法,如需其他參數(如異常通知的異常參數),可以根據代碼提示傳遞相應的屬性值。
2. 爲目標類添加註解
在 com.mengma.dao.CustomerDaoImpl 目標類中添加註解 @Repository("customerDao")。
3. 創建Spring配置文件
在 com.mengma.aspectj.annotation 包下創建 applicationContext.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"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!--掃描含com.mengma包下的所有註解-->
<context:component-scan base-package="com.mengma"/>
<!-- 使切面開啓自動代理 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
上述代碼中,首先導入了 AOP 命名空間及其配套的約束,使切面類中的 @AspectJ 註解能夠正常工作;第 13 行代碼添加了掃描包,使註解生效。需要注意的是,這裏還包括目標類 com.mengma.dao.CustomerDaoImpl 的註解,所以 base-package 的值爲 com.mengma;第 15 行代碼的作用是切面開啓自動代理。
4. 創建測試類
在 com.mengma.aspectj.annotation 包下創建一個名爲 AnnotationTest 的測試類,如下所示:
package com.mengma.aspectj.annotation;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.mengma.dao.CustomerDao;
public class AnnotationTest {
@Test
public void test() {
String xmlPath = "com/mengma/aspectj/xml/applicationContext.xml";
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(xmlPath);
// 從Spring容器獲取實例
CustomerDao customerDao = (CustomerDao) applicationContext.getBean("customerDao");
// 執行方法
customerDao.add();
}
}
5. 運行項目並查看結果
使用 JUnit 測試運行 AnnotationTest ,運行成功後,控制檯的輸出結果如圖:
刪除 add() 方法中的“int i=1/0;”,重新運行 AnnotationTest,此時控制檯的輸出結果如下圖 :
從上面兩次運行的輸出結果中可以看出,已成功使用 Annotation 的方式實現了 AOP 開發。與其他方式相比,基於 Annotation 方式實現 AOP 的效果是最方便的方式,所以實際開發中推薦使用註解的方式。