《Spring In action》學習筆記——AOP(面向切面編程)

1.AOP術語

通知(Advice):
切面的功能被稱爲Advice(通知),它定義了切面是什麼及何時使用。除了描述切面要完成的工作,通知還解決了何時執行這個工作的問題。它應用在某個方法被調用之前?之後?之前和之後?或是隻在方法拋出一個異常時?

連接點(Joinpoint):
Joinpoint是在程序執行過程中能夠插入切面的一個點。這個點可以是方法被調用時、異常被拋出時、甚至字段被編輯時。切面代碼可以通過這個點插入到程序的一般流程中,從而添加新的行爲。

切入點(Pointcut):
切入點可以縮小切面通知的連接點的範圍。如果說advice定義了切面的“什麼”和“何時”,那麼切入點就定義了“何地”。切入點的定義匹配advice要織入的一個或多個連接點。我們通常使用明確的類和方法名稱,或是利用正則表達式定義匹配的類和方法名稱模板來指定這些切入點。有些AOP框架允許創建動態切入點,可以根據運行時的狀態(比如方法的參數值)來應用通知。

切面(Aspect):
切面就是通知和切入點的結合。通知和切入點共同定義了關於切面的全部內容——它的功能、在何時和何地完成其功能。

目標(Target):
“目標”是被通知的對象,它可以是我們編寫的一個對象,或第三方對象。如果沒有AOP,這個對象就必須包含自己的主要邏輯和交叉事務的邏輯。通過使用AOP,目標對象就可以着重於自己的主要邏輯,不必考慮要被應用的任何通知。

代理(Proxy)
代理是向目標對象應用通知之後被創建的對象。對於客戶對象來說,目標對象(AOP之前)和代理對象(AOP之後)是一樣的——它們就應用是這樣的。這樣一來,程序的其他部分就不必修改對代理對象的支持。

織入(Weaving)
織入是把切面應用到目標對象來創建新的代理對象的過程。切面在指定連接點織入到目標對象。在目標對象的生命週期裏有多個時機可以發生織入過程:
  • 編譯時:切面在目標類編譯時被織入。這需要特殊的編譯器,AspectJ在織入編譯器就以這種方式織入切面。
  • 類加載時:切面在目標類加載到JVM時被織入。這需要特殊的ClassLoader,它可以在目標類被加載到程序之前增強類的字節代碼。AspectJ5的“加載時織入(LTW)”就以這種方式支持織入切面。
  • 運行時:切面在程序運行的某個時刻被織入。一般情況下,在織入切面時,AOP容器會動態創建一個代理對象來委託給目標對象。這就是Spring AOP織入切面的方式。

2.Spring對AOP的支持

Spring對AOP的支持具有以下4種情況:
  • 經典的基於代理的AOP(各版本Spring)
  • @AspectJ註解驅動的切面(僅Spring 2.0);
  • 純POJO切面(僅Spring2.0);
  • 注入式AspectJ切面(各版本Spring)
前三種都是基於代理的AOP的變體,因此,Spring對AOP的支持侷限於方法注入。如果我們的AOP需求超過了簡單方法注入的範疇(比如構造器或屬性注入),就應該考慮在AspectJ裏實現切面,利用Spring的從屬注入把Spring的Bean注入到AspectJ切面

由於Spring是在運行時創建代理,所以我們不需要使用特殊的編譯器把切面織入到Spring的AOP。

Spring生成被代理類的方式有兩種。如果目標對象實現的接口中包含需要代理的方法,Spring會使用JDK的java.lang.reflect.Proxy類,它允許Spring動態生成一個新類來實現必要的接口、織入任何通知、並且把對這些接口的任何調用都轉發到目標類。如果目標類沒有實現一個接口或實現的接口中不包含需要代理的方法,Spring就使用CGLIB庫生成目標類的一個子類。在創建這個子類時,Spring織入通知,並且把對這個子類的調用委託到目標類。在使用這種方式時,有兩點需要注意:
  • 使用前一種創建接口的代理方式比使用代理類更受人喜歡,因爲它能夠更好地實現程序的鬆耦合。
  • 被標記爲final的方法不能被通知。Spring爲目標類創建一個子類,需要被通知的任務方法都會被覆蓋並被織入通知,這對於final類型的方法是不可能的。
Spring是基於動態代理的,這隻支持方法連接點。如果需要方法截取之外的功能,可以利用AspectJ來補充Spring的AOP。

3.典型的Spring切面

場景:在一場表演中,表演前觀衆找位置,並關閉手機,表演精彩時觀衆鼓掌,表演槽糕的時候觀衆要求退票。現在在這個場景下使用Spring的切面
package com.sin90lzc.test;
//觀衆類
public class Audience {
	public Audience() {
	}
	//表演前找座位
	public void takeSeats() {
		System.out.println("The audience is taking their seats.");
	}
	//找到座位後關掉手機
	public void turnOffCellPhones() {
		System.out.println("The audience is turning off their cellphones");
	}
	//表演精彩時鼓掌
	public void applaud() {
		System.out.println("CLAP CLAP CLAP CLAP CLAP");
	}
	//表演槽糕時要求退票
	public void demandRefund() {
		System.out.println("Boo!We want our money back!");
	}
}
由Spring容器管理Audience:
<bean id="audience" class="com.springinaction.springido1.Audience" />

3.1創建通知

Spring的AOP裏有5種時間點的通知,分別由一個接口進行定義:

Spring AOP的5個時間點
通知類型 接口
Before org.springframework.aop.MethodBeforeAdvice
After-returning org.springframework.aop.AfterReturningAdvice
After-throwing org.springframework.aop.ThrowsAdvice
Around org.aopalliance.intercept.MethodInterceptor
Introduction org.springframework.aop.IntroductionInterceptor

除了MethodInterceptor之外,這些接口都屬於Spring框架。在定義周圍通知時,Spring利用了由AOP Alliance提供的接口,這是一個開源項目,其宗旨在於AOP的簡化及標準化。如果需要進一步瞭解AOP Alliance,可以訪問其站點http://aopalliance.sourceforge.net

現在就可以根據上面的場景建立一個通知(advice)了:
package com.sin90lzc.test;

import java.lang.reflect.Method;

import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.aop.ThrowsAdvice;

public class AudienceAdvice implements MethodBeforeAdvice,
		AfterReturningAdvice, ThrowsAdvice {

	private Audience audience;

	public void afterReturning(Object arg0, Method arg1, Object[] arg2,
			Object arg3) throws Throwable {
		audience.applaud();
	}

	public void before(Method arg0, Object[] arg1, Object arg2)
			throws Throwable {
		audience.takeSeats();
		audience.turnOffCellPhones();
	}

	public void afterThrowing(Throwable throwable) {

	}

	public Audience getAudience() {
		return audience;
	}

	public void setAudience(Audience audience) {
		this.audience = audience;
	}

}

這裏一個類中實現了3種不同類型的AOP通知。

前通知——MethodBeforeAdvice
在進入切入點(方法調用前)調用。MethodBeforeAdvice要求實現一個Before()方法:
public void before(Method arg0, Object[] arg1, Object arg2)
			throws Throwable {}
第一個參數代表要使用這個通知的方法。第二個參數是方法被調用時要傳遞的參數。最後一個參數是方法調用的目標(也就是被調用方法所在的對象)。

返回後通知——AfterReturningAdvice
這個通知需要實現afterReturning()方法:
public void afterReturning(Object arg0, Method arg1, Object[] arg2,
			Object arg3) throws Throwable {}
aferReturning方法的參數與MethodBeforeAdvice的before()方法的參數區別不大,只是第一個參數是從被調用方法返回的值。

拋出後通知——ThrowsAdvice
與MethodBeforeAdvice和AfterReturningAdvice不同的是,ThrowsAdvice不需要實現任何方法,它只是一個標記接口,告訴Spring相應的通知要求處理被拋出的異常。

對於ThrowsAdvice的實現可以是一個或多個afterThrowing()方法,其簽名具有如下形式:
public void afterThrowing([method],[args],[target],throwable);

周圍通知——MethodInterceptor
周圍通知相當於前通知、返回後通知、拋出後通知的結合。AudienceAdvice類可以用周圍通知來重寫。如:
package com.sin90lzc.test;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

public class AudienceAroundAdvice implements MethodInterceptor {

	public Object invoke(MethodInvocation invocation) throws Throwable {
		try {
			audience.takeSeats();
			audience.turnOffCellPhones();
			Object returnValue = invocation.proceed();
			audience.applaud();
			return returnValue;
		} catch (Exception e) {
			audience.demandRefund();
			throw e;
		}
	}

	public Audience getAudience() {
		return audience;
	}

	public void setAudience(Audience audience) {
		this.audience = audience;
	}

	private Audience audience;
}
使用周圍通知的好處之一是能以簡潔的方式在一個方法裏定義前通知和後通知。利用周圍通知還可以檢查和修改被通知方法的返回值,讓我們可以在把方法的返回值傳遞給調用者之前對其進行一些後處理。AfterReturningAdvice只能對返回值進行檢查,但不能修改它。

3.2 定義切入點和通知者

Advice(通知)已經解決了切面“做什麼”和“何時”的問題。接下來就要解決“在哪裏”的問題了。也就是切入點的定義。

Spring提供了幾種不同類型的切點,其中兩種最有用的是正則表達式切點和AspectJ表達式切點。

3.2.1聲明正則表達式切點

使用org.springframework.aop.supprot.JdkRegexpMethodPointcut類來定義正則表達式切點:
<bean id="performancePointcut" class="org.springframework.aop.support.JdkRegexpMethodPointcut">
	<property name="pattern" value=".*perform" />
</bean>

接下來需要把切入點與通知關聯起來,可以用類org.springframework.aop.support.DefaultPointcutAdvisor把這種關係關聯起來。
<bean id="audienceAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
	<property name="advice" ref="audienceAdvice" />
	<property name="pointcut" ref="performancePointcut" />
</bean>

DefaultPointcutAdvisor是個通知者類,它只是把通知關聯到切點。

聯合切點與通知
RegexpMethodPointcutAdvisor是個特殊的通知者類,可以在一個Bean裏定義切點和通知:
<bean id="audienceAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
	<property name="advice" ref="audienceAdvice" />
	<property name="pattern" value=".*perform" />
</bean>

3.2.2定義AspectJ切點

正則表達式雖然可以作爲切點定義語言來使用,但它並不是針對切點而創建的,其主要用途還是文本解析。與之相比,從AspectJ裏定義切點的方式就可以看出AspectJ的切點語言是一種真正的切點表達語言。
Spring使用類org.springframework.aop.aspectj.AspectJExpressionPointcut來定義AspectJ切點表達式:
<bean id="performancePointcut" class="org.springframework.aop.aspectj.AspectJExpressionPointcut">
	<property name="expression" value="execution(* Performer+.perform(..))" />
</bean>

爲了把AspectJ表達式切點與通知關聯起來,可以使用DefaultPointcutAdvisor,就像正則表達式切點一樣。同樣的,我們可以利用特殊的通知者,把切點表達式定義爲通知者的一個屬性。對於AspectJ表達式來說,使用的通知者類是org.springframework.aop.aspectj.AspectJExpressionPointcutAdvisor:
<bean id="audienceAdvisor" class="org.springframework.aop.aspectj.AspectJExpressionPointcutAdvisor">
	<property name="advice" ref="audienceAdvice" />
	<property name="expression" value="execution(* Performer+.perform(..))" />
</bean>

通知者把通知與切點關聯起來,從而完整地定義一個切面。但是,切面在Spring裏是以代理方式實現的,所以仍然需要代理目標Bean才能讓通知者發揮作用。

3.3 使用ProxyFactoryBean

Spring的ProxyFactoryBean是個工廠Bean,用於生成一個代理,把一個或多個通知者應用到Bean。示例:
<bean id="duke" class="org.springframework.aop.framework.ProxyFactoryBean">
	<property name="target" ref="dukeTarget" />
	<property name="interceptorNames">
		<list>
			<value>audienceAdvisor</value>
		</list>
	</property>
	<property name="proxyInterfaces">
		<list>
			<value>com.springinaction.springido1.Performer</value>
		</list>
	</property>
</bean>

當然,當有多個Bean都需要代理相同的接口或通知者時,可以抽象ProxyFactoryBean,達到簡化配置的目的。

4.自動代理

自動代理能夠讓切面的切點定義來決定哪個Bean需要代理,不需要我們爲特定的Bean明確地創建代理,從而提供了一個更完整的AOP實現。

實現自動代理Bean的方式有兩種:
  • “基於Spring上下文裏聲明的通知者Bean的基本自動代理”:通知者的切點表達式用於決定哪個Bean和哪個方法要被代理。
  • “基於@AspectJ註解驅動切面的自動代理”:切面裏包含的通知裏指定的切點將用於選擇哪個Bean和哪個方法要被代理。

4.1 爲Spring切面創建自動代理

Spring提供了BeanPostProcessor的一個方便實現:DefaultAdvisorAutoProxyCreator,它會自動檢查通知者的切點是否匹配Bean的方法,並且使用通知的代理來替換這個Bean的定義。要使用DefaultAdvisorAutoProxyCreator,只需要以下三步:
  1. 像上一節中定義通知者Bean。
  2. 添加DefaultAdvisorAutoProxyCreator的Bean定義。注意不需要id:
    <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" />

  3. 聲明目標Bean(像普通的Bean一樣聲明),但實際上會以代理Bean取代:
    <bean id="duke" class="com.springinaction.springido1.PoeticJuggler" autowire="constructor">
    <constructor-arg ref="sonnet29" />
    </bean>

4.2 自動代理@AspectJ切面

AspectJ5裏一個主要新功能是能夠把POJO類註解爲切面,這通常被稱爲@AspectJ。

利用@AspectJ註解,我們不需要聲明任何額外的類或Bean,就可以把它轉化爲一個切面。如:
package com.sin90lzc.test;

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.Pointcut;

//聲明切面
@Aspect
public class Audience {
	public Audience() {
	}

	// 定義切入點
	@Pointcut("execution(* *.perform(...))")
	public void performance() {
	}

	@Before("performance()")
	public void takeSeats() {
		System.out.println("The audience is taking their seats.");
	}

	@Before("performance()")
	public void turnOffCellPhones() {
		System.out.println("The audience is turning off their cellphones");
	}

	@AfterReturning("performance()")
	public void applaud() {
		System.out.println("CLAP CLAP CLAP CLAP CLAP");
	}

	@AfterThrowing("performance()")
	public void demandRefund() {
		System.out.println("Boo!We want our money back!");
	}
}
@Pointcut註解在@AspectJ切面裏定義一個可重用的切點。賦給@Pointcut註解的值是一個AspectJ切點表達式,表示這個切點應該匹配任意一個類的perform()方法。這個切點的名稱是源自注解所應用的方法的名稱,所以本例中切點的名稱就是performance()。實際的performance()方法與此並不相關,方法本身只是一個標記。

最後,我們還必須在Spring上下文裏聲明一個自動代理Bean,它知道如何把@AspectJ註解的Bean轉化爲代理通知。Spring提供了一個自動代理創建器類——AnnotaionAwareAspectJAutoProxyCreator,我們可以在Spring上下文裏把它註冊爲一個<bean>。爲了簡化,可使用Spring提供的一個自定義配置元素:
<aop:aspectj-autoproxy />

這個元素會在Spring上下文創建一個AnnotaionAwareAspectJAutoProxyCreator,從而根據@Pointcut註解定義的切點來自動代理相匹配的Bean。要使用該配置元素,還需要添加aop命名空間:
<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.0.xsd
	http://www.springframework.org/schema/aop
	http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">
</beans>

AnnotaionAwareAspectJAutoProxyCreator也基於典型的Spring通知者來創建代理,也就是說,它也會完成與DefaultAdvisorAutoProxyCreator同樣的工作。因此,如果在Spring上下文裏聲明瞭通知者Bean,它們也會被自動用於通知被代理的Bean。

4.3 定義純粹的POJO切面

Spring2.0在aop命名空間裏還提供了其他一些配置元素,簡化了把類轉化爲切面的操作。
Spring2.0的AOP配置元素
AOP配置元素 功能
<aop:advisor> 定義一個AOP通知者
<aop:after> 定義一個AOP後通知(不考慮被通知的方法是否成功返回)
<aop:after-returning> 定義一個AOP返回後通知
<aop:after-throwing> 定義一個AOP拋出後通知
<aop:around> 定義一個AOP周圍通知
<aop:aspect> 定義一個切面
<aop:before> 定義一個AOP前通知
<aop:config> 頂級AOP元素。大多數<aop:*>元素必須包含在<aop:config>裏
<aop:Pointcut> 定義一個切點

下面示例把audience Bean轉化爲切面:
<bean id="audience" class="com.springinaction.springido1.Audience" />

<aop:config>
	<aop:aspect ref="audience">
		<aop:pointcut id="performance" expression="execution(* *.perform(..))" />
		<aop:before method="takeSeats" pointcut-ref="performance" />
		<aop:before method="turnOffCellPhones" pointcut="execution(* *.perform(..))" />
		<aop:after-returning method="applaud"
			pointcut-ref="performance" />
		<aop:after-throwing method="demandRefund"
			pointcut-ref="performance" />
	</aop:aspect>
</aop:config>

4.4 注入AspectJ切面

發佈了30 篇原創文章 · 獲贊 31 · 訪問量 42萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章