時間:2005-12-23
作者:
Russell Miles瀏覽次數:
3616
本文關鍵字:
Spring,?
Java,?
AOP,?
crosscutting,?
面向方面編程,?
橫切,?
Cuckoo's Egg模式 在本系列的第一部分,我介紹了如何實現面向方面領域的“HelloWorld”:跟蹤和記錄方面。利用Spring框架所提供的面向方面編程(Aspect-Oriented Programming,AOP)功能,您看到了如何使用before-、after-和基于異常的通知,以及如何使用基于正則表達式的簡單切入點。跟蹤和記錄方面提供了非常不錯的上手例子,而本文將進一步介紹一種新的通知形式:around通知。
比起第一部分中介紹的那些通知類型,around形式的通知是一種更具侵入性也更強大的面向對象概念。本文將描述around通知的每個特性,以便您可以在自己的Spring AOP應用程序中正確地使用它。在本文最后,我將向您展示如何使用around通知來截獲和改變應用程序中各個特性相互作用的方式,以便實現Cuckoo's Egg(杜鵑的蛋)面向方面設計模式。
概述Spring AOP、IoC和代理
在第一部分,我們快速瀏覽了Spring的一些AOP特性,而沒有闡明Spring如何實現AOP的細節。要理解Spring框架如何運轉,尤其是它如何實現其AOP功能,首先您要明白,Spring是一個依賴于控制反轉(Inversion of Control,IoC)設計模式的輕量級框架。
注意:本文的目的不是要深入介紹IoC模式,介紹IoC只是為了使您明白該設計模式是如何影響Spring AOP實現的。有關IoC模式的更詳細的介紹請參見本文末尾的參考資料。
IoC設計模式的出現已經有一段時間了。一個最明顯的例子就是J2EE架構本身。隨著企業開發尤其是J2EE平臺的出現,應用程序開始依賴于由外部容器所提供的一些特性,比如bean創建、持久性、消息傳遞、會話以及事務管理。
IoC引入了一個新概念:由組件構成的框架,它與J2EE容器有許多類似之處。IoC框架分離了組件所依賴的功能,并且,根據Sam Newman文章中的說法,提供了“連接組件的‘膠水’”。
對組件所依賴特性的控制 被反轉 了,這樣外部框架就可以盡可能透明地提供這些特性了。IoC模式真正意識到了從傳統的由依賴于功能的組件來負責這些功能,到由獨立的框架來配置和提供這些功能的方式轉變。
圖1顯示了一些構成IoC模式的不同組件角色的例子。

圖1. 沒有對BusinessLogic bean應用方面時的順序圖.
圖字:
Component:組件
Provides Facilities:提供功能
Relies on and conforms to:依賴于并服從
Manages the services the framework can then use to provide facilities:管理框架隨后可以用來提供功能的服務
Service:服務
Your Component:您的組件
IoC Framework:IoC框架
External services:外部服務
IoC模式使用3種不同的方法來解除組件與服務控制的耦合:類型1、類型2和類型3。
- 類型1:接口注入
這是大部分J2EE實現所使用的方法。組件顯式地服從于一組接口,帶有關聯的配置元數據,以便允許框架對它們進行正確的管理。
- 類型2:Setter注入
外部元數據被用來配置組件相互作用的方式。在第一部分中,我們就是使用這種IoC方法利用springconfig.xml文件來配置Spring組件的。
- 類型3:構造函數注入
組件(包括構造組件時要用的參數)注冊到框架,而框架提供組件的實例以及所有要應用的指定功能。
IoC在組件開發和企業開發中越來越受歡迎。IoC的實際例子包括傳統的J2EE解決方案,比如:JBoss、Apache基金會的Avalon項目以及本文的Spring框架。實際上,Spring框架構建于IoC模式的基礎上是為了幫助將它的輕量級功能注入到它的相關應用程序的組件中。
那么IoC對于Spring AOP有何意義呢?Spring的IoC特性是使用IoC springconfig.xml配置文件對應用程序應用方面的推動因素之一。springconfig.xml配置文件通知Spring框架運行時有關應用程序的組件要被注入的功能類型的信息,所以自然輕量級的AOP功能就以同樣的方式應用了。然后Spring使用代理模式圍繞現有的類和bean實現指定的AOP功能。
圖2顯示了Spring及其IoC框架如何使用代理對象提供AOP功能(根據springconfig.xml文件中的IoC配置。)

圖2. springconfig.xml配置文件改變了Spring框架IoC,以便隨后向第一部分中的一個順序圖提供AOP代理(單擊圖像查看大圖)
在本系列下面的部分,您將不斷看到現在包含在順序圖中的代理對象。這只是為了說明對于Spring AOP來說沒有“魔法”,實際上只有一個面向對象設計模式的良好例子。
回到AOP:使用around通知的積極方面
在第一部分,您看到了如何使用Spring AOP來實現跟蹤和記錄方面。跟蹤和記錄都是“消極”方面,因為它們的出現并不會對應用程序的其他行為產生影響。它們都使用了消極的before和after形式的通知。
但是如果您希望改變應用程序的常規行為呢?例如說,您希望重寫一個方法?這樣的話,您就需要使用更積極的around形式的通知。
第一部分的簡單例子應用程序包括IbusinessLogic接口、BusinessLogic類和MainApplication類,如下所示:
public interface IBusinessLogic
{
public void foo();
}
public class BusinessLogic
implements IBusinessLogic
{
public void foo()
{
System.out.println(
"Inside BusinessLogic.foo()");
}
}
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;
public class MainApplication
{
public static void main(String [] args)
{
// Read the configuration file
ApplicationContext ctx =
new FileSystemXmlApplicationContext(
"springconfig.xml");
//Instantiate an object
IBusinessLogic testObject =
(IBusinessLogic) ctx.getBean(
"businesslogicbean");
// Execute the public
// method of the bean
testObject.foo();
}
}
要對一個BusinessLogic類的實例徹底重寫對foo()方法的調用,需要創建around通知,如下面的AroundAdvice類所示:
import org.aopalliance.intercept.MethodInvocation;
import org.aopalliance.intercept.MethodInterceptor;
public class AroundAdvice
implements MethodInterceptor
{
public Object invoke(
MethodInvocation invocation)
throws Throwable
{
System.out.println(
"Hello world! (by " +
this.getClass().getName() +
")");
return null;
}
}
要在Spring中用作around通知,AroundAdvice類必須實現MethodInterceptor接口和它的invoke(..)方法。每當截獲到方法的重寫,invoke(..)方法就會被調用。最后一步是改變包含在應用程序的springconfig.xml文件中的Spring運行時配置,以便可以對應用程序應用AroundAdvice。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
"http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- Bean configuration -->
<bean id="businesslogicbean"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces">
<value>IBusinessLogic</value>
</property>
<property name="target">
<ref local="beanTarget"/>
</property>
<property name="interceptorNames">
<list>
<value>theAroundAdvisor</value>
</list>
</property>
</bean>
<!-- Bean Classes -->
<bean id="beanTarget"
class="BusinessLogic"/>
<!-- Advisor pointcut definition for around advice -->
<bean id="theAroundAdvisor"
class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<property name="advice">
<ref local="theAroundAdvice"/>
</property>
<property name="pattern">
<value>.*</value>
</property>
</bean>
<!-- Advice classes -->
<bean id="theAroundAdvice"
class="AroundAdvice"/>
</beans>
根據該springconfig.xml配置文件,theAroundAdvisor截獲所有對BusinessLogic類的方法的調用。接下來,theAroundAdvisor被關聯到theAroundAdvice,表明當截獲一個方法時,就應該使用在AroundAdvice類中指定的通知。既然已經指定了around通知的正確配置,下一次執行MainApplication類時,BusinessLogic bean的foo()方法就會被截獲并重寫,如圖3所示:

圖3. 使用around通知重寫對BusinessLogic類中的foo()方法的調用
前面的例子顯示,BusinessLogic類中的foo()方法可以通過AroundAdvice類中的invoke(..)方法徹底重寫。原來的foo()方法完全不能被invoke(..)方法調用。如果希望從around通知內調用foo()方法,可以使用proceed()方法,可從invoke(..)方法的MethodInvocation參數中得到它。
public class AroundAdvice
implements MethodInterceptor
{
public Object invoke(
MethodInvocation invocation)
throws Throwable
{
System.out.println(
"Hello world! (by " +
this.getClass().getName() +
")");
invocation.proceed();
System.out.println("Goodbye! (by " +
this.getClass().getName() +
")");
return null;
}
}
圖4顯示了對proceed()的調用如何影響操作的順序(與圖3所示的初始around通知執行相比較)。

圖4. 從around通知內使用proceed()調用原來的方法
當調用proceed()時,實際是在指示被截獲的方法(在本例中是foo()方法)利用包含在MethodInvocation對象中的信息運行。您可以通過調用MethodInvocation類中的其他方法來改變該信息。
您可能希望更改包含在MethodInvocation類中的信息,以便在使用proceed()調用被截獲的方法之前對被截獲方法的參數設置新值。
通過對MethodInvocation對象調用getArguments()方法,然后在返回的數組中設置其中的一個參數對象,最初傳遞給被截獲的方法的參數可以被更改。
如果IbusinessClass和BusinessLogic類的foo()方法被更改為使用整型參數,那么就可以將傳遞給被截獲的調用的值由在AroundAdvice的notify(..)方法中傳遞改為在foo(int)中傳遞。
public class AroundAdvice
implements MethodInterceptor
{
public Object invoke(
MethodInvocation invocation)
throws Throwable
{
System.out.println(
"Hello world! (by " +
this.getClass().getName() +
")");
invocation.getArguments()[0] = new Integer(20);
invocation.proceed();
System.out.println(
"Goodbye! (by " +
this.getClass().getName() +
")");
return null;
}
}
在本例中,被截獲的方法的第一個形參被假設為int。實參本身是作為對象傳遞的,所以通過將其包裝在Integer類實例中的方法,基本的int類型的形參被改為對應數組中的新值。如果您將該參數設置為一個非Integer對象的值,那么在運行時就會拋出IllegalArgumentException異常。
您還將注意到,invoke(..)方法必須包含一個return語句,因為該方法需要返回值。但是,被重寫的foo()方法并不返回對象,所以invoke(..)方法可以以返回null結束。如果在foo()方法不需要的情況下,您仍然返回了一個對象,那么該對象將被忽略。
如果foo()方法確實需要返回值,那么需要返回一個與foo()方法的初始返回類型在同一個類或其子類中的對象。如果foo()方法返回一個簡單類型,例如,一個integer,那么您需要返回一個Integer類的對象,當方法被重寫時,該對象會自動由AOP代理拆箱,如圖5所示:

圖5. around通知的裝箱和自動拆箱
圖字:
Object invoke:對象調用
The integer return value is boxed in a Integer object in the AroundAdvice and then unboxed by the AOP Proxy:整型返回值被裝箱在AroundAdvic通知的一個Integer對象中,然后由AOP代理拆箱。
面向方面編程還是一個比較新的領域,尤其是與衍生出它的面向對象編程相比。設計模式通常被認為是常見問題的通用解決方案,因為面向方面發展的時間還不長,所以已發現的面向方面設計模式比較少。
此處要介紹的是一種正在浮現的模式,即Cuckoo's Egg設計模式。該模式還有其他的叫法,它在面向對象領域的對等體包括模仿對象(Mock Object)和模仿測試(Mock Testing),甚至代理模式也與它有一些類似之處。
Cuckoo's Egg面向方面設計模式可以被定義為應用程序上下文中功能部件的透明和模塊化的置換。就像杜鵑偷偷地把自己的蛋放在另一種鳥的巢中一樣,Cuckoo's Egg設計模式用一個替代功能部件實現置換現有的功能部件,而使造成的干擾盡可能少。
這種置換的實現方式可以是靜態的、動態的、部分的、完全的,針對一個對象的多個部分,或針對多個組件。使用面向方面的方法可以透明地實現功能部件的置換,而無需對應用程序的其余部分進行更改。要置換應用程序中現有功能部件的替代功能部件就是“杜鵑的蛋”。圖6顯示了Cuckoo's Egg設計模式中的主要組成元素。

圖6. Cuckoo's Egg設計模式中的主要組成元素
圖字:
Application:應用程序
Component:組件
Replacement Feature:替代功能部件
Component 1 and 2 together encompass a distinct feature of the software:組件1和2共同包含了軟件的一個獨立的功能部件
The Cuckoo's Egg pattern transparently replaces an existing feature of the software:Cuckoo's Egg模式透明地置換了軟件現有的功能部件
Before the pattern is applied:應用該模式前
After the pattern is applied:應用該模式后
Cuckoo's Egg設計模式依賴于around通知的概念。您需要借助于積極的和侵入性的around通知來截獲并有效置換應用程序中現有的功能部件。
有關Cuckoo's Egg設計模式的更多信息,以及AspectJ中的一個可選實現,請參見《AspectJ Cookbook》(O'Reilly,2004年12月出版)。
要使用Spring AOP實現Cuckoo's Egg設計模式,需要聲明一個around通知來截獲所有對要置換的功能部件的調用。與hot-swappable target sources(Spring AOP的一個功能部件,將在本系列的另一篇文章中介紹)不同,around通知的顯式使用使得Cuckoo's Egg實現可以有效地跨越對象邊界(因此也可以跨越bean邊界)進行整個功能部件的置換,如圖7所示。

圖7. 一個跨越bean邊界的組件
圖字:
A feature crosses the boundaries of BusinessLogic and BusinessLogic2 by depending on behavior supplied separately by the two beans:一個功能部件通過依賴于由BusinessLogic和BusinessLogic2各自提供的行為而跨越了這兩個bean的邊界
下面的代碼顯示了一個具有兩個bean的簡單應用程序,其中有一個功能部件跨越了該應用程序的多個方面。要置換的功能部件可以被視為包含IBusinessLogic bean中的foo()方法和IBusinessLogic2 bean中的bar()方法。IBusinessLogic2 bean中的baz()方法不是 該功能部件的一部分,所以不進行置換。
public interface IBusinessLogic
{
public void foo();
}
public interface IBusinessLogic2
{
public void bar();
public void baz();
}
該例子的完整源代碼可在本文末尾的參考資料小節中下載。
此處,ReplacementFeature類扮演了“杜鵑的蛋”的角色,它提供了將被透明地引入應用程序的替代實現。ReplacementFeature類實現了所有在該類引入時要被置換的方法。
public class ReplacementFeature
{
public void foo()
{
System.out.println(
"Inside ReplacementFeature.foo()");
}
public void bar()
{
System.out.println(
"Inside ReplacementFeature.bar()");
}
}
現在需要聲明一個around通知來截獲對跨越bean的功能部件的方法調用。CuckoosEgg類提供了某種around通知來檢查被截獲的方法,并將適當的方法調用傳遞給ReplacementFeature類的實例。
public class CuckoosEgg implements MethodInterceptor
{
public ReplacementFeature replacementFeature =
new ReplacementFeature();
public Object invoke(MethodInvocation invocation)
throws Throwable
{
if (invocation.getMethod().getName().equals("foo"))
{
replacementFeature.foo();
}
else
{
replacementFeature.bar();
}
return null;
}
}
因為與Spring框架關系密切,Cuckoo's Egg設計的詳細信息被放在springconfig.xml配置文件中。對springconfig.xml文件的更改將確保所有對IbusinessLogic和IBusinessLogic2 bean的foo()方法和bar()方法的調用都將被截獲,并傳遞給CuckoosEgg類的around通知。
...
<!--CONFIG-->
<bean id="businesslogicbean"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces">
<value>IBusinessLogic</value>
</property>
<property name="target">
<ref local="beanTarget"/>
</property>
<property name="interceptorNames">
<list>
<value>theCuckoosEggAdvisor</value>
</list>
</property>
</bean>
<bean id="businesslogicbean2"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces">
<value>IBusinessLogic2</value>
</property>
<property name="target">
<ref local="beanTarget2"/>
</property>
<property name="interceptorNames">
<list>
<value>theCuckoosEgg2Advisor</value>
</list>
</property>
</bean>
<!--CLASS-->
<bean id="beanTarget" class="BusinessLogic"/>
<bean id="beanTarget2" class="BusinessLogic2"/>
<!--ADVISOR-->
<bean id="theCuckoosEggAdvisor"
class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<property name="advice">
<ref local="theReplacementFeaturePart1Advice"/>
</property>
<property name="pattern">
<value>IBusinessLogic.*</value>
</property>
</bean>
<bean id="theCuckoosEgg2Advisor"
class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<property name="advice">
<ref local="theReplacementFeaturePart2Advice"/>
</property>
<property name="pattern">
<value>IBusinessLogic2.bar*</value>
</property>
</bean>
<!--ADVICE-->
<bean id="theReplacementFeaturePart1Advice" class="CuckoosEgg"/>
<bean id="theReplacementFeaturePart2Advice" class="CuckoosEgg"/>
...
當使用修改后的springconfig.xml文件運行例子應用程序時,要替換的、被指定為功能部件的一部分的方法調用完全被截獲并傳遞給ReplacementFeature類。
通常,即使在同一個實現環境中,我們也可以用不同的方法來實現同一種設計模式。實現上例的另一種方法是實現兩個獨立的通知。
最后需要注意的是,使用Cuckoo's Egg設計模式置換的功能部件,不管它是跨越bean的還是在一個類中,它的生命周期與它所置換的功能部件的目標生命周期匹配。在上例中這沒什么問題,因為只有一個功能部件實例被置換了,而且唯一的Cuckoo's Egg通知只維護一個替代功能部件。
這個例子非常簡單,而在實踐中,您很可能必須處理大量需要用各自的Cuckoo's Egg實例置換的功能部件實例。在這種情況下,單個的方面實例需要被關聯到單個的要置換的功能部件實例。本系列的下一篇文章將會考慮方面生命周期的用法,屆時將解決這個問題。
結束語
本文介紹了如何在Spring框架內謹慎使用around形式的通知。around形式的通知常用于實現Cuckoo's Egg設計模式時,所以我們引入了一個例子來說明如何使用Spring AOP實現這種面向方面設計模式。
在本系列的第三部分中,您將看到如何使用Spring框架中其他的AOP基本概念。這些概念包括:控制方面生命周期、使用基于introduction通知的積極方面改變應用程序的靜態結構,以及使用control flow切入點實現對方面編織的更細微的控制。
參考資料
原文出處
An Introduction to Aspect-Oriented Programming with the Spring Framework, Part 2 http://www.onjava.com/pub/a/onjava/2004/10/20/springaop2.html
?作者簡介 |
| Russell Miles是General Dynamics UK公司的一名軟件工程師,他負責Java和分布式系統,但是他目前主要的興趣在面向方面領域,尤其是AspectJ。 |