原理
AOP(Aspect Oriented Programming),也就是面向方面編程的技術。AOP基於IoC基礎,是對OOP的有益補充。
AOP將應用系統分爲兩部分,核心業務邏輯(Core businessconcerns)及橫向的通用邏輯,也就是所謂的方面Crosscutting enterprise concerns,例如,所有大中型應用都要涉及到的持久化管理(Persistent)、事務管理(TransactionManagement)、安全管理(Security)、日誌管理(Logging)和調試管理(Debugging)等。
AOP正在成爲軟件開發的下一個光環。使用AOP,你可以將處理aspect的代碼注入主程序,通常主程序的主要目的並不在於處理這些aspect。AOP可以防止代碼混亂。
Springframework是很有前途的AOP技術。作爲一種非侵略性的、輕型的AOP framework,你無需使用預編譯器或其他的元標籤,便可以在Java程序中使用它。這意味着開發團隊裏只需一人要對付AOP framework,其他人還是像往常一樣編程。
AOP概念
讓我們從定義一些重要的AOP概念開始。
— 方面(Aspect):一個關注點的模塊化,這個關注點實現可能另外橫切多個對象。事務管理是J2EE應用中一個很好的橫切關注點例子。方面用Spring的Advisor或攔截器實現。
— 連接點(Joinpoint):程序執行過程中明確的點,如方法的調用或特定的異常被拋出。
— 通知(Advice):在特定的連接點,AOP框架執行的動作。各種類型的通知包括“around”、“before”和“throws”通知。通知類型將在下面討論。許多AOP框架包括Spring都是以攔截器做通知模型,維護一個“圍繞”連接點的攔截器鏈。
— 切入點(Pointcut):指定一個通知將被引發的一系列連接點的集合。AOP框架必須允許開發者指定切入點,例如,使用正則表達式。
— 引入(Introduction):添加方法或字段到被通知的類。Spring允許引入新的接口到任何被通知的對象。例如,你可以使用一個引入使任何對象實現IsModified接口,來簡化緩存。
— 目標對象(Target Object):包含連接點的對象,也被稱作被通知或被代理對象。
— AOP代理(AOP Proxy):AOP框架創建的對象,包含通知。在Spring中,AOP代理可以是JDK動態代理或CGLIB代理。
— 編織(Weaving):組裝方面來創建一個被通知對象。這可以在編譯時完成(例如使用AspectJ編譯器),也可以在運行時完成。Spring和其他純Java AOP框架一樣,在運行時完成織入。
各種通知類型包括:
— Around通知:包圍一個連接點的通知,如方法調用。這是最強大的通知。Aroud通知在方法調用前後完成自定義的行爲,它們負責選擇繼續執行連接點或通過返回它們自己的返回值或拋出異常來短路執行。
— Before通知:在一個連接點之前執行的通知,但這個通知不能阻止連接點前的執行(除非它拋出一個異常)。
— Throws通知:在方法拋出異常時執行的通知。Spring提供強制類型的Throws通知,因此你可以書寫代碼捕獲感興趣的異常(和它的子類),不需要從Throwable或Exception強制類型轉換。
— After returning通知:在連接點正常完成後執行的通知,例如,一個方法正常返回,沒有拋出異常。
Around通知是最通用的通知類型。大部分基於攔截的AOP框架(如Nanning和Jboss 4)只提供Around通知。
如同AspectJ,Spring提供所有類型的通知,我們推薦你使用最爲合適的通知類型來實現需要的行爲。例如,如果只是需要用一個方法的返回值來更新緩存,你最好實現一個after returning通知,而不是around通知,雖然around通知也能完成同樣的事情。使用最合適的通知類型使編程模型變得簡單,並能減少潛在錯誤。例如,你不需要調用在around通知中所需使用的MethodInvocation的proceed()方法,因此就調用失敗。
切入點的概念是AOP的關鍵,它使AOP區別於其他使用攔截的技術。切入點使通知獨立於OO的層次選定目標。例如,提供聲明式事務管理的around通知可以被應用到跨越多個對象的一組方法上。 因此切入點構成了AOP的結構要素。
攔截器(也稱攔截機)
攔截機 (Interceptor), 是 AOP (Aspect-Oriented Programming) 的另一種叫法。AOP本身是一門語言,只不過我們使用的是基於JAVA的集成到Spring 中的 SpringAOP。同樣,我們將通過我們的例子來理解陌生的概念。
接口類
Java代碼
package com.test.TestSpring3;
public interface UserService // 被攔截的接口
...{
public void printUser(String user);
}
實現類
Java代碼
package com.test.TestSpring3;
public class UserServiceImp implements UserService// 實現UserService接口
...{
public void printUser(String user) ...{
System.out.println("printUser user:" + user);// 顯示user
}
}
AOP攔截器
Java代碼
package com.test.TestSpring3;
import org.aopalliance.intercept.MethodInterceptor;
importorg.aopalliance.intercept.MethodInvocation;
public class UserInterceptor implementsMethodInterceptor
// AOP方法攔截器
...{
public Object invoke(MethodInvocation arg0) throws Throwable ...{
try ...{
if(arg0.getMethod().getName().equals("printUser"))
// 攔截方法是否是UserService接口的printUser方法
...{
Object[] args =arg0.getArguments();// 被攔截的參數
System.out.println("user:" + args[0]);
arg0.getArguments()[0] ="hello!";// 修改被攔截的參數
}
System.out.println(arg0.getMethod().getName() + "---!");
return arg0.proceed();// 運行UserService接口的printUser方法
} catch (Exception e) ...{
throw e;
}
}
}
測試類
Java代碼
package com.test.TestSpring3;
importorg.springframework.beans.factory.BeanFactory;
importorg.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.context.ApplicationContext;
importorg.springframework.context.support.ClassPathXmlApplicationContext;
importorg.springframework.context.support.FileSystemXmlApplicationContext;
importorg.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
importorg.springframework.web.context.support.WebApplicationContextUtils;
public class TestInterceptor ...{
public static void main(String[] args) ...{
ApplicationContext ctx = new FileSystemXmlApplicationContext(
"classpath:applicationContext.xml");
// ApplicationContext ctx = newClassPathXmlApplicationContext("applicationContext.xml");
UserService us = (UserService)ctx.getBean("userService");
us.printUser("shawn");
}
}
配置文件
Xml代碼
<?xml version="1.0"encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC"-//SPRING//DTD BEAN//EN""http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="userServiceImp"
class="com.test.TestSpring3.UserServiceImp" />
<bean id="userInterceptor"class="com.test.TestSpring3.UserInterceptor" />
<bean id="userService"
class="org.springframework.aop.framework.ProxyFactoryBean">
<!-- 代理接口-->
<propertyname="proxyInterfaces">
<value>com.test.TestSpring3.UserService</value>
</property>
<!-- 目標實現類-->
<property name="target">
<ref local="userServiceImp" />
</property>
<!-- 攔截器 -->
<property name="interceptorNames">
<list>
<value>userInterceptor</value>
</list>
</property>
</bean>
</beans>
輸出:
user:shawn
printUser---!
printUser user:hello!
結論:調用方法的時候 傳入的值被攔截修改了.
攔截器中的事務管理(事務攔截機)
如果不採用攔截機的機制時,在使用JDBC進行數據庫訪問時,存在兩種情況:
自動提交 這是JDBC驅動默認的模式,每次數據庫操作(CRUD)成功完成後,都作爲一個單獨的事務自動提交,如果未成功完成,即拋出了 SQLException 的話,僅最近的一個操作將回滾。
非自動提交 這是想更好的控制事務時需要程序地方式進行控制:
在進行該事務單元的任何操作之前 setAutoCommit(false)
在成功完成事務單元后commit()
在異常發生後rollback()
自動提交模式是不被推薦的,因爲每個操作都將產生一個事務點,這對於大的應用來說性能將受到影響;再有,對於常見的業務邏輯,這種模式顯得無能爲力。比如:
轉帳,從A帳戶取出100元,將其存入B帳戶;如果在這兩個操作之間發生了錯誤,那麼用戶A將損失了100元,而本來應該給帳戶B的,卻因爲失敗給了銀行。
所以,建議在所有的應用中,如果使用 JDBC 都將不得不採用非自動提交模式(你們要能發現了在我們的 JDBC 那個例子中,我們採用的就是自動提交模式,我們是爲了把精力放在JDBC上,而不是事務處理上),即我們不得不在每個方法中:
Java代碼
try {
// 在獲得連接後,立即通過調用 setAutoCommit(false) 將事務處理置爲非自動提交模式 // Prepare Query to fetch the userInformation
pst = conn.prepareStatement(findByName);
// ... conn.commit();
} catch(Exception ex) {
conn.rollback();
throw ex;
}finally {
try {
// Close Result Set and Statement
if (rset != null) rset.close();
if (pst != null) pst.close();
}catch (Exception ex) {
ex.printStackTrace();
throw new Exception("SQL Error while closing objects = " +ex.toString());
}
}
這樣代碼在AOP的倡導者看來是“骯髒”的代碼。他們認爲,所有的與事務有關的方法都應當可以集中配置(見聲明性事務控制),並自動攔截,程序應當關心他們的主要任務,即商業邏輯,而不應和事務處理的代碼攪和在一起。
我先看看 Spring 是怎麼做到攔截的:
Spring 內置支持的事務處理攔截機
這裏因爲要用到JpetStore項目中的代碼,我們將 applicationContext.xml 全部內容列出:
<?xml version="1.0" encoding="UTF-8"?>
<!--
-Application context definition for JPetStore's business layer.
-Contains bean references to the transaction manager and to the DAOs in
-dataAccessContext-local/jta.xml (see web.xml's"contextConfigLocation").
Jpetstore 的應用上下文定義,包含事務管理和引用了在 dataAccessContext-local/jta.xml(具體使用了哪個要看 web.xml 中的 'contextConfigLocation' 的配置)中註冊的DAO
-->
<beansxmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/aophttp://www.springframework.org/schema/aop/spring-aop-2.0.xsd
http://www.springframework.org/schema/txhttp://www.springframework.org/schema/tx/spring-tx-2.0.xsd">
<!-- ========================= GENERAL DEFINITIONS========================= -->
<!-- Configurer that replaces ${...} placeholders with values fromproperties files
佔位符的值將從列出的屬性文件中抽取出來
-->
<!-- (in this case, mail and JDBC related properties) -->
<bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<list>
<value>WEB-INF/mail.properties</value>
<value>WEB-INF/jdbc.properties</value>
</list>
</property>
</bean>
<!-- MailSender used by EmailAdvice
指定用於發送郵件的javamail 實現者,這裏使用了spring 自帶的實現。此 bean 將被 emailAdvice 使用
-->
<bean id="mailSender"class="org.springframework.mail.javamail.JavaMailSenderImpl">
<property name="host" value="${mail.host}"/>
</bean>
<!-- ========================= BUSINESS OBJECT DEFINITIONS======================== -->
<!-- 不需要,因爲被SpringMVC 的實現使用 Genericvalidator for Account objects, to be used for example by the Spring web tier-->
<bean id="accountValidator"class="org.springframework.samples.jpetstore.domain.logic.AccountValidator"/>
<!-- 不需要,因爲被SpringMVC 的實現使用 Genericvalidator for Order objects, to be used for example by the Spring web tier-->
<bean id="orderValidator"class="org.springframework.samples.jpetstore.domain.logic.OrderValidator"/>
<!--
主要的商業邏輯對象,即我們所說的門面對象
注入了所有的DAO,這些DAO是引用了 dataAccessContext-xxx.xml 中定義的DAO
門面對象中的所有方法的事務控制將通過下面的 aop:config 來加以控制
- JPetStore primary business object (default implementation).
- Transaction advice gets applied through the AOP configuration below.
-->
<bean id="petStore"class="org.springframework.samples.jpetstore.domain.logic.PetStoreImpl">
<property name="accountDao" ref="accountDao"/>
<property name="categoryDao"ref="categoryDao"/>
<property name="productDao" ref="productDao"/>
<property name="itemDao" ref="itemDao"/>
<property name="orderDao" ref="orderDao"/>
</bean>
<!-- ========================= ASPECT CONFIGURATION======================== -->
<!-- AOP配置,用來控制哪些方法將需要進行事務處理,採用了AspectJ 的語法 -->
<aop:config>
<!--
This definition creates auto-proxy infrastructure based on the givenpointcut,
expressed in AspectJ pointcut language. Here: applying the advice named
"txAdvice" to all methods on classes named PetStoreImpl.
-->
<!-- 指出在PetStoreFacade 的所有方法都將採用txAdvice(在緊接着的元素中定義了)事務方針,注意,我們這裏雖然指定的是接口 PetStoreFacace, 但其暗示着其所有的實現類也將
同樣具有這種性質,因爲本身就是實現類的方法在執行的,接口是沒有方法體的。 -->
<aop:advisor pointcut="execution(**..PetStoreFacade.*(..))" advice-ref="txAdvice"/>
<!--
This definition creates auto-proxy infrastructure based on the givenpointcut,
expressed in AspectJ pointcut language. Here: applying the advice named
"emailAdvice" to insertOrder(Order) method of PetStoreImpl
-->
<!-- 當執行PetStoreFacade.insertOrder方法,該方法最後一個參數爲Order類型時(其實我們的例子中只有一個 insertOrder 方法,但這告訴了我們,當我們的接口或類中有重載了的方法,
並且各個重載的方法可能使用不同的攔截機機制時,我們可以通過方法的參數加以指定),將執行emailAdvice(在最後定義的那個元素)-->
<aop:advisor pointcut="execution(**..PetStoreFacade.insertOrder(*..Order))" advice-ref="emailAdvice"/>
</aop:config>
<!--
事務方針聲明,用於控制採用什麼樣的事務策略
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_REQUIREDwith 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>
<!-- 攔截機,用於在適當的時機(通過AOP配置,如上面)在方法執行成功後發送郵件
AOP advice used to send confirmation email after order has beensubmitted -->
<!-- -->
<bean id="emailAdvice" class="org.springframework.samples.jpetstore.domain.logic.SendOrderConfirmationEmailAdvice">
<property name="mailSender" ref="mailSender"/>
</bean>
<!-- ========================= 忽略 REMOTE EXPORTER DEFINITIONS ======================== -->
</beans>
這個配置比想象的要簡單的多:
Xml代碼
<aop:config>
<!-- This definition creates auto-proxyinfrastructure based on the given pointcut, expressed in AspectJ pointcutlanguage.
Here: applying the advice named "txAdvice" to all methods onclasses named PetStoreImpl. 指出在 PetStoreFacade
的所有方法都將採用txAdvice(在緊接着的元素中定義了)事務方針,注意,我們這裏雖然指定的是接口 PetStoreFacace,
但其暗示着其所有的實現類也將同樣具有這種性質,因爲本身就是實現類的方法在執行的,接口是沒有方法體的。 -->
<aop:advisor pointcut="execution(* *..PetStoreFacade.*(..))"advice-ref="txAdvice"/>
<!-- 其它攔截機-->
</aop:config>
1. 所有的攔截機配置都放在 <aop:config> 配置元素中.
2. 下面還是需要理解一下幾個有關AOP的專用名詞,不過,是挺抽象的,最好能會意出其的用意
pointcut 切入點,比如:updateAccount 方法需要進行事務管理,則這個切入點就是“執行方法體”(execution)。Spring 所有支持的切入點類型在都在 Spring reference: 6.2.3.1. Supported Pointcut Designators 中列出了。
advice 要對這個切入點進行什麼操作,比如事務控制
advisor Spring 特有的概念,將上兩個概念合到一個概念中來,即一個 advisor 包含了一個切入點及對這個切入點所實施的操作。
因爲 方法執行切入點 execution 爲最常見的切入點類型,我們着重介紹一下,execution 的完全形式爲:
execution(modifiers-pattern?ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)
這是一個正則表達式,其中由'?' 結尾的部分是可選的。翻譯過來就是:
執行(方法訪問修飾符? 方法返回類型 聲明類型? 方法名(方法參數類型) 拋出異常?)
所有的這些都是用來定義執行切入點,即那些方法應該被侯選爲切入點:
方法訪問修飾符 即 public, private 等等
方法返回類型 即方法返回的類型,如 void, String 等等
聲明類型 1.5的語法,現在可以先忽略它
方法名 方法的名字
方法參數類型 方法的參數類型
拋出異常 方法聲明的拋出的異常
例如,所有dao代碼被定義在包 com.xyz.dao 及子包 com.xyz.dao.hibernate, 或者其它,如果還有的話,子包中, 裏面定義的是提供DAO功能的接口或類,那麼表達式:
execution(* com.xyz.dao..*.*(..))
表示切入點爲:執行定義在包com.xyz.dao 及其子包(因爲 .. 所致) 中的任何方法
詳細情況可以參見Spring refernce: 6.2.3.4. Examples
因此這個表達式爲執行定義在類PetStoreFacade 及其實現類中的所有方法,採取的動作定義在 txAdvice 中. 關於該 advice 的定義,(見聲明性事務控制)一節
<aop:advisor pointcut="execution(**..PetStoreFacade.*(..))" advice-ref="txAdvice"/>
Spring 自定攔截機
來爲了進行事務控制,我們只需簡單地配置幾下,所有的工作都由 Spring 來做。這樣固然很好,但有時我們需要有我們特有的控制邏輯。因爲Spring 不可能包含所有人需要的所有攔截機。所以它提供了通過程序的方式加以定製的方式。我們的項目中就有這麼一個攔截機,在用戶確認付款後,將定單信息通過 email 的方式發送給註冊用戶的郵箱中。
<aop:config>
...
<!-- 當執行 PetStoreFacade.insertOrder方法,該方法最後一個參數爲Order類型時(其實我們的例子中只有一個 insertOrder 方法,但這告訴了我們,當我們的接口或類中有重載了的方法,
並且各個重載的方法可能使用不同的攔截機機制時,我們可以通過方法的參數加以指定),將執行emailAdvice(在最後定義的那個元素)-->
<aop:advisor pointcut="execution(**..PetStoreFacade.insertOrder(*..Order))"advice-ref="emailAdvice"/>
</aop:config>
紅色的註釋已經說的很清楚這個Advisor 了,它的切入點(pointcut)爲PetStoreFacade 的 voidinsertOrder(Order order) 方法,採取的動作爲引用的 emailAdvice, 下面我們就來看看 emailAdvice:
<bean id="emailAdvice"class="org.springframework.samples.jpetstore.domain.logic.SendOrderConfirmationEmailAdvice">
<property name="mailSender" ref="mailSender"/>
</bean>
它給了這個 advice 的實現類爲 logic 包中 SendOrderConfirmationEmailAdvice,該Bean 引用了我們前面定義的郵件發送器(一個 Spring 內置的郵件發送器).
下面看看這個實現類:
public classSendOrderConfirmationEmailAdvice implements AfterReturningAdvice,InitializingBean {
// user jes on localhost
private static final String DEFAULT_MAIL_FROM ="[email protected]";
private static final String DEFAULT_SUBJECT = "Thank you for yourorder!";
private final Log logger = LogFactory.getLog(getClass());
private MailSender mailSender;
private String mailFrom = DEFAULT_MAIL_FROM;
private String subject = DEFAULT_SUBJECT;
public void setMailSender(MailSender mailSender) {
this.mailSender = mailSender;
}
public void setMailFrom(String mailFrom) {
this.mailFrom = mailFrom;
}
public void setSubject(String subject) {
this.subject = subject;
}
public void throws Exception {
if (this.mailSender == null) {
throw new IllegalStateException("mailSender is required");
}
}
/**
*
* @param returnValue 被攔截的方法的返回值
* @param m 被攔截的方法的所有信息(Method類封裝了這些信息)
* @param args 被攔截的方法的所有參數組成的數組
* @param target 目標對象,對於方法執行來說,即是方法所在的類的實例(與 this 同,批當前對象)
* @throws java.lang.Throwable
*/
public void afterReturning(Object returnValue, Method m, Object[] args,Object target) throws Throwable {
// 我們被攔截的方法爲 voidinsertOrder(Order order),方法只有一個參數,所以可知數據的第1個元素即是被傳進的 order對象
// 得到了order 對象,就可以將 order 對應的帳戶名及帳單號發送到郵件中,以便確認無誤。
Order order = (Order) args[0];
Account account = ((PetStoreFacade) target).getAccount(order.getUser().getUsername());
// don't do anything if email address is not set
if (account.getEmail() == null || account.getEmail().length() == 0) {
return;
}
StringBuffer text = new StringBuffer();
text.append("Dear ").append(account.getFirstname()).
append('').append(account.getLastname());
text.append(", thank your for your order from JPetStore. " +
"Please note that yourorder number is ");
text.append(order.getId());
SimpleMailMessage mailMessage = new SimpleMailMessage();
mailMessage.setTo(account.getEmail());
mailMessage.setFrom(this.mailFrom);
mailMessage.setSubject(this.subject);
mailMessage.setText(text.toString());
try {
this.mailSender.send(mailMessage);
} catch (MailException ex) {
// just log it and go on
logger.warn("An exception occured when trying to send email",ex);
}
}
}
1. 紅色的內容即爲反向注入的 mailSender 屬性
2. 藍色的內容爲 Spring Bean 的一個通用的接口 InitializingBean ,實現類需要實現該接口定義的方法 afterPropertiesSet() ,該方法中一般是在Bean 被初始化後並設置了所有的setter 注入後調用的。所以這裏是保證郵件發送器配置正確。因爲如果沒有配置正確,下面的工作是無法進行的,所以與其等那時拋出異常,還不如早早地在部署時就告知(通過拋出 IllegalStateException 來提示)
3. 綠色的內容爲這個 Advise 的核心,即在切入點被切入後將採用的動作。因爲 Advise 也同樣有多種類型,比如我們這裏的“方法正常返回”,“方法執行前”,“方法執行後”,“環繞在方法執行前後”,“方法拋出異常時”等等(詳情參見 SpringReference: 6.2.4. Declaring advice)。但是我們的邏輯爲在用戶確認定單並且執行成功(所謂的成功是指將這一定單插入到了表 Order 中了)後,將發送一確認信。所以”方法正常返回“完全符合我們的要求。
接口AfterReturningAdvice即是 Spring中表示”方法正常返回“ 這一語義的 Advice, 所以我們實現這個接口及其必須的方法 afterReturning.
方法代碼的工作其實並不重要,只要我們理解這些“魔法”一樣的技術後,實現代碼是很簡單的。值得提及的是這個方法的參數,這些參數是封裝了切入點的所有信息,請見上面的註釋。在我們的實現中只使用了被攔截方法的參數,在複雜的 Advice 實現中可能會用到切入點所有信息。