1. 程式人生 > >SSM框架 之 Spring-AOP(面向切面程式設計)

SSM框架 之 Spring-AOP(面向切面程式設計)

1AOP概述

AOP(Aspect-Oriented Programming,面向切面程式設計):是一種新的方法論,是對傳統 OOP(Object-Oriented Programming,面向物件程式設計)的補充。
兩種程式設計思想。是對面向物件程式設計的一種補充。
面向切面程式設計:是指在程式執行期間將某段程式碼,動態的切入到某個類的指定方法的指定位置的這種程式設計思想叫做面向切面程式設計。
AOP程式設計操作的主要物件是切面(aspect),而切面模組化橫切關注點。
在應用AOP程式設計時,仍然需要定義公共功能,但可以明確的定義這個功能應用在哪裡,以什麼方式應用,並且不必修改受影響的類。這樣一來橫切關注點就被模組化到特殊的類裡——這樣的類我們通常稱之為“切面”。
AOP的好處:
每個事物邏輯位於一個位置,程式碼不分散,便於維護和升級
業務模組更簡潔,只包含核心業務程式碼

2AOP術語

2.1橫切關注點:從每個方法中抽取出來的同一類非核心業務。
2.2切面(Aspect):封裝橫切關注點資訊的類,每個關注點體現為一個通知方法。
2.3通知(Advice):切面必須要完成的各個具體工作
2.4目標(Target):被通知的物件
2.5代理(Proxy):向目標物件應用通知之後建立的代理物件
2.6連線點(Joinpoint)
橫切關注點在程式程式碼中的具體體現,對應程式執行的某個特定位置。例如:類某個方法呼叫前、呼叫後、方法捕獲到異常後等。
在應用程式中可以使用橫縱兩個座標來定位一個具體的連線點:
2.7切入點(pointcut):

定位連線點的方式。每個類的方法中都包含多個連線點,所以連線點是類中客觀存在的事物。如果把連線點看作資料庫中的記錄,那麼切入點就是查詢條件——AOP可以通過切入點定位到特定的連線點。切點通過org.springframework.aop.Pointcut 介面進行描述,它使用類和方法作為連線點的查詢條件。

為了方便理解以上專業術語,給出下圖:


3AspectJ

3.1簡介

AspectJ:Java社群裡最完整最流行的AOP框架。
在Spring2.0以上版本中,可以使用基於AspectJ註解或基於XML配置的AOP。

3.2在Spring中啟用AspectJ註解支援

①匯入JAR包

commons-logging-1.1.3.jar

spring-beans-4.0.0.RELEASE.jar

spring-context-4.0.0.RELEASE.jar

spring-core-4.0.0.RELEASE.jar

spring-expression-4.0.0.RELEASE.jar

 
AOP依賴的包

spring-aop-4.0.0.RELEASE.jar

spring-aspects-4.0.0.RELEASE.jar

//增強版

com.springsource.net.sf.cglib-2.2.0.jar

com.springsource.org.aopalliance-1.0.0.jar

com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar



②引入aop名稱空間

③配置
<aop:aspectj-autoproxy>
當Spring IOC容器偵測到bean配置檔案中的<aop:aspectj-autoproxy>元素時,會自動為與AspectJ切面匹配的bean建立代理

3.3用AspectJ註解宣告切面

①要在Spring中宣告AspectJ切面,只需要在IOC容器中將切面宣告為bean例項。②當在Spring IOC容器中初始化AspectJ切面之後,Spring IOC容器就會為那些與 AspectJ切面相匹配的bean建立代理。
③在AspectJ註解中,切面只是一個帶有@Aspect註解的Java類,它往往要包含很多通知。
④通知是標註有某種註解的簡單的Java方法。
⑤AspectJ支援5種類型的通知註解:

[1]@Before:前置通知,在方法執行之前執行

[2]@After:後置通知,在方法執行之後執行

[3]@AfterRunning:返回通知,在方法返回結果之後執行

[4]@AfterThrowing:異常通知,在方法丟擲異常之後執行

[5]@Around:環繞通知,圍繞著方法執行

3.4AOP版的可以日誌記錄的計算器

LogAspect中的程式碼
/**
 * 1、告訴ioc這個元件的存在
 * 2、告訴ioc這是一個切面使用@Aspect
 * @author syl
 *
 */
@Aspect
@Component
public class LogAspect {
    
    /**
     * try{
     *      @Before前置通知
     *      method.invoke();
     *      @AfterRunning返回通知
     * }catch(e){
     *      @AfterThrowing:異常通知,
     * }
     * @After
     *
     * 告訴Spring這些放在都在那個方法的哪個位置執行
     * 1)、告訴位置
     [1]@Before:前置通知,在方法執行之前執行
     [2]@After:後置通知,在方法執行最終結束之後執行。
	    如果沒異常
     [3]@AfterRunning:返回通知,在方法返回結果之後執行
     [4]@AfterThrowing:異常通知,在方法丟擲異常之後執行


	1、編寫切入點表示式,來告訴spring是切入哪個方法的這個位置
     */
    @Before(value="execution(public * *.add(int, int))")
    public void logStart(){
	System.out.println("AOP日誌,方法開始");
    }
    
    @After(value="execution(public * *.add(int, int))")
    public void logEnd(){
	System.out.println("AOP日誌,方法最終結束");
    }
    
    @AfterThrowing(value="execution(public * *.add(int, int))")
    public void logException(){
	System.out.println("AOP日誌,方法出現異常");
    }
    
    @AfterReturning(value="execution(public * *.add(int, int))")
    public void logReturn(){
	System.out.println("AOP日誌,方法正常執行");
    }

applicationContext中的內容
<!--1、自動掃描所有的元件  -->
<context:component-scan base-package="com.atguigu"></context:component-scan>
<!--2、開啟基於註解的aop功能  -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
<!--
 0)、被代理物件也得先加入到ioc中
 1)、編寫日誌切面,並加入到ioc中,@Component @Aspect
 2)、給切面編寫通知方法。
 3)、加上何時何地切入的註解。切入點表示式
 4)、開啟基於註解的aop功能即可
 -->

4、AOP細節


1切入點表示式

1.1作用

通過表示式的方式定位一個或多個具體的連線點。

1.2語法細節

①切入點表示式的語法格式:execution([許可權修飾符] [返回值型別] [簡單類名/全類名] [方法名]([引數列表]))
②舉例說明


表示式execution(* com.atguigu.spring.ArithmeticCalculator.*(..))
含義ArithmeticCalculator介面中宣告的所有方法;第一個“*”代表任意修飾符及任意返回值;第二個“*”代表任意方法;
“..”匹配任意數量、任意型別的引數;若目標類、介面與該切面類在同一個包中可以省略包名。


表示式execution(public * ArithmeticCalculator.*(..))
含義ArithmeticCalculator介面的所有公有方法


表示式execution(public double ArithmeticCalculator.*(..))
含義ArithmeticCalculator介面中返回double型別數值的方法


表示式execution(public double ArithmeticCalculator.*(double, ..))
含義第一個引數為double型別的方法;“..” 匹配任意數量、任意型別的引數。


表示式execution(public double ArithmeticCalculator.*(double, double))
含義引數型別為double,double型別的方法


③在AspectJ中,切入點表示式可以通過 “&&”、“||”、“!”等操作符結合起來。
表示式execution (* *.add(int,..)) || execution(* *.sub(int,..))
含義任意類中第一個引數為int型別的add方法或sub方法

1.3AOP細節-切入點表示式應用到實際的切面類中

//1、不能用本身的型別去找,只能用介面型別
//2、IOC容器中儲存的不是這個物件的本身,而是代理物件
//3、為什麼介面型別又是可以的.因為jdk在建立動態代理的時候,
//需要被代理物件的介面
/*Calculator bean = ioc.getBean(Calculator.class);
bean.add(1, 2);
System.out.println(bean.getClass());*/
MathCalculator bean = ioc.getBean(MathCalculator.class);
bean.add(1, 2);
System.out.println(bean.getClass());

2當前連線點細節

2.1概述

切入點表示式通常都會是從巨集觀上定位一組方法,和具體某個通知的註解結合起來就能夠確定對應的連線點。那麼就一個具體的連線點而言,我們可能會關心這個連線點的一些具體資訊,
例如:當前連線點所在方法的方法名、當前傳入的引數值等等。這些資訊都封裝在JoinPoint介面的例項物件中。

3通知


3.1概述

在具體的連線點上要執行的操作。
一個切面可以包括一個或者多個通知。
通知所使用的註解的值往往是切入點表示式。

3.2前置通知

前置通知:在方法執行之前執行的通知
使用@Before註解

3.3後置通知

後置通知:後置通知是在連線點完成之後執行的,即連線點返回結果或者丟擲異常的時候
使用@After註解

3.4返回通知

返回通知:無論連線點是正常返回還是丟擲異常,後置通知都會執行。如果只想在連線點返回的時候記錄日誌,應使用返回通知代替後置通知。
使用@AfterReturning註解
在返回通知中訪問連線點的返回值
在返回通知中,只要將returning屬性新增到@AfterReturning註解中,就可以訪問連線點的返回值。該屬性的值即為用來傳入返回值的引數名稱
必須在通知方法的簽名中新增一個同名引數。在執行時Spring AOP會通過這個引數傳遞返回值
原始的切點表示式需要出現在pointcut屬性中
@AfterReturning(value = "com.atguigu.aspect.LogAspect.mypoint()", returning = "res")
public void validReturn(JoinPoint joinPoint, Object res) {
	Signature signature = joinPoint.getSignature();
	String name = signature.getName();
	System.out.println("AOP引數驗證,【" + name + "】方法正常返回,返回值為:" + res);
}

3.5異常通知

異常通知:只在連線點丟擲異常時才執行異常通知
將throwing屬性新增到@AfterThrowing註解中,也可以訪問連線點丟擲的異常。Throwable是所有錯誤和異常類的頂級父類,所以在異常通知方法可以捕獲到任何錯誤和異常。
如果只對某種特殊的異常型別感興趣,可以將引數宣告為其他異常的引數型別。然後通知就只在丟擲這個型別及其子類的異常時才被執行
@AfterThrowing(value = "execution(public * *.*(int, int))", throwing = "e")
public void logException(JoinPoint joinPoint, Throwable e) {
	// 獲取方法名
	String name = joinPoint.getSignature().getName();
	System.out.println("AOP日誌,【" + name + "】方法出現異常:異常物件:" + abc);
}

3.6環繞通知

環繞通知是所有通知型別中功能最為強大的,能夠全面地控制連線點,甚至可以控制是否執行連線點。
對於環繞通知來說,連線點的引數型別必須是ProceedingJoinPoint。它是 JoinPoint的子介面,允許控制何時執行,是否執行連線點。
在環繞通知中需要明確呼叫ProceedingJoinPoint的proceed()方法來執行被代理的方法。如果忘記這樣做就會導致通知被執行了,但目標方法沒有被執行。
注意:環繞通知的方法需要返回目標方法執行之後的結果,即呼叫 joinPoint.proceed();的返回值,否則會出現空指標異常。
/**
 * 最強大通知。 一般不常用
 * 
 * @param proceedingJoinPoint
 * @return
 */
@Around(value = "com.atguigu.aspect.LogAspect.mypoint()")
public Object vaildAround(ProceedingJoinPoint proceedingJoinPoint) {
	// proceedingJoinPoint封裝了連線點的詳細資訊
	// proceed,執行目標方法 method.invoke
	Object proceed = null;
	Object[] args = proceedingJoinPoint.getArgs();
	try {
		// //傳入目標執行時需要的引數列表
		// 前置通知
		System.out.println("proceed...之前");


		// method.invoke
		// 目標方法執行完成後會有返回值,這個返回值一定return出去
		proceed = proceedingJoinPoint.proceed(args);
		// 返回通知
		System.out.println("proceed...之後");
	} catch (Throwable e) {
		// e.printStackTrace();
		// 異常通知
		System.out.println("proceed...異常");
		// 1、注意:
		// 一定將這個異常繼續丟擲去,以方便外界都能收到這個異常
		throw new RuntimeException(e);
	} finally {
		// 後置通知
		System.out.println("proceed...結束");
	}


	return proceed;
}


3.7重用切入點定義

在編寫AspectJ切面時,可以直接在通知註解中書寫切入點表示式。但同一個切點表示式可能會在多個通知中重複出現。
在AspectJ切面中,可以通過@Pointcut註解將一個切入點宣告成簡單的方法。切入點的方法體通常是空的,因為將切入點定義與應用程式邏輯混在一起是不合理的。
切入點方法的訪問控制符同時也控制著這個切入點的可見性。如果切入點要在多個切面中共用,最好將它們集中在一個公共的類中。在這種情況下,它們必須被宣告為public。
在引入這個切入點時,必須將類名也包括在內。如果類沒有與這個切面放在同一個包中,還必須包含包名。
其他通知可以通過方法名稱引入該切入點
@Pointcut(value = "execution(public * *.*(int, int))")
public void mypoint() {
}


@Before(value = "mypoint()")
public void logStart(JoinPoint joinPoint) {
	Object[] args = joinPoint.getArgs();
	String name = joinPoint.getSignature().getName();
	System.out.println("AOP日誌:【" + name + "】方法開始執行,引數是:"
			+ Arrays.asList(args));
}


@After("mypoint()")
public void logEnd(JoinPoint joinPoint) {
	String name = joinPoint.getSignature().getName();
	System.out.println("AOP日誌:【" + name + "】方法執行結束!");
}


@AfterThrowing(value = "mypoint()", throwing = "e")
public void logException(JoinPoint joinPoint, Exception e) {
	String name = joinPoint.getSignature().getName();
	System.out.println("AOP日誌:【" + name + "】方法執行出現異常:" + e);
}


@AfterReturning(value = "mypoint()", returning = "res")
public void logReturn(JoinPoint joinPoint, Object res) {
	String name = joinPoint.getSignature().getName();
	System.out.println("AOP日誌:【" + name + "】方法正常結束,返回值為:" + res);
}

3.8指定切面的優先順序

在同一個連線點上應用不止一個切面時,除非明確指定,否則它們的優先順序是不確定的。
切面的優先順序可以通過實現Ordered介面或利用@Order註解指定。
實現Ordered介面,getOrder()方法的返回值越小,優先順序越高。
若使用@Order註解,序號出現在註解中
@Aspect
@Component
@Order(2)
public class LogAspect {
}

@Component
@Aspect
@Order(1)
public class ValidatorAspect {
}

先切入的最後出去。


5、 以XML方式配置切面

1概述

除了使用AspectJ註解宣告切面,Spring也支援在bean配置檔案中宣告切面。這種宣告是通過aop名稱空間中的XML元素完成的。

正常情況下,基於註解的宣告要優先於基於XML的宣告。通過AspectJ註解,切面可以與AspectJ相容,而基於XML的配置則是Spring專有的。由於AspectJ得到

越來越多的 AOP框架支援,所以以註解風格編寫的切面將會有更多重用的機會。

2配置細節

在bean配置檔案中,所有的Spring AOP配置都必須定義在<aop:config>元素內部。對於每個切面而言,都要建立一個<aop:aspect>元素來為具體的切面實現

引用後端bean例項。切面bean必須有一個識別符號,供<aop:aspect>元素引用。

<bean id="logAspect" class="com.atguigu.aspect.LogAspect"></bean>
<bean id="validatorAspect" class="com.atguigu.aspect.ValidatorAspect"></bean>
<bean id="mathCalulator" class="com.atguigu.calulator.MathCalulator"></bean>


<aop:config>
	<aop:aspect ref="logAspect"></aop:aspect>
	<aop:aspect ref="validatorAspect"></aop:aspect>
</aop:config>

3宣告切入點

切入點使用<aop:pointcut>元素宣告。
切入點必須定義在<aop:aspect>元素下,或者直接定義在<aop:config>元素下。
定義在<aop:aspect>元素下:只對當前切面有效
定義在<aop:config>元素下:對所有切面都有效
基於XML的AOP配置不允許在切入點表示式中用名稱引用其他切入點。
<aop:pointcut expression="execution(* *.*(..))" id="mypoint"/>

4宣告通知

在aop名稱空間中,每種通知型別都對應一個特定的XML元素。
通知元素需要使用<pointcut-ref>來引用切入點,或用<pointcut>直接嵌入切入點表示式。
method屬性指定切面類中通知方法的名稱
<aop:before method="logStart" pointcut="execution(* *.*(..))"/

5完整程式碼如下

 <!--基於註解的AOP
1、將被代理的物件和切面類都要加入ioc容器中 
2、配置切面類的每個通知方法,都在何時何地執行
3、開啟基於註解的aop功能
-->
<!--基於XML的AOP  -->
<!--1、將這些元件加入到ioc中  -->
<bean id="mathCalculator" class="com.atguigu.inf.MathCalculator"></bean>
<bean id="aalidatorAspect" class="com.atguigu.aspect.AalidatorAspect"></bean>
<bean id="logAspect" class="com.atguigu.aspect.LogAspect"></bean>


<!--2、配置切面類的詳細資訊,依賴於aop名稱空間  -->
<aop:config>
<aop:pointcut expression="execution(* *.*(..))" id="mypoint"/>
<aop:pointcut expression="execution(* *.add(..))" id="mypoint1"/>
<!-- 配置第二個切面,預設根據切面配置的優先順序執行-->
<aop:aspect ref="aalidatorAspect" order="1">
    <aop:before method="validStart" pointcut-ref="mypoint"/>
    <aop:after-returning method="validReturn" pointcut-ref="mypoint" returning="res"/>
</aop:aspect>
<!--使用aop:aspect配置一個切面  -->
<aop:aspect ref="logAspect" order="0">
    <!-- 配置前置通知是哪個方法 -->
    <aop:before method="logStart" pointcut="execution(* *.*(..))"/>
    <aop:after method="logEnd" pointcut-ref="mypoint"/>
    <!--returning指定返回值  -->
    <aop:after-returning method="logReturn" pointcut-ref="mypoint" returning="res"/>
    <!--throwing指定異常引數的名 -->
    <aop:after-throwing method="logException" pointcut-ref="mypoint" throwing="abc"/>
</aop:aspect>
</aop:config>