10.3 開發Spring 1.2 AOP應用
本節將會給大家展示一個恐怖的例子,FBI特務人員已經介入了您的生活,您所做的一切都在他們的監視之中,包括聊QQ,泡MM,這在現實生活中是真實存在的,為了民眾的安全和穩定,對嫌疑犯進行必要的監控是必要的。
注意:本章雖然介紹了多種AOP實現方式,然而,在實際項目中只要使用一種就可以達到目的了(因為Spring的AOP存在多種寫法,完全掌握還是挺復雜),其它方式僅供參考,千萬不要像孔乙己一樣,研究“茴”字的N種寫法,這樣就脫離了學習技術的初衷了:學習是為了解決問題,不是為了炫耀自己。另外,如果在項目中濫用AOP的後果就是系統的執行效率大大降低,甚至配置不當會導致死循環。記住一個真理:系統越復雜,效率越低,出故障的可能越大。另外一條建議:千萬不要用AOP在服務器上記錄日志,或者在服務器上打印不必要的調試信息,那樣對系統只能有害無益,日志輸出是單線程操作,切記。做項目,一般來說是功能越少越好。高手更多的時候只能做出破壞力大,不易維護的垃圾系統。
10.3.1 開發Man對象
這個項目非常簡單,仿照上節內容,創建項目並添加Spring開發功能,不同的是添加library的時候要把Spring 2.0 AOP Libraries加入進來。因為Spring 2.0的類庫是兼容1.2的,所以這裡就用2.0了。項目名為Spring1_2AOP。接下來我們要創建一個自由人的對象,他有聊QQ和泡MM這兩個方法,還有一個姓名屬性。好了,先建立這個類:
/** * 具有聊QQ和泡MM兩個行為的人對象,還有一個用戶名屬性。* @author BeanSoft*/public class Man { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } public void qq() { System.out.println("我在聊QQ"); } public void mm() { System.out.println("我在泡MM"); } }
清單10.6 Man類源碼
10.3.2 開發前置通知(Before advice)對象:FBI
首先貼一段Spring文檔中關於Before advice的介紹:
前置通知(Before advice): 在某連接點(join point)之前執行的通知,但這個通知不能阻止連接點前的執行(除非它拋出一個異常)。
說通俗點就是寫一個如何處理監視結果的對象,可以把監視結果打印出來以作為必要的時候的呈堂證物,或者派探員立即跟蹤,但是這個過程只能在你進行某活動前進行,否則就失去監視的意義了,這個對象更像“諸葛亮”。詳細的了解這個類需要學習JDK裡面關於反射部分的內容,下面是這個類的代碼:
import java.lang.reflect.Method; import org.springframework.aop.MethodBeforeAdvice; /*** 聯邦調查局的探員將您的所有行動都記錄在案。 * @author BeanSoft*/public class FBI implements MethodBeforeAdvice { public void before(Method method, Object[] args, Object target) throws Throwable { Man man = (Man)target; System.err.println("FBI 發現" + man.getName() + "正在進行 " + method.getName() + " 活動。"); } }
清單10.7 FBI類源碼
10.3.3 裝配攔截器和Bean
最後要做的,就是創建一個平民對象,注意不是自由人哦,因為平民是隨時處於FBI的監視之下的。這個對象本質上是類ProxyFactoryBean的一個示例,這個類位於包org.springframework.aop.framework下,自由人只能活在這個代理工廠類的陰影下了,也就是成了平民了。好了,我們把相應的Spring配置文件代碼放給大家:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd"> <bean id="man" class="Man"> <property name="name"> <value type="java.lang.String">張三</value> </property> </bean> <bean id="fbi" class="FBI" /> <bean id="civilian" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="target"> <ref bean="man" /> </property> <property name="interceptorNames"> <list> <value>fbi</value> </list> </property> </bean> </beans>
清單10.8 Spring AOP 配置文件源碼 applicationContext.xml
在這個文件中,定義了兩個bean:man和fbi,都是普通的類定義。復雜一些的地方在civilian這個bean的定義中,它要攔截或者監視的目標(target)是man,負責進行處理監視結果的對象(interceptorNames)是fbi,具體進行監視工作的對象,就是這個ProxyFactoryBean,它相當於竊聽器之類的東西,但是很顯然竊聽結果是需要人來處理的,那就是FBI。
簡單說: man成為了civilian,它被ProxyFactoryBean監控,監控結果交給FBI處理。很顯然,如果沒有國家,也就沒有civilian,更談不上FBI了。所以,在這裡您不能再去找man,因為man在實際生活中是不存在的,所以您只能找civilian,這樣您才能感覺到FBI的存在。下面是相關的bean關系圖:
圖 10.9 Bean關系圖
10.3.4 測試和運行
OK,如上節討論,只有當平民的時候才會被監視,現在我們就可以寫一個測試類來感受一下平民生活:
import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext;public class AOPTest { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); Man man = (Man) ctx.getBean("civilian"); man.qq(); man.mm(); } }
清單10.9 Spring AOP 測試類源碼
運行一下,您就可以看到可怕的真相:
log4j:WARN No appenders could be found for logger (org.springframework.context.support.ClassPathXmlApplicationContext). log4j:WARN Please initialize the log4j system properly. FBI 發現張三正在進行 qq 活動。FBI 發現張三正在進行 mm 活動。我在聊QQ 我在泡MM
是不是很恐怖呢?FBI正在監控您的一舉一動,並把這些東西都記錄在案。現在你應該可以了解AOP的過程了:調用man這個bean的任何一個方法之前,都會事先通知(調用)fbi這個bean並告知相關的調用信息,這些信息包括方法(method),參數(args)以及目標對象(target,這裡就是man這個對象)。
如果你把上面的代碼改成ctx.getBean(“man”),那麼自由人是不會被監控的,所以這時候您就不會看到FBI輸出的恐怖信息了。此時的輸出如下:
log4j:WARN No appenders could be found for logger (org.springframework.context.support.ClassPathXmlApplicationContext). log4j:WARN Please initialize the log4j system properly.我在聊QQ 我在泡MM
10.3.5 AOP簡介和相關概念
現在我們已經寫了一個很簡單的Spring AOP 例子,現在就給大家簡單介紹一下相關的概念,這些信息來自於Spring的中文文檔。
面向切面編程(AOP)提供另外一種角度來思考程序結構,通過這種方式彌補了面向對象編程(OOP)的不足。 除了類(classes)以外,AOP提供了 切面。切面對關注點進行模塊化,例如橫切多個類型和對象的事務管理。 (這些關注點術語通常稱作 橫切(crosscutting) 關注點。)
我們來定義一些重要的AOP概念。這些術語不是Spring特有的。 不幸的是,Spring術語並不是特別的直觀;如果Spring使用自己的術語,將會變得更加令人困惑。
l 切面(Aspect): 一個關注點的模塊化,這個關注點可能會橫切多個對象。事務管理是J2EE應用中一個關於橫切關注點的很好的例子。 在Spring AOP中,切面可以使用通用類(基於模式的風格) 或者在普通類中以 @Aspect注解(@AspectJ風格)來實現。
l 連接點(Joinpoint): 在程序執行過程中某個特定的點,比如某方法調用的時候或者處理異常的時候。 在Spring AOP中,一個連接點 總是 代表一個方法的執行。 通過聲明一個org.aspectj.lang.JoinPoint類型的參數可以使通知(Advice)的主體部分獲得連接點信息。
l 通知(Advice): 在切面的某個特定的連接點(Joinpoint)上執行的動作。通知有各種類型,其中包括“around”、“before”和“after”等通知。 通知的類型將在後面部分進行討論。許多AOP框架,包括Spring,都是以攔截器做通知模型, 並維護一個以連接點為中心的攔截器鏈。
l 切入點(Pointcut): 匹配連接點(Joinpoint)的斷言。通知和一個切入點表達式關聯,並在滿足這個切入點的連接點上運行(例如,當執行某個特定名稱的方法時)。 切入點表達式如何和連接點匹配是AOP的核心:Spring缺省使用AspectJ切入點語法。
l 引入(Introduction): (也被稱為內部類型聲明(inter-type declaration))。聲明額外的方法或者某個類型的字段。 Spring允許引入新的接口(以及一個對應的實現)到任何被代理的對象。 例如,你可以使用一個引入來使bean實現IsModified接口,以便簡化緩存機制。
l 目標對象(Target Object): 被一個或者多個切面(aspect)所通知(advise)的對象。也有人把它叫做 被通知(advised) 對象。 既然Spring AOP是通過運行時代理實現的,這個對象永遠是一個 被代理(proxied) 對象。
l AOP代理(AOP Proxy): AOP框架創建的對象,用來實現切面契約(aspect contract)(包括通知方法執行等功能)。 在Spring中,AOP代理可以是JDK動態代理或者CGLIB代理。 注意:Spring 2.0最新引入的基於模式(schema-based)風格和@AspectJ注解風格的切面聲明,對於使用這些風格的用戶來說,代理的創建是透明的。
l 織入(Weaving): 把切面(aspect)連接到其它的應用程序類型或者對象上,並創建一個被通知(advised)的對象。 這些可以在編譯時(例如使用AspectJ編譯器),類加載時和運行時完成。 Spring和其他純Java AOP框架一樣,在運行時完成織入。 通知的類型:
l 前置通知(Before advice): 在某連接點(join point)之前執行的通知,但這個通知不能阻止連接點前的執行(除非它拋出一個異常)。
l 返回後通知(After returning advice): 在某連接點(join point)正常完成後執行的通知:例如,一個方法沒有拋出任何異常,正常返回。
l 拋出異常後通知(After throwing advice): 在方法拋出異常退出時執行的通知。
l 後通知(After (finally) advice): 當某連接點退出的時候執行的通知(不論是正常返回還是異常退出)。
l 環繞通知(Around Advice): 包圍一個連接點(join point)的通知,如方法調用。這是最強大的一種通知類型。 環繞通知可以在方法調用前後完成自定義的行為。它也會選擇是否繼續執行連接點或直接返回它們自己的返回值或拋出異常來結束執行。 環繞通知是最常用的一種通知類型。
大部分基於攔截的AOP框架,例如Jboss,以及EJB 3裡面的攔截器(後續章節我們會加以介紹),都只提供環繞通知。 跟AspectJ一樣,Spring提供所有類型的通知,我們推薦你使用盡量簡單的通知類型來實現需要的功能。
例如,如果你只是需要用一個方法的返回值來更新緩存,雖然使用環繞通知也能完成同樣的事情, 但是你最好使用After returning通知而不是環繞通知。 用最合適的通知類型可以使得編程模型變得簡單,並且能夠避免很多潛在的錯誤。 比如,你不需要調用JoinPoint(用於Around Advice)的proceed() 方法,就不會有調用的問題。 在Spring 2.0中,所有的通知參數都是靜態類型,因此你可以使用合適的類型(例如一個方法執行後的返回值類型)作為通知的參數而不是使用一個對象數組。
切入點(pointcut)和連接點(join point)匹配的概念是AOP的關鍵,這使得AOP不同於其它僅僅提供攔截功能的舊技術。 切入點使得定位通知(advice)可獨立於OO層次。 例如,一個提供聲明式事務管理的around通知可以被應用到一組橫跨多個對象中的方法上(例如服務層的所有業務操作)。
10.4 開發 Spring 2.0 AOP 應用
Spring 2.0實現了兩種方式的AOP配置,一種是基於XML配置文件式的,可以用在JDK1.4上,另一種是基於@AspectJ風格的標注(Annotation)進行AOP開發,可以用在JDK1.5的系統上。本節就對上節的應用進行改寫,使用Spring 2.0 AOP的方式來開發。關於Spring AOP的資料和相關概念的詳細信息,可以閱讀Spring的中文文檔。
10.4.1 使用aop 標簽實現AOP
這種方式相對繁瑣,所不同的是不需要再定義ProxyFactoryBean的實例,而且自動給相關的bean定義加入AOP功能,不在需要顯式的去訪問代理過的另外給出名字的bean定義了。
好了,現在讓我們新建一個項目,名為Spring2_0AOP,並按照10.3節內容設置好必要的類庫和必要的文件。那麼再這個例子中,Man類的代碼不需要做任何修改。需要改的是FBI這個類,而且它也不需要再實現某些接口了,類的源碼如下所示:
import org.aspectj.lang.JoinPoint;
/**
*聯邦調查局的探員將您的所有行動都記錄在案。
*@authorBeanSoft
*/
publicclass FBI {
publicvoid before(JoinPoint point){
Man man = (Man)point.getTarget();
System.err.println("FBI 發現" + man.getName() + "正在進行 " +
point.getSignature().getName() + " 活動。");
}
}
清單10.10 FBI類源碼
注意這個類裡面的方法 before(JoinPoint),方法名可以是任意的,可以帶一個JoinPoint類型的參數,也可以不帶參數直接寫成before(),但是這個連接點(JoinPoint)對象帶來了所有和這次方法調用有關的信息,包括方法參數,目標對象等等,所以一般要做日志記錄的話會帶上它。
接下來是測試類的代碼,和以前的幾乎沒有任何不同,只不過現在直接訪問的是man這個bean。源碼如下所示:
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
publicclass AOPTest {
publicstaticvoid main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
Man man = (Man) ctx.getBean("man");
man.qq();
man.mm();
}
}
清單10.11 測試類AOPTest源碼
這個類的執行結果和上面的例子是類似的,如下所示:
log4j:WARN No appenders could be found for logger (org.springframework.context.support.ClassPathXmlApplicationContext).
log4j:WARN Please initialize the log4j system properly.
FBI 發現張三正在進行 qq 活動。
我在聊QQ
FBI 發現張三正在進行 mm 活動。
我在泡MM
下面再介紹配置文件的寫法,先看看完整的配置文件代碼:
<?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.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">
<bean id="fbi" class="FBI" />
<bean id="man" class="Man">
<property name="name">
<value type="java.lang.String">張三</value>
</property>
</bean>
<aop:config>
<aop:pointcut id="manPointcut"
expression="execution(* Man.*(..))" />
<aop:aspect id="beforeExample" ref="fbi">
<aop:before pointcut-ref="manPointcut" method="before" />
</aop:aspect>
</aop:config>
</beans>
清單10.12 AOP XML格式配置文件源碼applicationContext.xml
將這個配置文件的代碼和清單10.8進行對比,可以看到兩個不同:
1. 配置文件的開頭加入了aop命名空間,如代碼中粗斜體所示。
2. 使用aop:config標簽來定義AOP,不是使用ProxyFactoryBean來定義一個新的bean。
簡要介紹一下這個配置。兩個bean的定義都沒有什麼大的變化,一個是人的對象,另一個則是聯邦調查局的探員。而aop:config中定義了所有的AOP設置信息。aop:pointcut定義了一個切入點,id給出了這個切入點的唯一名字,而expression定義了切入點的表達式,那麼這個定義到底表示了什麼信息呢?它的意思是表示一種場景,即執行(execution)Man對象的所有方法的這種情況,這就是表達式execution(* Man.*(..))的意義所在,Man.*(..)表示Man類的所有方法。接下來呢,需要定義一個切面,用aop:aspect來定義,它的ref屬性指定了這個切面所對應的bean定義的id,這裡指向fbi這個bean類;子標簽aop:before則指示了當發生了名為manPointcut的切入點(情況)前(用pointcut-ref屬性指定,pointcut-ref=”manPointcut”),就調用名為before的方法,這個方法位於aspect裡面的引用的那個bean中,這裡是fbi(即ref=”fbi”)。其實Spring執行到這裡後,會自動的把這些代碼翻譯成底層的Bean定義(後台依然會采用ProxyFactoryBean這樣的機制),然後把對應的獲取bean的操作直接委托給代理類,這就是為什麼上文提到的測試類只需要訪問原來的man這個bean,對應的攔截類就會被執行的原因。從這裡看到Spring 2.0中要定義一個AOP的bean類,仍然是比較復雜的,XML文件和概念都增加了很多,需要讀者慢慢來學習和理解。
本節的詳細參考資料可以閱讀Spring參考文檔的6.3. Schema-based AOP support一節。
10.4.2使用標注(@AspectJ)實現AOP
下面的文檔來自於Spring:"@AspectJ"使用了Java 5的注解,可以將切面聲明為普通的Java類。 AspectJ 5發布的 AspectJ project (http://www.eclipse.org/aspectj)中引入了這種@AspectJ風格。 Spring 2.0 使用了和AspectJ 5一樣的注解,使用了AspectJ 提供的一個庫來做切點(pointcut)解析和匹配。
為了在Spring配置中使用@AspectJ aspects,你必須首先啟用Spring對基於@AspectJ aspects的配置支持,自動代理(autoproxying)基於通知是否來自這些切面。 自動代理是指Spring會判斷一個bean是否使用了一個或多個切面通知,並據此自動生成相應的代理以攔截其方法調用,並且確認通知是否如期進行。
通過在你的Spring的配置文件中引入下列元素來啟用Spring對@AspectJ的支持:
<aop:aspectj-autoproxy/>
也可以通過在你的application context中添加如下定義來啟用@AspectJ支持:
<bean class="org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator" />
你需要在你的應用程序的classpath中引入兩個AspectJ庫:aspectjweaver.jar和 aspectjrt.jar。我們這裡用的MyEclipse,在添加Spring開發功能時已經自動的加入了這些類庫文件,無需手工配置了。
定義切面Aspect:在啟用@AspectJ支持的情況下,在application context中定義的任意帶有一個@Aspect切面(擁有@Aspect注解)的bean都將被Spring自動識別並用於配置在Spring AOP。
定義切入點Pointcut:現在通過在 @AspectJ 注解風格的 AOP 中,一個切入點簽名通過一個普通的方法定義來提供,並且切入點表達式使用 @Pointcut 注解來表示(作為切入點簽名的方法必須返回 void 類型)。
好了,引用了這麼些文檔,我們需要介紹這個基於標注的新的AOP項目了,這個項目的名字是Spring2_0AOPAspectJ,如前一節所示加入了Spring核心和AOP類庫後,就可以開發了。那麼相比較10.4.1 使用aop 標簽實現AOP一節,這一個項目的代碼僅僅有兩個地方要改。首先我們要修改FBI類的源碼,加入標注來實現切面和切入點定義,如下所示:
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
/**
*聯邦調查局的探員將您的所有行動都記錄在案。
*@authorBeanSoft
*/
@Aspect
publicclass FBI {
@Before("execution(* Man.*(..))")
publicvoid before(JoinPoint point){
Man man = (Man)point.getTarget();
System.err.println("FBI 發現" + man.getName() + "正在進行 " +
point.getSignature().getName() + " 活動。");
}
}
這個類中的@Before後面的"execution(* Man.*(..))"是切入點所對應的切入點表達式,其意義和上一節的是一致的,仍然表示的是執行 Man 類的所有方法時將觸發此方法的執行。