Spring框架學習與實踐(十一)

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 註解的介紹如表所示:

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 的效果是最方便的方式,所以實際開發中推薦使用註解的方式。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章