用Spring 2.0和AspectJ簡化企業(yè)應用程序
作者 Adrian Colyer譯者 俞黎敏 發(fā)布于 2007年10月3日 上午4時10分
- Java
- 主題
- AOP
Spring:簡單而強大
Spring的目標是使企業(yè)應用程序開發(fā)盡可能地簡單和高效。這一理論的實例可以從Spring的JDBC、ORM、JMX、依賴注入等方法,以及企業(yè)應用程序開發(fā)的其他許多重要領域中見到。Spring還區(qū)分了使事情簡單化和過分單純化之間的差異。最不可思議的是同時提供了簡單化和強大的功能。企業(yè)應用程序中復雜性的一個根源來自影響應用程序多個部分的特性和需求的實現。相關于這些特性的代碼最終散布在應用程序代碼中,使得它更難以添加、維護和理解。Spring 2.0使得以模塊化的方式實現這些特性變得更加簡單,極大地簡化了整體的應用程序代碼,并且有時使得在實現沒有它的情況下十分痛苦的編碼需求變得易如反掌。
事務管理是影響應用程序多個部分的一個特性實例:一般來說所有的操作都在服務層。在Spring中解決這種需求的方式是通過使用AOP。Spring 2.0在它對AOP的支持中提供了一個明顯的簡化,同時還提供了比Spring 1.x所提供的更多富有表現力的功能。這些改善之處主要來自兩個主要的領域:通過使用XML schema極大地簡化了配置,以及與AspectJ的整合帶來了更好的富有表現力的功能和更簡單的advice模型。
在本文中,我將首先介紹在典型的企業(yè)應用程序中,Spring AOP和AspectJ適用于什么地方,之后介紹在2.0中新的Spring AOP支持。大部分篇幅用來講解企業(yè)應用程序中AOP的采用路線圖,通過大量可以只用AOP實現的特性實例,但是用任何其他的方法進行實現都將非常困難。
簡化企業(yè)應用程序
典型的企業(yè)應用程序——比如一個Web應用程序——由許多層構成。一個包含視圖和控制器的Web層,一個表現系統(tǒng)業(yè)務接口的服務層,一個負責保存和獲取持久化領域對象的數據訪問或者存儲層,與所有這些層共事的,還有一個核心業(yè)務邏輯所在的領域模型。
Web層、服務層和數據訪問層有著許多相同的重要特征:它們應該盡可能地瘦,它們不應該包含業(yè)務邏輯,并且它們一般通過Spring組裝在一起。在這些層中,Spring負責創(chuàng)建對象和配置。領域模型則有些不同:領域對象由程序員利用新的操作器創(chuàng)建(或者利用從數據庫中獲取的ORM工具進行擴建)。領域對象有許多唯一的實例,它們(可以)有豐富的行為。
服務層可以包含特定于應用程序用例的邏輯,但是所有領域相關的邏輯都應該放在領域模型本身里面。
服務層一般是使用聲明式企業(yè)服務(例如事務)的地方。聲明式的企業(yè)服務,例如事務和安全是影響應用程序中多個點的很好的需求實例。事實上,即使你想讓(比如)事務劃分只在單個地方,將這項功能與你的應用程序邏輯分開,使得代碼更加簡單,避免不必要的耦合,這也仍然很好。
由于服務對象是Spring管理的bean,Spring AOP天生適合于在這個層中處理需求。事實上,任何人在使用Spring的聲明式事務支持時,就已經是在使用Spring AOP了,無論他們是否意識到這一點。Spring AOP很成熟,得到了廣泛的應用。它非常適合于Web、服務和數據訪問層中受Spring管理的bean,只要你的需求可以通過advice bean方法執(zhí)行得到處理(且這些層的許多用例都屬于這一類)。
當提到影響你領域模型中多個點的需求時,你應用程序的最重要部分——Spring AOP——的幫助就小多了。你可以編程式地使用Spring AOP,但是這樣會很難使用,并且還要你自己負責創(chuàng)建代理和管理同一性。AspectJ天生適合于實現影響領域對象的特性。AspectJ方面不需要任何特殊的代理創(chuàng)建,并且可以很恰當地通知運行時在你的應用程序代碼中,或者通過你可能使用的框架所創(chuàng)建的對象。當你想要模塊化影響你應用程序的所有不同層的行為,或者模塊化性能以任何方式感知的行為時,AspectJ也是一種非常好的解決方案。
因此,我們最想要的是一種一致的Spring AOP和AspectJ方法,以便我們可以很容易地一起使用這兩種工具,以便如果需求發(fā)生變化,你用(比如)Spring AOP開發(fā)的能力就可以轉移到AspectJ上。無論我們正在使用哪種組合,我們仍然喜歡依賴注入和Spring所提供的配置的所有益處。Spring 2.0中新的AOP支持正好帶來了這一點。
底層的技術:AspectJ和Spring AOP簡介
AOP使得實現在應用程序中影響多個點的特性變得更加簡單。這主要因為AOP提供了對名為通知(advice)的這個東西的支持。通知不同于必須顯式調用的方法,每當發(fā)生匹配的觸發(fā)事件時,它就自動地執(zhí)行。繼續(xù)事務主題,觸發(fā)事件是服務層中一個方法的執(zhí)行,并且通知邏輯提供所需的事務劃分。用AOP的話來說,觸發(fā)事件被稱作連接點(join point),而切入點表達式(pointcut expression)則用來選擇通知要在那里運行的連接點。這個簡單的倒置意味著不用將調用散布到你全部應用程序代碼中的事務管理器,而是只要編寫一個切入點表達式,定義你需要事務管理器在什么地方完成某事的所有點,并將它與適當的通知關聯起來。AspectJ和Spring AOP提供對這個模型的支持,事實上,它們有著完全相同的切入點表達語言。
在接下來的討論中,注意Spring和AspectJ保持為獨立的工程,這很重要。Spring只使用反射和由AspectJ 5作為一個庫所暴露的工具API。Spring 2.0仍然是一個運行時基于代理的框架,且AspectJ織入器(weaver)不用于Spring方面。
我相信你們中大多數人都知道,AspectJ是一種包含完整編譯器的語言(構建為Eclipse JDT Java編譯器的一個擴展),對離線或者在運行時將(與)二進制的class文件(鏈接的方面)作為類織入的支持,被加載到了虛擬機中。AspectJ的最新發(fā)布版本是AspectJ 5,它為Java 5語言提供完整的支持。
AspectJ 5也引入了方面聲明的第二種風格,我們稱之為“@AspectJ”,它允許你將一個方面編寫為一個包含注解的Java類。這種方面可以通過一般的Java 5編譯器進行編譯。例如,傳統(tǒng)的“HelloWorld”方面在AspectJ編程語言中看起來像這樣:
public aspect HelloFromAspectJ {
pointcut mainMethod() : execution(* main(..));
after() returning : mainMethod() {
System.out.println("Hello from AspectJ!);
}
}
與傳統(tǒng)的HelloWorld類共同編譯這個方面,當你運行應用程序時,會看到這樣的輸出:
Hello World!
Hello from AspectJ!
我們可以用@Aspect風格編寫相同的方面如下:
@Aspect
public class HelloFromAspectJ {
@Pointcut("execution(* main(..))")
public void mainMethod() {}
@AfterReturning("mainMethod()")
public void sayHello() {
System.out.println("Hello from AspectJ!");
}
}
就本文而言,AspectJ 5中另一項重要的新特性是一個完全AspectJ感知的反射API(你可以在運行時為它的通知和切入點成員等等請求一個方面),和讓第三方使用AspectJ的切入點解析和匹配引擎的工具API。這些API的第一大用戶,就像你很快會見到的,是Spring AOP。
與AspectJ相反,Spring AOP是一個基于代理的運行時框架。在使用Spring AOP時,并沒有特殊的工具或者構建需求,因而Spring AOP是一種很容易開始的方法。作為一種基于代理的框架,它既有優(yōu)點也有缺點。除了已經提到過的容易使用的因素之外,基于代理的框架還能夠獨立地通知相同類型的不同實例。將這一點與AspectJ基于類型的語義相比,在這里,類型的每一個實例都有著相同的行為。對于像Spring這樣的框架而言,能夠獨立地通知獨立的對象(Spring beans)是一個重要的必要條件。另一方面,Spring AOP只支持AspectJ功能的一個子集:有可能在Spring beans中通知方法的執(zhí)行,但是其他沒什么。
基于代理的框架一般會有同一性的問題:有兩個對象(代理和目標)都表示應用程序中的同一個實體。必須始終小心地傳遞適當的引用,確保給實例化過的任何新的目標對象創(chuàng)建代理。Spring AOP通過管理bean實例化(以便代理可以被透明地創(chuàng)建)和通過依賴注入(以便Spring始終可以注入適當的引用),巧妙地解決了這些問題。
Spring 2.0中新的AOP支持
2.0中的Spring AOP可以完全向后與Spring 1.x應用程序和配置兼容。它還提供了比Spring 1.x更簡單且更強大的配置。新的AOP支持是基于schema的,因此在你的Spring beans配置文件中將需要相關的命名空間和schema定位屬性。它看起來像這樣:
<beans xmlns="http://www./schema/beans"
xmlns:xsi="http://www./2001/XMLSchema-instance"
xmlns:aop="http://www./schema/aop"
xsi:schemaLocation=
"http://www./schema/beans
http://www./schema/beans/spring-beans.xsd
http://www./schema/aop
http://www./schema/aop/spring-aop.xsd">
...
</beans>
與使用DTD時所需要的更簡單的xml配置相比,那么目前為止我們還沒有超越——但這是標準的xml配置,并且可以在你的IDE中的一個模板里創(chuàng)建,并且只在每當你需要創(chuàng)建一個Spring配置時才被重用。當我們開始將一些內容添加到配置中時,你會領略到這一好處。
Spring 2.0默認使用AspectJ 切入點語言(受執(zhí)行連接點種類的限制)。如果它看到一個AspectJ 切入點表達式,它就調出AspectJ對它進行解析和匹配。這意味著你用Spring AOP編寫的任何切入點表達式都將以與AspectJ完全相同的方式進行工作。此外,Spring實際上能理解@AspectJ方面,因此有可能共用Spring和AspectJ之間完整的方面定義。激活這項功能很容易,只要將<aop:aspectj-autoproxy>元素包括在你的配置中。如果AspectJ自動代理以這種方式激活,那么在你的應用程序上下文中定義的、包含@AspectJ方面的任何bean,都將被Spring AOP視為一個方面,并將相應地通知上下文中的bean。
下面是當你以這種方式使用Spring AOP時的Hello World程序。首先,應用程序上下文文件中bean元素的內容:
<bean id="helloService"
class="org.aspectprogrammer.hello.spring.HelloService"/>
<aop:aspectj-autoproxy/>
<bean id="helloFromAspectJ"
class="org.aspectprogrammer.hello.aspectj.HelloFromAspectJ"/>
HelloService是一個簡單的Java類:
public class HelloService {
public void main() {
System.out.println("Hello World!");
}
}
HelloFromAspectJ與你在本文前面見過的被注解的Java類(@AspectJ方面)完全相同。以下是啟動Spring容器的一個小主類,獲得一個對helloService bean的引用,并在它上面調用’main’方法:
public class SpringBoot {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext(
"org/aspectprogrammer/hello/spring/application-context.xml");
HelloService service = (HelloService) context.getBean("helloService");
service.main();
}
}
運行這個程序產生下面的輸出:
Hello World!
Hello from AspectJ!
記住,這仍然是Spring AOP(我們根本沒有在使用AspectJ編譯器或者織入器),但它是提供關于@AspectJ方面的反射信息和解析并匹配代表Spring的切入點的AspectJ。
Spring 2.0還支持用一個簡單的POJO支持的方面聲明的一種xml形式(不需要任何注解)。xml形式也使用相同的AspectJ 切入點語言子集,并支持相同的五種AspectJ 通知類型(前置通知(before advice)、后置通知(after returning advice)、異常通知(after throwing advice)、后通知(after [finally] advice)和 環(huán)繞通知(around advice))。
下面是使用一個基于XML的方面聲明的hello world應用程序:
<bean id="helloService"
class="org.aspectprogrammer.hello.spring.HelloService"/>
<aop:config>
<aop:aspect ref="helloFromSpringAOP">
<aop:pointcut id="mainMethod" expression="execution(* main(..))"/>
<aop:after-returning pointcut-ref="mainMethod" method="sayHello"/>
</aop:aspect>
</aop:config>
<bean id="helloFromSpringAOP"
class="org.aspectprogrammer.hello.spring.HelloAspect"/>
aop命名空間中的元素可以用來聲明方面、切入點和通知,有著與它們的AspectJ和@AspectJ等效物完全相同的語義。“aspect”元素引用Spring bean(完全由Spring配置和實例化),并且每個通知元素都在該bean中指定將被調用來執(zhí)行通知的方法。在這個例子中,HelloAspect類只是:
public class HelloAspect {
public void sayHello() {
System.out.println("Hello from Spring AOP!");
}
}
運行程序將產生熟悉的輸出:
Hello World!
Hello from Spring AOP!
如果你還沒有編寫過這樣的程序,就下載Spring 2.0,親自嘗試一下,這可是個好主意。
我不想把本文變成是關于Spring AOP的一個完全的教程,而是想要加緊看一些可以有效地以這種方式實現的特性實例。我將只是指出,傳遞Spring從使用AspectJ 切入點語言中獲得的其中某個東西,是編寫靜態(tài)類型的通知(聲明它們真正需要的那些參數的方法)的能力,與始終使用非類型的Object數組相反——這使得通知方法更容易編寫。
采用路線圖
理論說得夠多了……讓我們看一下你在企業(yè)應用程序中實際上如何以及為什么要使用AOP的一些例子。開始AOP,并不一定是一種肯定一切或者否定一切的爆炸性方法。采用可以分階段進行,每個階段都為增加的技術暴露回報以更多的益處。
建議的采用路線圖是只開始使用Spring提供的開箱即用的方面(例如事務管理)。許多Spring用戶將已經在這么做了,但多半不太欣賞AOP被“背地里”使用著。根據這一點,你可以實現在使用Spring AOP的Web、服務和數據訪問層中可能會有的任何定制橫切需求。
實現影響領域模型的特性必需使用AspectJ。你聽到這句話時可能感到驚訝:有大量的AspectJ方面對于你在開發(fā)時都非常有幫助,而且不影響在產品中以任何方式運行的應用程序。這些方面可以增加很多價值,并且采用風險非常小,因此建議用它們開始AspectJ。根據這一點,你可以選擇通過AspectJ實現“基礎結構的”需求——典型的實例為剖析(profiling)、跟蹤(tracing)、錯誤處理(error-handling)等等。隨著你越來越習慣于AspectJ和所配套的工具,最終你可以用方面在領域邏輯自身中開始實現功能。
關于AOP采用路線圖的其他信息,請見《Eclipse AspectJ》一書中的第11章,或者developerWorks AOP@Work系列中“Next steps with aspects”一文。這兩個資源都專門關注AspectJ,而我在這里則正在討論同時使用Spring和AspectJ。
讓我們依次看一下這每一種采用階段。
當在一個工程中使用AOP時,首先要做的最有意義的事是定義一組切入點表達式,描述你應用程序中的不同模塊或者層。這些切入點表達式在采用的所有不同階段中都將很有幫助,并且定義一次將減少重復,改善代碼的清晰度。如果我們用@AspectJ符號編寫這些切入點,它們就可以通過任何常規(guī)的Java 5編譯器進行編譯。利用一般的AspectJ語言關鍵字也可能編寫相同的東西,用ajc編譯源文件,并將生成的.class文件添加到classpath中。我將用@AspectJ作為開始Spring AOP的兩種方法中更為容易的那一種。許多讀者將會熟悉Spring所攜帶的“jpetstore”范例應用程序。我已經稍微重寫了這個應用程序,給它增加了一些方面(本文稍后會討論到)。以下是在pet store中捕捉主要層和模塊的“SystemArchitecture”方面的開頭部分:
@Aspect
public class SystemArchitecture {
/**
* we're in the pet store application if we're within any
* of the pet store packages
*/
@Pointcut("within(org.springframework.samples.jpetstore..*)")
public void inPetStore() {}
// modules
// ===========
@Pointcut("within(org.springframework.samples.jpetstore.dao..*)")
public void inDataAccessLayer() {}
@Pointcut("within(org.springframework.samples.jpetstore.domain.*)")
public void inDomainModel() {}
@Pointcut("within(org.springframework.samples.jpetstore.service..*)")
public void inServiceLayer() {}
@Pointcut("within(org.springframework.samples.jpetstore.web..*)")
public void inWebLayer() {}
@Pointcut("within(org.springframework.samples.jpetstore.remote..*)")
public void inRemotingLayer() {}
@Pointcut("within(org.springframework.samples.jpetstore.validation..*)")
public void inValidationModule() {}
// module operations
// ==================
@Pointcut("execution(* org.springframework.samples.jpetstore.dao.*.*(..))")
public void doaOperation() {}
@Pointcut("execution(* org.springframework.samples.jpetstore.service.*.*(..))")
public void businessService() {}
@Pointcut("execution(public * org.springframework.samples.jpetstore.validation.*.*(..))")
public void validation() {}
}
既然我們已經有了談論應用程序(“inServiceLayer”、“businessOperation”等等)的術語,讓我們用它來做一些有意義的事情吧。
使用開箱即用的Spring方面
advisor是Spring 1.x遺留下來的一個Spring概念,它包含了一個非常小的方面,帶有單獨的一條通知,和關聯的切入點表達式。對于事務劃分而言,advisor就是我們所需要的一切。典型的事務需求為:服務層中的所有操作都要利用(幾個)底層資源管理器的默認隔離級別在一個事務(REQUIRED語義)中執(zhí)行。此外,一些操作可以被標識為“只讀”事務——這一知識可以給這類事務帶來明顯的性能改善。jpestore advisor聲明如下:
<!--
all aspect and advisor declarations are gathered inside an
aop:config element
-->
<aop:config>
<aop:advisor
pointcut="org.springframework.samples.jpetstore.SystemArchitecture.businessService()"
advice-ref="txAdvice"/>
</aop:config>
這個聲明僅僅意味著:當執(zhí)行一個“businessService”時,我們需要運行被“txAdvice”引用的通知。“BusinessService”切入點在我們前面討論過的org.springframework.samples.jpetstore.SystemArchitecture方面中定義。它與在服務接口中定義的任何操作的執(zhí)行相匹配。由于事務通知本身可能需要相當多的配置,因此Spring在tx命名空間中提供了tx:advice元素,使得這項工作變得更加簡單和清晰。這就是給jpetstore應用程序的“txAdvice”定義:
<!--
Transaction advice definition, based on method name patterns.
Defaults to PROPAGATION_REQUIRED for all methods whose name starts with
"insert" or "update", and to PROPAGATION_REQUIRED with read-only hint
for all other methods.
-->
<tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="insert*"/>
<tx:method name="update*"/>
<tx:method name="*" read-only="true"/>
</tx:attributes>
</tx:advice>
還有一種更加簡單的方法來配置使用注解的事務。在使用@Transactional注解時,你唯一需要的XML是:
<!--
Tell Spring to apply transaction advice based on the presence of
the @Transactional annotation on Spring bean types.
-->
<tx:annotation-driven/>
使用注解方法時,PetService實現要做如下注解:
/*
* all operations have TX_REQUIRED, default isolation level,
* read-write transaction semantics by default
*/
@Transactional
public class PetStoreImpl implements PetStoreFacade, OrderService {
...
/**
* override defaults on a per-method basis
*/
@Transactional(readOnly=true)
public Account getAccount(String username) {
return this.accountDao.getAccount(username);
}
...
}
簡化Web、服務和數據訪問層
Spring AOP可以用來簡化Web、服務和數據訪問層。在本節(jié)中,我們要看兩個實例:一個取自數據訪問層,一個取自服務層。
假設你已經用Hibernate 3而不是用Spring HibernateTemplate支持類實現了你的數據訪問層。你現在準備開始在應用程序中使用Spring,想要在服務層中利用Spring的細粒度DataAccessException層次結構。Spring的HibernateTemplate將自動為你把HibernateExceptions轉換成DataAccessExceptions,但是由于現階段你已經有一個非常滿意的數據層實現,因此并不想馬上用Spring支持類對它進行重寫。這意味著你需要自己實現異常轉換。這個需求聲明起來很簡單:
從數據訪問層中拋出任何HibernateException之后,在將它遞給調用者之前將它轉換成一個DataAccessException。
利用AOP,實現幾乎與需求聲明一樣簡單。沒有AOP時實現這個需求是件非常令人頭痛的事。這就是“myapp”的HibernateExceptionTranslator方面:
@Aspect
public class HibernateExceptionTranslator {
private HibernateTemplate hibernateTemplate;
public void setHibernateTemplate(HibernateTemplate aTemplate) {
this.hibernateTemplate = aTemplate;
}
@AfterThrowing(
throwing="hibernateEx",
pointcut="org.aspectprogrammer.myapp.SystemArchitecture.dataAccessOperation()"
)
public void rethrowAsDataAccessException(HibernateException hibernateEx) {
throw this.hibernateTemplate
.convertHibernateAccessException(hibernateEx);
}
}
方面需要一個HibernateTemplate,以便執(zhí)行轉換——我們要用依賴注入對它進行配置,就像任何其他的Spring bean一樣。通知聲明應該有望非常容易地理解為需求聲明的一個直接轉換:“@AfterThrowing從dataAccessOperation()操作中拋出一個HibernateException (hibernateEx) ,并重新拋出 rethrowAsDataAccessException”。簡單而有力!
我們現在可以用ajc(AspectJ編譯器)構建應用程序,這樣我們就完事了。但是這里不需要使用ajc,因為Spring AOP也能識別@AspectJ方面。
在應用程序上下文文件中,我們需要兩個配置。首先我們要告訴Spring,包含@AspectJ方面的類型的任何bean都應該用來配置Spring AOP代理。這是通過在應用程序上下文配置文件中的任何位置聲明下列元素來實現的一個一次性配置:
<aop:aspectj-autoproxy>
然后我們需要聲明異常轉換bean,并對它進行配置,就像對待任何一般的Spring bean一樣(這里并沒有任何特定于AOP的東西):
<bean id="hibernateExceptionTranslator"
class="org.aspectprogrammer.myapp.dao.hibernate.HibernateExceptionTranslator">
<property name="hibernateTemplate">
<bean class="org.springframework.orm.hibernate3.HibernateTemplate">
<constructor-arg index="0" ref="sessionFactory" />
</bean>
</property>
</bean>
僅僅因為bean的類(HibernateExceptionTranslator)是一個@AspectJ方面,就足以配置Spring AOP了。
為了完整起見,我們也看一下如何用方面聲明的xml形式來完成這項工作(例如對于在JDK 1.4下進行工作的)。hibernateExceptionTranslator的bean定義與上面所述的一樣。類本身不再被注解,但是它剩下的部分也完全相同:
public class HibernateExceptionTranslator {
private HibernateTemplate hibernateTemplate;
public void setHibernateTemplate(HibernateTemplate aTemplate) {
this.hibernateTemplate = aTemplate;
}
public void rethrowAsDataAccessException(HibernateException hibernateEx) {
throw this.hibernateTemplate
.convertHibernateAccessException(hibernateEx);
}
}
由于這不再是一個@AspectJ方面,我們無法使用aspectj-autoproxy元素,而是用XML定義該方面:
<aop:config>
<aop:aspect ref="hibernateExceptionTranslator">
<aop:after-throwing
throwing="hibernateEx"
pointcut="org.aspectprogrammer.myapp.SystemArchitecture.dataAccessOperation()"
method="rethrowAsDataAccessException"/>
</aop:aspect>
</aop:config>
這看起來與前一個版本一樣:after-throwing 從dataAccessOperation操作中拋出hibernateEx,并且重新拋出rethrowAsDataAccessException。注意aop:aspect元素的“ref”屬性,它引用了我們前面定義的hibernateExceptionTranslator bean。這是rethrowAsDataAccessException方法將要在那里被調用的bean實例,而hibernateEx則是在該方法中聲明的參數名(這個例子中的唯一參數)。就是這樣。我們已經實現了需求(兩次?。@聾AspectJ風格,我們有15個非空的代碼行,和一行XML。這足以為我們在整個數據訪問層中提供一致、正確的行為,但是它可能很大。
這個特殊方面的一大好處在于,如果你以后想要將數據層移植到一個基于利用Hibernate的實現、或者任何其他JPA實現的JPA(EJB 3持久化),你的服務層將不會受到影響,并且可以繼續(xù)使用DataAccessExceptions(Spring將為JPA提供模板和異常轉換,就像對其他的ORM實現所做的一樣)。
既然我們可以在服務層中使用細粒度的DataAccessExceptions了,就可以利用這一點做些有意義的事情。讓我們在將失敗傳遞給客戶端之前,實現由于并發(fā)失敗而失敗的任何等冪服務操作都將被透明地重試可設定次數的橫切需求。
以下是完成這項工作的一個方面:
@Aspect
public class ConcurrentOperationExecutor implements Ordered {
private static final int DEFAULT_MAX_RETRIES = 2;
private int maxRetries = DEFAULT_MAX_RETRIES;
private int order = 1;
private boolean retryOnOptimisticLockFailure = false;
/**
* configurable number of retries
*/
public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}
/**
* Whether or not optimistic lock failures should also be retried.
* Default is not to retry transactions that fail due to optimistic
* locking in case we overwrite another user's work.
*/
public void setRetryOnOptimisticLockFailure(boolean retry) {
this.retryOnOptimisticLockFailure = retry;
}
/**
* implementing the Ordered interface enables us to specify when
* this aspect should run with respect to other aspects such as
* transaction management. We give it the highest precedence
* (1) which means that the retry logic will wrap the transaction
* logic - we need a fresh transaction each time.
*/
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
/**
* For now, just assume that all business services are idempotent
*/
@Pointcut("org.aspectprogrammer.myapp.SystemArchitecture.businessService()")
public void idempotentOperation() {}
@Around("idempotentOperation()")
public Object doConcurrentOperation(ProceedingJoinPoint pjp)
throws Throwable {
int numAttempts = 0;
ConcurrencyFailureException failureException;
do {
try {
return pjp.proceed();
}
catch(OptimisticLockingFailureException ex) {
if (!this.retryOnOptimisticLockFailure) {
throw ex;
}
else {
failureException = ex;
}
}
catch(ConcurrencyFailureException ex) {
failureException = ex;
}
}
while(numAttempts++ < this.maxRetries);
throw lockFailureException;
}
}
這個方面還是可以被Spring AOP或者AspectJ使用,這一點不變。around advice (doConcurrentOperation)采用了類型ProceedingJoinPoint的一個特殊參數。當proceed在這個對象中被調用時,無論“around”什么樣的通知(在這個例子中為服務操作)都將執(zhí)行。如果你去掉注釋和樣板getters-and-setters,這個方面的業(yè)務端仍然只有32行代碼。由于我們在配置文件中已經有aspectj-autoproxy元素,我們需要增加的就只是一個簡單的bean定義了:
<bean id="concurrentOperationExecutor"
class="org.aspectprogrammer.myapp.service.impl.ConcurrentOperationExecutor">
<property name="maxRetries" value="3"/>
<property name="order" value="1"/>
</bean>
如果服務層中并非所有的操作都是等冪的,該怎么辦?我們如何判斷等冪的操作呢?這就是切入點語言的威力開始顯現之處。我們已經有一個表示等冪操作的概念的抽象:
@Pointcut("org.aspectprogrammer.myapp.SystemArchitecture.businessService()")
public void idempotentOperation() {}
如果我們想要改變構成表示等冪操作的東西,我們所要做的就是改變切入點。例如,我們可以給等冪操作定義一個標識注解:@Idempotent。我們可以非常簡單地將切入點表達式改為只與包含Idempotent注解的業(yè)務服務相匹配:
@Pointcut(
"org.aspectprogrammer.myapp.SystemArchitecture.businessService() &&
@annotation(org.aspectprogrammer.myapp.Idempotent)")
public void idempotentOperation() {}
現在比使用APT簡單一些了!切入點只說:“idempotentOperation是有著Idempotent 注解的businessService”。
希望你的大多數服務操作都是等冪的。在這種情況下,注解非等冪的操作就可能比挑出等冪操作要容易得多。像@IrreversibleSideEffects這樣的東西應該會成功。這在技術上和心理上都說得過去(指想要用IrreversibleSideEffects對他們的代碼進行注解的人!我寧愿重寫代碼而避免使用它們;)。由于idempotentOperation的定義只有一處,很容易改變:
@Pointcut(
"org.aspectprogrammer.myapp.SystemArchitecture.businessService() &&
!@annotation(org.aspectprogrammer.myapp.IrreversibleSideEffects)")
public void idempotentOperation() {}
idempotentOperation是一個沒有IrreversibleSideEffects注解的businessService。
用開發(fā)時間方面提升生產力
一旦你習慣了給Spring AOP編寫@AspectJ方面,就會從AspectJ中獲得額外的益處,即使你只在開發(fā)期間使用它(并且在你正在運行的應用程序中沒有AspectJ編譯的方面)。方面可以用來針對測試(它們使得某些模擬和錯誤注入變得更加容易)、調試和診斷問題,以及確保為你的應用程序所設計的設計指導方針得到實施。首先,讓我們看一個設計實施方面(enforcement aspects)的實例。繼續(xù)在數據訪問層中進行,我們現在要引入Spring HibernateTemplate,讓Spring替我們管理Hibernate會話,而不用我們自己管理。以下這個方面將確保程序員不會忘記開始管理他們自己的會話:
public aspect SpringHibernateUsageGuidelines {
pointcut sessionCreation()
: call(* SessionFactory.openSession(..));
pointcut sessionOrFactoryClose()
: call(* SessionFactory.close(..)) ||
call(* Session.close(..));
declare error
: sessionCreation() || sessionOrFactoryClose()
: "Spring manages Hibernate sessions for you, " +
"do not try to do it programmatically";
}
有了這個方面之后,如果一位程序員在給Eclipse使用AspectJ Development Tools(AJDT)插件,他或者她就將在問題視圖中看到一個編譯錯誤的標識,并在源代碼中出錯的位置(與任何一般的編譯錯誤完全一樣)會有錯誤文本:“Spring替你管理Hibernate會話,請不要試圖編程式地進行管理”(Spring manages Hibernate sessions for you, do not try to do it programmatically)。建議引入像這樣的實施方面的方法是,將AspectJ編譯步驟增加到用實施方面“織入”應用程序的構建過程——如果被方面發(fā)現構建錯誤,這項任務將會失敗。
現在讓我們看一下簡單的診斷方面(diagnosis aspect)?;仡櫼幌挛覀冊鴮⒁恍┦聞諛俗R為只讀(一項很重要的性能優(yōu)化)。隨著應用程序復雜性的增加,從概念上來說,從事務劃分所發(fā)生的服務層操作的位置,到作為指定用例的一部分而執(zhí)行的業(yè)務領域邏輯,這之間可能十分遙遠。如果在一個只讀的事務期間,領域邏輯更新了一個領域對象的狀態(tài),我們就會有丟失更新的風險(從來沒有提交到數據庫)。這可能成為那些莫名其妙bug的根源。
LostUpdateDetector方面可以在開發(fā)時間用來偵測可能的丟失更新。
public aspect LostUpdateDetector {
private Log log = LogFactory.getLog(LostUpdateDetector.class);
pointcut readOnlyTransaction(Transactional txAnn) :
SystemArchitecture.businessService() &&
@annotation(txAnn) && if(txAnn.readOnly());
pointcut domainObjectStateChange() :
set(!transient * *) &&
SystemArchitecture.inDomainModel();
..
我已經通過在方面中定義兩個有用的切入點開始了。readOnlyTransaction是有著@Transactional注解的businessService()的執(zhí)行,readOnly()屬性設置為true。domainObjectStateChange是任何非瞬時領域inDomainModel()的更新。(注意,這是進行了簡化,但是對于組成一個領域對象狀態(tài)變化的東西仍然很有用——我們可以將該方面擴展為處理集合等等,如果我們希望如此的話)。利用所定義的這兩個概念,我們現在就可以通過potentialLostUpdate()表達想說的話了:
pointcut potentialLostUpdate() :
domainObjectStateChange() &&
cflow(readOnlyTransaction(Transactional));
potentialLostUpdate是在一個readOnlyTransaction(期間)的控制流中所做的一個domainObjectState變化。你從這里可以領略到切入點語言生效的威力。通過組成兩個具名的切入點表達式,我們已經能夠非常簡單地表達一個很強大的概念。與你只有一個粗糙的攔截模型可用時相比,利用切入點語言更容易表達像potentialLostUpdate這樣的條件。它也比像EJB 3所提供的那些過于單純的攔截機制要強大得多。
最后,當發(fā)生potentialLostUpdate時,我們當然需要真正地做一些事情:
after() returning : potentialLostUpdate() {
logLostUpdate(thisJoinPoint);
}
private void logLostUpdate(JoinPoint jp) {
String fieldName = jp.getSignature().getName();
String domainType = jp.getSignature().getDeclaringTypeName();
String newValue = jp.getArgs()[0].toString();
Throwable t = new Throwable("potential lost update");
t.fillInStackTrace();
log.warn("Field [" + fieldName + "] in type [" + domainType + "] " +
"was updated to value [" + newValue + "] in a read-only " +
"transaction, update will be lost.",t);
}
}
以下是有了這個方面之后,運行一個測試案例所得到的日志信息:
WARN - LostUpdateDetector.logLostUpdate(41) | Field [name] in type
[org.aspectprogrammer.myapp.domain.Pet] was updated to value [Mr.D.]
in a read-only transaction, update will be lost.
java.lang.Throwable: potential lost update
at org.aspectprogrammer.myapp.debug.LostUpdateDetector.logLostUpdate(LostUpdateDetector.aj:40)
at org.aspectprogrammer.myapp.debug.LostUpdateDetector.afterReturning(LostUpdateDetector.aj:32)
at org.aspectprogrammer.myapp.domain.Pet.setName(Pet.java:32)
at org.aspectprogrammer.myapp.service.impl.PetServiceImpl.updateName(PetServiceImpl.java:40)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:100)
at org.aspectprogrammer.myapp.service.impl.ConcurrentOperationExecutor.doConcurrentOperation(ConcurrentOperationExecutor.java:37)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:478)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:344)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:196)
作為題外話,解釋一下干凈且易讀的堆棧軌跡(和適當的樂觀重試邏輯)。易讀的堆棧軌跡(stack trace)是由于從異常堆棧軌跡項中去除了干擾的另一個方面。沒有適當的堆棧軌跡管理方面,所有的Spring AOP攔截堆??蛞捕急伙@示出來,出現了像下面所示這樣的堆棧軌跡。我想,你會認同說簡化版是一個很大的改進!
WARN - LostUpdateDetector.logLostUpdate(41) | Field [name] in type
[org.aspectprogrammer.myapp.domain.Pet] was updated to value [Mr.D.]
in a read-only transaction, update will be lost.
java.lang.Throwable: potential lost update
at org.aspectprogrammer.myapp.debug.LostUpdateDetector.logLostUpdate(LostUpdateDetector.aj:40)
at org.aspectprogrammer.myapp.debug.LostUpdateDetector.ajc$afterReturning$org_aspectprogrammer_myapp_debug_LostUpdateDetector$1$b5d4ce0c(LostUpdateDetector.aj:32)
at org.aspectprogrammer.myapp.domain.Pet.setName(Pet.java:32)
at org.aspectprogrammer.myapp.service.impl.PetServiceImpl.updateName(PetServiceImpl.java:40)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:287)
at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:181)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:148)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:100)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:170)
at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:71)
at org.aspectprogrammer.myapp.service.impl.ConcurrentOperationExecutor.doConcurrentOperation(ConcurrentOperationExecutor.java:37)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:568)
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:558)
at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:57)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:170)
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:95)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:170)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:176)
at $Proxy8.updateName(Unknown Source)
at org.aspectprogrammer.myapp.debug.LostUpdateDetectorTests.testLostUpdateInReadOnly(LostUpdateDetectorTests.java:23)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at junit.framework.TestCase.runTest(TestCase.java:154)
at junit.framework.TestCase.runBare(TestCase.java:127)
at junit.framework.TestResult$1.protect(TestResult.java:106)
at junit.framework.TestResult.runProtected(TestResult.java:124)
at junit.framework.TestResult.run(TestResult.java:109)
at junit.framework.TestCase.run(TestCase.java:118)
at junit.framework.TestSuite.runTest(TestSuite.java:208)
at junit.framework.TestSuite.run(TestSuite.java:203)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:478)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:344)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:196)
簡化“基礎結構”需求的實現
當你越來越習慣于AspectJ和所配套的工具組時,你就可以用AspectJ來實現影響你應用程序所有部分的需求,包括領域模型。作為一個簡單的實例,我將向你介紹如何剖析jpetstore范例的應用程序。讓我們首先看一下Profiler方面,然后填入一些外圍的細節(jié):
public aspect Profiler {
private ProfilingStrategy profiler = new NoProfilingStrategy();
public void setProfilingStrategy(ProfilingStrategy p) {
this.profiler = p;
}
pointcut profiledOperation() :
Pointcuts.anyPublicOperation() &&
SystemArchitecture.inPetStore() &&
!within(ProfilingStrategy+);
Object around() : profiledOperation() {
Object token = this.profiler.start(thisJoinPointStaticPart);
Object ret = proceed();
this.profiler.stop(token,thisJoinPointStaticPart);
return ret;
}
}
我們已經將profiledOperation()定義為[the]PetStore()中的anyPublicOperation()了。該方面表現得就像委托給ProfilingStrategy的控制器,我們將利用依賴注入通過Spring對它進行配置。
<bean id="profiler"
class="org.springframework.samples.jpetstore.profiling.Profiler"
factory-method="aspectOf">
<property name="profilingStrategy">
<ref local="jamonProfilingStrategy"/>
</property>
</bean>
<bean id="jamonProfilingStrategy"
class="org.springframework.samples.jpetstore.profiling.JamonProfilingStrategy"
init-method="reset"
destroy-method="report">
</bean>
注意給方面bean使用了“factory-method”屬性,這是配置單例(singleton)AspectJ方面和配置一般的Spring bean之間的唯一區(qū)別。我正在用JAMon進行剖析,它提供了一個非常簡單的API。
public class JamonProfilingStrategy implements ProfilingStrategy {
public Object start(StaticPart jpStaticPart) {
return MonitorFactory.start(jpStaticPart.toShortString());
}
public void stop(Object token, StaticPart jpStaticPart) {
if (token instanceof Monitor) {
Monitor mon = (Monitor) token;
mon.stop();
}
}
}
這就是我們激活適用于整個pet store的剖析所必須做的全部工作。通過將JAMon提供的jsp增加到pet store應用程序,我們就可以在Web瀏覽器中觀看到剖析的輸出。以下是我在應用程序周圍點擊一會之后的屏幕快照:

簡化領域模型
具有影響你領域模型的多個部分的業(yè)務邏輯需求,這也并不罕見。有些明顯的實例為:設計模式實現(請見Nick Leseicki關于這個主題的精彩的developerWorks文章 :part 1、part 2),領域對象的依賴注入(例如使用Spring的@Configurable注解),以及業(yè)務規(guī)則和策略的實現。在采用的這個階段,你的核心業(yè)務邏輯變成了依賴于方面的存在。
你編寫的方面將特定于你的領域。AspectJ和AJDT都利用AspectJ構建,我們在它們的構建中使用了大量特定于領域的方面。舉個例子,下面是我在1.5.1發(fā)布的開發(fā)期間增加到AspectJ的一個方面:它實現了一項經常被請求的特性,當一個異常被一個空的捕捉塊淹沒時,用它來發(fā)布一個警告。
public aspect WarnOnSwallowedException {
pointcut resolvingATryStatement(TryStatement tryStatement, BlockScope inScope)
: execution(* TryStatement.resolve(..)) &&
this(tryStatement) &&
args(inScope,..);
after(TryStatement tryStatement, BlockScope inScope) returning
: resolvingATryStatement(tryStatement,inScope) {
if (tryStatement.catchBlocks != null) {
for (int i = 0; i < tryStatement.catchBlocks.length; i++) {
Block catchBlock = tryStatement.catchBlocks[i];
if (catchBlock.isEmptyBlock() ||
catchBlock.statements.length == 0) {
warnOnEmptyCatchBlock(catchBlock,inScope);
}
}
}
}
private void warnOnEmptyCatchBlock(Block catchBlock, BlockScope inScope) {
inScope.problemReporter()
.swallowedException(catchBlock.sourceStart(),
catchBlock.sourceEnd());
}
}
即使在這個實例中,這個方面只在代碼庫中建議了一個位置,但它除了JDT編譯器的功能之外,還通過將這個AspectJ模塊化,使得代碼更加清楚了,也使得未來的維護人員非常清楚如何實現這項特性。涉及利用方面給領域建模的進一步詳情,則是另一篇文章的主題了。
小結
Spring的目標是提供一種簡單而強大的企業(yè)應用程序開發(fā)方法。利用它對AOP的支持,以及與AspectJ的整合,這種方法延伸到了影響應用程序多個部分的特性的實現。傳統(tǒng)上而言,這些特性的實現都散布到整個應用程序邏輯中,使得它難以添加、去除和維護特性,并且使得應用程序邏輯復雜化。利用方面,Spring讓你能夠給這些特性編寫整潔、簡單且模塊化的實現。AOP的采用可以分多個階段進行:通過利用Spring提供的開箱即用的方面開始,然后可以利用Spring AOP在Web、服務和數據訪問層中添加你自己的@AspectJ方面。AspectJ本身可以被用來提供開發(fā)生產力,而不用在AspectJ中引入任何依賴。更進一步探討了橫貫你應用程序多個層的基礎結構需求,可以利用AspectJ方面被簡單地實現。最后,你可以用方面來簡化你領域模型本身的實現。
關于作者
Adrian Colyer是Interface21的首席科學家,是Eclipse.org的AspectJ項目負責人,以及AspectJ Development Tools(AJDT)項目的創(chuàng)辦人。2004年,他被MIT Technology Review投票選為世界前100名年輕的改革者之一,并且經常進行關于Spring、AOP和AspectJ主題的演講。
關于Interface21
Interface21提供Spring、AOP和AspectJ方面的培訓和咨詢。至于課程安排或者要安排培訓的,請見www.。
Adrian Colyer和Spring社區(qū)其他成員出席2006年12月7至10日會議的相關內容請見http://www.。