在Java語言中,從織入切面的方式上來看,存在三種織入方式:編譯期織入、類加載期織入和運行期織入。編譯期織入是指在Java編譯期,采用特殊的編譯器,將切面織入到Java類中;而類加載期織入則指通過特殊的類加載器,在類字節碼加載到JVM時,織入切面;運行期織入則是采用CGLib工具或JDK動態代理進行切面的織入。
AspectJ采用編譯期織入和類加載期織入的方式織入切面,是語言級的AOP實現,提供了完備的AOP支持。它用AspectJ語言定義切面,在編譯期或類加載期將切面織入到Java類中。
在低版本的Spring中,你只能通過接口定義切面,在Spring 2.0中你可以通過AspectJ的切點表達式語法定義切點,Spring 2.0采用AspectJ的解析包解析切點織入切面。但這並不是我們這篇文章要講的內容。在這篇文章裡,我們希望從更高的層面上集成Spring和AspectJ,直接采用AspectJ織入切面,並讓Spring IoC容器管理切面實例。
Spring AOP提供了有限的AOP支持,在一般情況下,這些支持已經能夠滿足我們的開發要求,但如果對AOP有更高的要求(如實例化切面、屬性訪問切面等),則需要使用AspectJ的支持,而AspectJ又可以利用Spring IoC的依賴注入能力,兩者相得益彰,琴瑟合鳴。
如何使用AspectJ LTW
我們前面提到過,AspectJ提供了兩種切面織入方式,第一種通過特殊編譯器,在編譯期,將AspectJ語言編寫的切面類織入到Java類中,可以通過一個Ant或Maven任務來完成這個操作;第二種方式是類加載期織入,也簡稱為LTW(Load Time Weaving)。這裡,我們只介紹LTW的織入,編譯期織入請參看:http://www.eclipse.org/aspectj/doc/released/devguide/antTasks.html。
使用AspectJ LTW有兩個主要步驟,第一,通過JVM的-javaagent參數設置LTW的織入器類包,以代理JVM默認的類加載器;第二,LTW織入器需要一個aop.xml文件,在該文件中指定切面類和需要進行切面織入的目標類。下面,我們來了解一下具體的做法:
1.一般情況下,我們不會直接在DOS窗口中,通過Java命令啟動應用或進行測試。這就要求我們在IDE環境下,或應用部署的環境下,設置JVM的參數。我們以Eclipse和Tomcat為例,分別講述IDE和Web應用服務器中設置-javaapent JVM參數的方法。
在Eclipse下的設置
在Eclipse中,如果我們要改變JVM參數,可以在項目類導航樹中選中某個可運行類->右鍵單擊->Run As->Run...,可以在彈出的Run設置窗口設置該類的各項運行屬性,切換到Arguments Tab頁,在VM arguments中通過-javaagent指定AspectJ 織入器類包,如下圖所示:
這裡,我們設置為:-javaagent:D:\masterSpring\resources\aspectj-1.5.3\lib\aspectjweaver.jar
在Tomcat下的設置
打開<Tomcat_Home>\bin\catalina.bat,在該批處理文件頭部添加以下的設置:
set JAVA_OPTS=-javaagent:D:\masterSpring\resources\aspectj-1.5.3\lib\aspectjweaver.jar
這樣,Tomcat服務啟動時,JVM就會使用這個參數了。
2.配置LTW織入器的aop.xml織入配置文件
LTW織入器在工作時,首先會查找類路徑下META-INF /aop.xml的配置文件,並根據配置文件的設置進行織入的操作。下面是一個簡單的aop.xml文件:
<aspectj>
<aspects>
<aspect name="com.baobaotao.aspectj.TestAspectJ"/> ①切面類
</aspects>
<weaver>
<include within="com.baobaotao..*"/> ② 指定需要進行織入操作的目標類范圍
</weaver>
</aspectj>
在①中,通過<aspect>指定LTW織入器需要處理的切面類,這些切面類是用AspectJ語法編寫的。②處通過通配符指定需要進行織入操作的目標類。通過..*將需要處理的目標類限制在項目類包下是一個比較好的方法,否則織入器將對所有類進行操作,而這並不是我們期望的行為。
AspectJ織入切面結合Spring IoC容器管理切面實例
讓AspectJ為Java類提供切面織入服務,同時讓目標類和切面類享受Spring IoC依賴注入功能,這樣,兩者是緊密地集成在一起了。
首先,我們來看一下需要AspectJ進行切面織入的目標類:
package com.baobaotao;
public class Waitress ...{
private String name;
public void serveTo(String client) ...{
System.out.println(name + " serves to " + client+"...");
}
public String getName() ...{
return name;
}
public void setName(String name) ...{
this.name = name;
}
}
Waitress擁有一個name屬性和一個serveTo()方法。現在我們需要通過AspectJ為Waitress進行切面織入,以便在侍者提供服務之前強制使用禮貌用語:
package com.baobaotao;
public aspect TestAspectj ...{
private pointcut traceServeTo() :execution(* serveTo(..));①切點
before(): traceServeTo() ...{②前置增強
System.out.println(message);
}
private String message; ③禮貌用語
public void setMessage(String message)...{
this.message = message;
}
}
(注:為了能夠編寫AspectJ的切面,你首先需要從http://www.eclipse.org/aspectj/downloads.php下載AspectJ開發插件,以支持AspectJ語法。目前AspectJ分別為Eclipse、JBuilder、NetBeans、JDeveloper IDE.以及Emacs and JDEE提供了插件。)
TestAspectj切面類將對Waitress的serveTo()方法進行前置增強,在①處定義了切點,在②處定義了前置增強方法。此外,該切面類還擁有一個message屬性,用於提供規范的服務前禮貌用語,我們希望通過配置,在Spring IoC容器中注入該屬性。
在Spring配置文件中,我們可以按配置一般Bean相似的方式配置AspectJ切面類(TestApectj)和織入AspectJ的目標類(Waitress):
<bean id="aspectj" class="com.baobaotao.ThreadAspectj" factory-method="aspectOf">
<property name="message" value="How are you!"/>
</bean>
<bean id="waitress" class="com.baobaotao.Waitress">
<property name="name" value="Katty"/>
</bean>
注意,配置AspectJ切面類裡,需要指定factory-method="aspectOf"屬性,以便確保Spring從AspectJ獲取切面實例,而非自己創建該實例。
為了讓ThreadAspectj起作用,當然我們需要調整aop.xml的配置:
<aspectj>
<aspects>
<aspect name="com.baobaotao.ThreadAspectj "/>
</aspects>
<weaver options="-showWeaveInfo
-XmessageHandlerClass:org.springframework.aop.aspectj.AspectJWeaverMessageHandler">
<include within="com.baobaotao..*" />
</weaver>
</aspectj>
運行以下的測試代碼(同樣的,你需要為該類設置JVM javaagent參數):package com.baobaotao;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class WaitressAspectjTest ...{
public static void main(String[] args) ...{
String configPath = "com/baobaotao /beans.xml";
ApplicationContext ctx = new ClassPathXmlApplicationContext(configPath);
Waitress waitress = (Waitress)ctx.getBean("waitress");
waitress.serveTo("Johnson");
}
}
控制台輸出以下的信息:
①說明AspectJ切面織入到Waitress..serveTo()中,且禮貌用語從Spring IoC中注入
Katty serves to Johnson...
從輸出信息中,我們可以知道,Spring成功地管理了AspectJ的切面,AspectJ的切面類也成功地織入到目標類中。
讓Spring管理容器外的對象
Spring為管理容器外創建的對象提供了一個AspectJ語法編寫的切面類:
org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect,它位於spring-aspects.jar包中。spring-aspects.jar類包沒有隨Spring標准版一起發布,但你可以在完整版中找到它,位於Spring項目的dist目錄下。該切面類匹配所有標注@Configurable的類,該注解類org.springframework.beans.factory.annotation.Configurable則位於spring.jar中。
AspectJ在類加載時,將AnnotationBeanConfigurerAspect切面將織入到標注有@Configurable注解的類中。
AnnotationBeanConfigurerAspect將這些類和Spring IoC容器進行了關聯,AnnotationBeanConfigurerAspect本身實現了BeanFactoryAware的接口。
這樣,標注了@Configurable的類通過AspectJ LTW織入器織入AnnotationBeanConfigurerAspect切面後,就和Spring IoC容器間接關聯起來了,實現了Spring管理容器外對象的功能。
展現該功能的一個比較好的實例是管理Spring IoC容器外的領域對象。回想一下我們通常如何進行Dao類的單元測試:比如測試一個論壇主題ThreadDao。首先,我們需要在單元測試類中手工創建論壇主題Thread領域對象、帖子Topic領域對象、附件Attachment領域對象並設置好屬性值,然後手工設置這些領域對象的關聯關系。
對於習慣了使用Spring IoC依賴注入功能的開發者而言,可能更希望讓Spring IoC容器來做這樣工作——當然,原來我們就可以做這樣的工作,在Spring配置文件中配置好領域對象,然後通過ctx.getBean(beanName)獲取領域對象。但很多開發者可能並不喜歡這種方式。他們既希望以傳統的new Thread()方式創建領域對象,但又能夠享受Spring IoC所提供的依賴注入的好處。Spring管理容器外對象的功能讓我們擁有了這個能力。
下面,我們將通過一個實例展現這一神秘的功能。首先,來看一下我們希望管理的兩個領域對象:
package com.baobaotao.configure;
import java.io.Serializable;
import org.springframework.beans.factory.annotation.Configurable;
@Configurable ①
public class Thread implements Serializable...{
private String title;
private Topic topic;
//get/setter
public String toString()...{
return "title:"+title+";\ntopic:"+topic;
}
}
Thread是論壇主題的領域對象,一個論壇主題對應一個主帖,並擁有多個跟帖,為了簡單,這裡僅保留主貼對象topic,Topic領域對象類如下所示:package com.baobaotao.configure;
import java.io.Serializable;
import org.springframework.beans.factory.annotation.Configurable;
@Configurable ①
public class Topic implements Serializable...{
private String title;
private String content;
//get/setter
public String toString()...{
return "title:"+title+";content:"+content;
}
}
Thread和Topic在①處,都標注了@Configurable注解。僅僅標注了注解並沒有任何用途,我們需要利用AspectJ LTW在加載這些領域對象類時為標注@Configurable注解的類織入切面。
首先,我們得將匹配@Configurable注解類的切面類AnnotationBeanConfigurerAspect所在的spring-aspects.jar類包添加到類路徑上。spring-aspects.jar類包本身擁有一個aop.xml配置文件,其內容如下所示:<aspectj>
<aspects>
<aspect name="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect"/>
<aspect name="org.springframework.transaction.aspectj.AnnotationTransactionAspect"/>
</aspects>
</aspectj>
該配置文件會將AnnotationBeanConfigurerAspect和AnnotationTransactionAspect切面類應用到所有類中。
AnnotationTransactionAspect用於處理@Transaction注解,這裡我們沒有用到。由於,我們希望限制進行AspectJ切面織入目標類的范圍,所以我們需要再定義一個aop.xml文件:
<?xml version="1.0"?>
<aspectj>
<weaver options="-showWeaveInfo
-XmessageHandlerClass:org.springframework.aop.aspectj.AspectJWeaverMessageHandler">
<include within="com.baobaotao.configure..*" /> ① 使AspectJ織入器僅對該包下類進行操作
</weaver>
</aspectj>
通過<weaver>的options屬性的設置,指定在日志中顯示織入操作的信息,通過<include>元素指定需要進行AspectJ織入的目標類。可以簡單地將這個配置文件放到src/META-INF/目錄下。
前面,我們提到過切面類AnnotationBeanConfigurerAspect實現了BeanFactoryAware接口,所以需要在Spring配置文件中配置它,以便其可以感知Spring IoC容器,此外我們還需要配置Thread和Topic領域對象Bean,為標注了@Configurable的領域對象提供依賴注入的功能:<bean class="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect"
factory-method="aspectOf"/>①
<bean class="com.baobaotao.configure.Topic" scope="prototype"> ② 配置領域對象
<property name="title" value="測試帖子"/>
<property name="content" value="測試內容"/>
</bean>
<bean class="com.baobaotao.configure.Thread" scope="prototype"> ③ 配置領域對象
<property name="title" value="測試的主題"/>
<property name="topic" ref="com.baobaotao.configure.Topic"/>
</bean>
在①處我們聲明了一個AnnotationBeanConfigurerAspect Bean,並且定義了factory-method="aspectOf"屬性,確保Spring從AspectJ獲取切面實例,而不是嘗試自己去創建該實例。
Spring在aop命名空間中為配置AntationBeanConfigurerAspect提供了專門的配置元素:<aop:spring-configured/>,可以用這種簡潔的配置替代①處的配置。在②和③處,我們在Spring IoC中配置了領域對象Bean。
至此,一切已經就緒,我們可以編寫一個測試類測試Spring管理容器外對象的功能:
package com.baobaotao.configure;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class ConfigureAnnoAspectTest ...{
public static void main(String[] args) ...{
String configPath = "com/baobaotao/configure/beans.xml";
ApplicationContext ctx = new ClassPathXmlApplicationContext(configPath);
Thread thread = new Thread(); ① 象傳統一樣通過new構造領域對象
System.out.println(thread.toString()); ② 查看領域對象的信息
}
}
在①處使用傳統創建領域對象的方式構造一個Thread領域對象,在②處打印出該領域對象的信息。為ConfigureAnnoAspectTest類設置好JVM的javaagent參數,啟用AspectJ LTW織入器,設置完成後,運行該測試類,控制台將輸出以下的信息: …
① 以下兩行表示織入器注冊切面類
INFO [main] (AspectJWeaverMessageHandler.java:55)
- [AspectJ] register aspect
org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect
INFO [main] (AspectJWeaverMessageHandler.java:55)
- [AspectJ] register aspect
org.springframework.transaction.aspectj.AnnotationTransactionAspect
…
②以下幾行表示織入器對匹配目標類進行織入操作INFO [main] (AspectJWeaverMessageHandler.java:55)
- [AspectJ] weaving 'com/baobaotao/configure/Topic'
INFO [main] (AspectJWeaverMessageHandler.java:55) -
[AspectJ] Join point 'initialization(void com.baobaotao.configure.Topic.<init>())'
in Type 'com.baobaotao.configure.Topic' (Topic.java:8) advised by
afterReturning advice from
'org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect'
(AbstractBeanConfigurerAspect.aj:43)
INFO [main] (AspectJWeaverMessageHandler.java:55)
- [AspectJ] weaving 'com/baobaotao/configure/Thread'
INFO [main] (AspectJWeaverMessageHandler.java:55) -
[AspectJ] Join point 'initialization(void
com.baobaotao.configure.Thread.<init>())' in Type
'com.baobaotao.configure.Thread' (Thread.java:7) advised by
afterReturning advice from
'org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect'
(AbstractBeanConfigurerAspect.aj:43)'
③以下表示織入器略過不在目標范圍內的類
INFO [main] (AspectJWeaverMessageHandler.java:55) - [AspectJ] not weaving
'org/springframework/context/support/ClassPathXmlApplicationContext'
④領域對象的信息
title:測試的主題;
topic:title:測試帖子;content:測試內容
查看以上的信息,我們發現④處輸出的領域信息是我們在Spring IoC容器中配置的信息,可見我們通過new Thread()創建的領域對象,其實已經從Spring IoC容器中獲取到對應的Bean了。
這個過程參與的角色比較多,關系錯蹤復雜,我們有必須對這一過程重新進行梳理,找出角色間的關系和參與的操作,請看下圖:
AspectJ LTW織入器(aspectjweaver.jar)根據aop.xml中配置信息,在類加載期將切面類(AnnotationBeanConfigurerAspect)織入到標注@Configurable的類(Thread和Topic)中。
Spring IoC容器中配置了AnnotationBeanConfigurerAspect,使其可以感知Spring IoC容器,此外,Spring還為標注了@Configurable的類配置了對應的Bean。這樣,Thread和Topic通過new實例化對象時,其實是通過AnnotationBeanConfigurerAspect從容器中獲取實例。
在這一過程中,我們有兩個問題需要進一步說明:第一,AnnotationBeanConfigurerAspect是靜態的類,也即一個ClassLoader對應一個實例;第二,AnnotationBeanConfigurerAspect通過類反射機制獲取Thread和Topic的類全限定名:com.baobaotao.configure.Thread和com.baobaotao.configure.Topic,並用這個名稱到Spring IoC容器中獲取對應的Bean,因為如果配置時未指定Bean的名字,Spring使用類的全限定類作為Bean的名字。如果你希望采用命名的Bean,則需要在@Configurable中指定Bean的命名,如@Configurable(“thread”)。
小結
Spring 2.0對AOP進行了很大的改善,除了提供基於@ApsectJ和Schema的切面定義外,還允許集成AspectJ,即使用AspectJ切面織入功能,又可以通過Spring IoC管理切面類和目標類。所以,只要你願意,完全可以使用AspectJ進行切面定義,而使用Spring 2.0進行Bean的管理。究竟如何選擇,最好從實際項目的需要出發,以最Progaramtic的方式選擇其中最簡單最適合的方式。