spring學習(三)—AOP

Aspect Oriented Programming 面向切面編程。解耦是程序員編碼開發過程中一直追求的。AOP也是爲了解耦所誕生。

AOP/OOP

AOP、OOP在字面上雖然非常類似,但卻是面向不同領域的兩種設計思想。OOP(面向對象編程)針對業務處理過程的實體及其屬性和行爲進行抽象封裝,以獲得更加清晰高效的邏輯單元劃分。
而AOP則是針對業務處理過程中的切面進行提取,它所面對的是處理過程中的某個步驟或階段,以獲得邏輯過程中各部分之間低耦合性的隔離效果。這兩種設計思想在目標上有着本質的差異。
上面的陳述可能過於理論化,舉個簡單的例子,對於“僱員”這樣一個業務實體進行封裝,自然是OOP/OOD的任務,我們可以爲其建立一個“Employee”類,並將“僱員”相關的屬性和行爲封裝其中。而用AOP設計思想對“僱員”進行封裝將無從談起。
同樣,對於“權限檢查”這一動作片斷進行劃分,則是AOP的目標領域。而通過OOD/OOP對一個動作進行封裝,則有點不倫不類。
換而言之,OOD/OOP面向名詞領域,AOP面向動詞領域。
比如說,對於一個學生來說,他的成績從期中到期末的比較,這只是在他個人方面比較,關注點在縱向比較 使用OOP的思想來實現比較方便,而對於全班學生期中成績的比較,則是在橫向比較,這時AOP的思想比較適合
面向切面編程(AOP)提供另外一種角度來思考程序結構,通過這種方式彌補了面向對象編程(OOP)的不足。 除了類(classes)以外,AOP提供了 切面。切面對關注點進行模塊化,例如橫切多個類型和對象的事務管理。 (這些關注點術語通常稱作 橫切(crosscutting) 關注點。)

Spring的一個關鍵的組件就是 AOP框架。 儘管如此,Spring IoC容器並不依賴於AOP,這意味着你可以自由選擇是否使用AOP,AOP提供強大的中間件解決方案,這使得Spring IoC容器更加完善。

AOP的術語

  • 切面(Aspect):首先,我們在項目中把一些重複的代碼,我們可以移出來,我們稱之爲橫切關注點,比如servlet中的權限管理,文件上傳等。一個關注點的模塊化,就是切面,切面可以使用xml配置和註解@Aspect方式實現
  • 連接點(JoinPoint): 這個更好解釋了,就是spring允許你使用通知的地方,那可真就多了,基本每個方法的前,後(兩者都有也行),或拋出異常時都可以是連接點。spring只支持方法連接點。
  • 通知(Advice): 在切點的某個特定的連接點上執行的動作,在上一篇的文章中,我們使用了xml方式練習了幾種通知,之後我們學習如何使用註解方式配置
  • 切入點(Pointcut) :匹配連接點的斷言,通知和一個切入點表達式關聯,並在滿足這個切入點的連接點上運行
  • 引入(introduction) :允許我們向現有的類添加新方法屬性。
  • 目標(target): 引入中所提到的目標類,也就是要被通知的對象,也就是真正的業務邏輯,他可以在毫不知情的情況下,被咱們織入切面。而自己專注於業務本身的邏輯。
  • AOP代理(proxy) :AOP框架創建的對象,用來實現切面契約,在spring中,AOP代理可以是jdk動態代理或cglib代理
  • 織入(weaving) : 把切面(aspect)連接到其它的應用程序類型或者對象上,並創建一個被通知(advised)的對象。 這些可以在編譯時(例如使用AspectJ編譯器),類加載時和運行時完成。 Spring和其他純Java AOP框架一樣,在運行時完成織入。

    spring用代理類包裹切面,把他們織入到Spring管理的bean中。也就是說代理類僞裝成目標類,它會截取對目標類中方法的調用,讓調用者對目標類的調用都先變成調用僞裝類,僞裝類中就先執行了切面,再把調用轉發給真正的目標bean。

通知的類型:

  • 前置通知(Before advice): 在某連接點(join point)之前執行的通知,但這個通知不能阻止連接點前的執行(除非它拋出一個異常)。
  • 返回後通知(After returning advice): 在某連接點(join point)正常完成後執行的通知:例如,一個方法沒有拋出任何異常,正常返回。
  • 拋出異常後通知(After throwing advice): 在方法拋出異常退出時執行的通知。
  • 後通知(After (finally) advice): 當某連接點退出的時候執行的通知(不論是正常返回還是異常退出)。
  • 環繞通知(Around Advice): 包圍一個連接點(join point)的通知,如方法調用。這是最強大的一種通知類型。 環繞通知可以在方法調用前後完成自定義的行爲。它也會選擇是否繼續執行連接點或直接返回它們自己的返回值或拋出異常來結束執行。

環繞通知是最常用的一種通知類型。大部分基於攔截的AOP框架,例如Nanning和JBoss4,都只提供環繞通知。

跟AspectJ一樣,Spring提供所有類型的通知,我們推薦你使用盡量簡單的通知類型來實現需要的功能。 例如,如果你只是需要用一個方法的返回值來更新緩存,雖然使用環繞通知也能完成同樣的事情, 但是你最好使用After returning通知而不是環繞通知。 用最合適的通知類型可以使得編程模型變得簡單,並且能夠避免很多潛在的錯誤。 比如,你不需要調用 JoinPoint(用於Around Advice)的 proceed() 方法,就不會有調用的問題。

Spring的AOP代理

Spring缺省使用J2SE 動態代理(dynamic proxies)來作爲AOP的代理。這樣任何接口都可以被代理。

Spring也支持使用CGLIB代理. 對於需要代理類而不是代理接口的時候CGLIB代理是很有必要的。 如果一個業務對象並沒有實現一個接口,默認就會使用CGLIB。 此外,面向接口編程 也是一個最佳實踐,業務對象通常都會實現一個或多個接口。

此外,還可以強制的使用CGLIB:我們將會在以後討論這個問題,解釋問什麼你會要這麼做。

在Spring 2.0之後,Spring可能會提供多種其他類型的AOP代理,包括了完整的生成類。這不會影響到編程模型。

@AspectJ支持

如果你使用Java 5的話,推薦使用Spring提供的@AspectJ切面支持,通過這種方式聲明Spring AOP中使用的切面。 “@AspectJ”使用了Java 5的註解,可以將切面聲明爲普通的Java類。 AspectJ 5發佈的 AspectJ project 中引入了這種@AspectJ風格。 Spring 2.0 使用了和AspectJ 5一樣的註解,使用了AspectJ 提供的一個庫來做切點(pointcut)解析和匹配。 但是,AOP在運行時仍舊是純的Spring AOP,並不依賴於AspectJ 的編譯器或者織入器(weaver)。

啓用@AspectJ支持

爲了在Spring配置中使用@AspectJ aspects,你必須首先啓用Spring對基於@AspectJ aspects的配置支持,自動代理(autoproxying)基於通知是否來自這些切面。 自動代理是指Spring會判斷一個bean是否使用了一個或多個切面通知,並據此自動生成相應的代理以攔截其方法調用,並且確認通知是否如期進行。

通過在你的Spring的配置中引入下列元素來啓用Spring對@AspectJ的支持:

<!-- 啓用aspectJ切面 註解配置通知 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
<!-- 用於使用註解配置bean  -->
<context:annotation-config></context:annotation-config>
<!-- 當使用註解配置bean時,以下配置會對當前包及其子包進行搜索,不再配置包中的bean將不起作用 -->
<context:component-scan base-package="com.lgh.annotation"></context:component-scan>

同時我們需要在應用程序的classpath中引入兩個AspectJ庫:aspectjweaver.jar 和 aspectjrt.jar。
使用maven進行添加jar包:

<dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>3.2.17.RELEASE</version>
    </dependency>

    <!-- aspectjweaver -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.5</version>

        </dependency>
        <!-- aspectjrt -->
        <dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.8.5</version>
</dependency>

聲明一個切面

在啓用@AspectJ支持的情況下,在application context中定義的任意帶有一個@Aspect切面(擁有@Aspect註解)的bean都將被Spring自動識別並用於配置在Spring AOP。 以下例子展示了爲了完成一個不是非常有用的切面所需要的最小定義:

下面是在application context中的一個常見的bean定義,這個bean指向一個使用了 @Aspect 註解的bean類:

<bean name="tx" class="com.lgh.annotation.aspect.TxManager"></bean>

下面是 TxManager類定義,使用了 org.aspectj.lang.annotation.Aspect 註解。

package com.lgh.annotation.aspect;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;

@Aspect

public class TxManager {

...

}

切面(用 @Aspect 註解的類)和其他類一樣有方法和字段定義。他們也可能包括切入點,通知和引入(inter-type)聲明。

聲明一個切入點(pointcut)

回想一下,切入點決定了連接點關注的內容,使得我們可以控制通知什麼執行。 Spring AOP只支持Spring bean方法執行連接點。所以你可以把切入點看做是匹配Spring bean上的方法執行。 一個切入點聲明有兩個部分:一個包含名字和任意參數的簽名,還有一個切入點表達式,該表達式決定了我們關注那個方法的執行。 在@AspectJ中,一個切入點實際就是一個普通的方法定義提供的一個簽名,並且切入點表達式使用 @Pointcut註解來表示。 這個方法的返回類型必須是 void。 如下的例子定義了一個切入點’transfer’,這個切入點匹配了任意名爲”transfer”的方法執行:

@Pointcut(“execution(* transfer(..))”)
結和通知使用註解方式寫的話就是:
@Before(“execution(* com.lgh.annotation.dao..(..))”)

支持的切入點指定者

Spring AOP 支持在切入點表達式中使用如下的AspectJ切入點指定者:

其他的切入點類型

完整的AspectJ切入點語言支持額外的切入點指定者,但是Spring不支持這個功能。 他們分別是call, initialization, preinitialization, staticinitialization, get, set, handler, adviceexecution, withincode, cflow, cflowbelow, if, @this 和 @withincode。 在Spring AOP中使用這些指定者將會導致拋出IllegalArgumentException異常。

Spring AOP支持的切入點指定者可能在將來的版本中得到擴展,不但支持更多的AspectJ 切入點指定者(例如”if”),還會支持某些Spring特有的切入點指定者,比如”bean”(用於匹配bean的名字)。

execution - 匹配方法執行的連接點,這是你將會用到的Spring的最主要的切入點指定者。

within - 限定匹配特定類型的連接點(在使用Spring AOP的時候,在匹配的類型中定義的方法的執行)。

this - 限定匹配特定的連接點(使用Spring AOP的時候方法的執行),其中bean reference(Spring AOP 代理)是指定類型的實例。

target - 限定匹配特定的連接點(使用Spring AOP的時候方法的執行),其中目標對象(被代理的appolication object)是指定類型的實例。

args - 限定匹配特定的連接點(使用Spring AOP的時候方法的執行),其中參數是指定類型的實例。

@target - 限定匹配特定的連接點(使用Spring AOP的時候方法的執行),其中執行的對象的類已經有指定類型的註解。

@args - 限定匹配特定的連接點(使用Spring AOP的時候方法的執行),其中實際傳入參數的運行時類型有指定類型的註解。

@within - 限定匹配特定的連接點,其中連接點所在類型已指定註解(在使用Spring AOP的時候,所執行的方法所在類型已指定註解)。

@annotation - 限定匹配特定的連接點(使用Spring AOP的時候方法的執行),其中連接點的主題有某種給定的註解。

因爲Spring AOP限制了連接點必須是方法執行級別的,pointcut designators的討論也給出了一個定義,這個定義和AspectJ的編程指南中的定義相比顯得更加狹窄。 除此之外,AspectJ它本身有基於類型的語義,在執行的連接點’this’和’target’都是指同一個對象,也就是執行方法的對象。 Spring AOP是一個基於代理的系統,並且嚴格區分代理對象本身(對應於’this’)和背後的目標對象(對應於’target’)

合併切入點表達式

切入點表達式可以使用using ‘&&’, ‘||’ 和 ‘!’來合併.還可以通過名字來指向切入點表達式。 以下的例子展示了三種切入點表達式: anyPublicOperation(在一個方法執行連接點代表了任意public方法的執行時匹配); inTrading(在一個代表了在交易模塊中的任意的方法執行時匹配) 和 tradingOperation(在一個代表了在交易模塊中的任意的公共方法執行時匹配)。

@Pointcut(“execution(public * *(..))”)

private void anyPublicOperation() {}

@Pointcut(“within(com.xyz.someapp.trading..*”)

private void inTrading() {}

@Pointcut(“anyPublicOperation() && inTrading()”)

private void tradingOperation() {}就上所示的,從更小的命名組件來構建更加複雜的切入點表達式是一種最佳實踐。 當用名字來指定切入點時使用的是常見的Java成員可視性訪問規則。 (比如說,你可以在同一類型中訪問私有的切入點,在繼承關係中訪問受保護的切入點,可以在任意地方訪問公共切入點。 成員可視性訪問規則不影響到切入點的 匹配。
切入點定義一個你可以在任何需要切入點表達式的地方可引用的切面 ,比如在項目中對數據庫進行增刪改查,這時用到了事務管理,這是一段重複代碼,spring可以幫我們把重複代碼提走使用HibernateTransactionManager類對事務進行控制,這時調用業務層時使用xml配置則是:

<bean id="mySessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="configLocation" value="classpath:hibernate.cfg.xml"></property>
</bean>

<!-- 事務管理 器-->
<bean id="myManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="mySessionFactory"></property>
</bean>



<!-- 配置事務管理特性 -->
<tx:advice id="txAdvice" transaction-manager="myManager">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
<tx:method name="get*" read-only="true"/>
<tx:method name="find*" read-only="true"/>
</tx:attributes>

</tx:advice>
<!-- 
 execution : 方法執行時候
 1* 不限制返回值
 2* 任意類
 3* 任意方法
 4..任意方法

 -->
<aop:config>
<aop:pointcut expression="execution(* com.lgh.shop.admin.biz.impl.*.*(..))" id="ps"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="ps"/>
</aop:config>

註解示例

package com.lgh.annotation.aspect;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;

@Aspect
public class TxManager {
    // 前置通知(Before advice)
    @Before("execution(* com.lgh.annotation.dao.*.*(..))")
    public void beginTx() {
        System.out.println("開啓事務 1");
    }

    // 返回後通知(After returning advice)
    @AfterReturning("execution(* com.lgh.annotation.dao.*.*(..))")
    public void commitTx() {
        System.out.println("提交事務 1");
    }

    // 拋出後通知(After throwing advice)
    @AfterThrowing("execution(* com.lgh.annotation.dao.*.*(..))")
    public void rollbackTx() {
        System.out.println("出現異常,回滾事務 1");
    }

    /*
     * 後通知(After (finally) advice)
     * 
     * 不論一個方法是如何結束的,在它結束後(finally)
     * 後通知(After (finally) advice)都會運行。 使用 @After
     * 註解來聲明。這個通知必須做好處理正常返回和異常返回兩種情況。
     * 通常用來釋放資源。
     */
    @After("execution(* com.lgh.annotation.dao.*.*(..))")
    public void finallyTx() {
        System.out.println("關閉session 1");
    }

}
"execution(* com.lgh.annotation.dao.*.*(..))"

這段代碼表示當dao層的任意類調用任意方法時通知執行
此時我們需要一個UserDao

package com.lgh.annotation.dao;

public interface UserDao {

    void save();
    void del();
}

實現類:

package com.lgh.annotation.dao.impl;

import org.springframework.stereotype.Service;

import com.lgh.annotation.dao.UserDao;
/*
 * 這裏使用了註解方式配置bean.
 * */
@Service(value="userDao")
public class UserDaoImpl implements UserDao {

    @Override
    public void save() {

        System.out.println("保存商品");
    }

    @Override
    public void del() {
         System.out.println("刪除商品");
         //這裏人爲的製造了一個除零異常
         int i = 3/0;

    }

}

測試:

package com.lgh.annotation.test;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.lgh.annotation.dao.UserDao;

public class SpringAnnoTest {

    public static void main(String[] args) {
        ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserDao userDao = (UserDao) ac.getBean("userDao");
        userDao.save();

        System.out.println("---------------------------");
        userDao.del();
    }

}

結果:

開啓事務 1
保存商品
關閉session 1
提交事務 1
---------------------------
開啓事務 1
刪除商品
關閉session 1
出現異常,回滾事務 1
Exception in thread "main" java.lang.ArithmeticException: / by zero
    at com.lgh.annotation.dao.impl.UserDaoImpl.del(UserDaoImpl.java:22)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

我們看到當我們調用dao層類的方法時,執行了通知
有時我們不需要配置這麼多的通知,只配一個環繞通知,就能完成所有事情
配置環繞通知

package com.lgh.annotation.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;

@Aspect

public class TxAroundManager {

    /*
     * 有時我們不需要配置這麼多的通知,
     * 只配一個環繞通知,就能完成所有事情    
     * */

    @Around("execution(* com.lgh.annotation.dao.*.*(..))")
    public void AroundTx(ProceedingJoinPoint pjp) throws Throwable{
        try {
            System.out.println("開啓事務 2");
            pjp.proceed();
            System.out.println("提交事務 2");
        } catch (Exception e) {
            System.out.println("出現異常,回滾事務 2");
        }finally {
            System.out.println("關閉session 2");
        }

    }

//  @AfterThrowing("execution(* com.lgh.annotation.dao.*.*(..))")
//  public void rollbackTx(){
//      System.out.println("出現異常,回滾事務 2");
//  }
//  @After("execution(* com.lgh.annotation.dao.*.*(..))")
//  public void finallyTx(){
//      System.out.println("關閉session 2");
//  }
//  
}

xml配置:

<!-- 
先把上一個切面注起來 
<bean name="tx" class="com.lgh.annotation.aspect.TxManager"></bean>
--> 
<bean name="txAround" class="com.lgh.annotation.aspect.TxAroundManager"></bean>

結果:

開啓事務 2
保存商品
提交事務 2
關閉session 2
---------------------------
開啓事務 2
刪除商品
出現異常,回滾事務 2
關閉session 2

通知順序

當同一個切入點(執行方法)上有多個通知需要執行時,執行順序規則是什麼呢?

<bean name="tx" class="com.lgh.annotation.aspect.TxManager"></bean>
<bean name="txAround" class="com.lgh.annotation.aspect.TxAroundManager"></bean>
開啓事務 1
開啓事務 2
保存商品
提交事務 2
關閉session 2
關閉session 1
提交事務 1
---------------------------
開啓事務 1
開啓事務 2
刪除商品
出現異常,回滾事務 2
關閉session 2
關閉session 1
提交事務 1

這時默認執行xml配置前面的,我們使用註解@Order可以對其控制
@Order(1),括號中的數字越小,優先級越高,先執行,

@Aspect
@Order(5)
public class TxManager {}


@Aspect
@Order(1)
public class TxAroundManager {}

結果:

開啓事務 2
開啓事務 1
保存商品
關閉session 1
提交事務 1
提交事務 2
關閉session 2
---------------------------
開啓事務 2
開啓事務 1
刪除商品
關閉session 1
出現異常,回滾事務 1
出現異常,回滾事務 2
關閉session 2
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章