四、Spring的AOP

weixin_34320159發表於2017-03-10

AOP實現可分為兩類(按AOP框架修改原始碼的時機):

  • 靜態AOP實現:AOP框架在編譯階段對程式進行修改,即實現對目標類的增強,生成靜態的AOP代理類(生成的*.class檔案已經被改掉了,需要使用特定的編輯器)。以AspectJ為代表。
  • 動態AOP實現:AOP框架在執行階段動態生成AOP代理(在記憶體中以JDK動態代理或cglib動態代理生成AOP代理類)。以實現對目標類的增強。以Spring AOP為代表。

AOP的基本概念:

AOP框架具有如下兩個特徵:

  • 個步驟之間的良好隔離性。
  • 原始碼無關性。

關於面向切面程式設計的一些術語:

  • 切面(Aspect):切面用於組織多個Advice,Advice放在切面中定義。
  • 連線點(Joinpoint):程式執行過程中明確的點,如方法的呼叫,或者異常的丟擲。在Spring AOP中,連線點總是方法的呼叫。
  • 增強處理(Advice):AOP框架在特定的點執行的增強處理。處理有“before”,“around”,“after”等。
  • 切入點(Pointcut):可以插入增強處理的連線點。簡而言之,當某個連線點滿足指定要求時,該連線點將被新增增強處理,該連線點也就變成了切入點。
pointcut xxxPointcut():execution(void H*.say*())

如何使用表示式定義切入點是AOP的核心,Spring預設使用AspectJ切入點語法:

  • 引入:將方法或欄位新增到被處理的類中。Spring允許將新的介面引入到任何被處理的物件中。例如,你可以使用一個引入,使任何物件實現IsModified介面,以此來簡化快取。
  • 目標物件:被AOP框架進行增強處理的物件,也被稱為增強的物件。如果AOP框架採用的是動態AOP實現,那麼該物件就是一個被代理的物件。
  • AOP代理:AOP框架建立的物件,代理就是對目標物件的增強。Spring中的AOP代理可以是JDK動態代理,也可以是cglib代理。前者為實現介面的目標物件的代理,後者不實現介面的目標物件的代理。
  • 織入(Weaving):將增強處理新增到目標物件中,並建立一個被增強的物件(AOP代理)的過程就是織入。植入有兩種實現方式---編譯時增強(如AspectJ)和執行時增強(如Spring AOP)。Spring和其他純Java AOP框架一樣,在執行時完成織入。

Spring的AOP支援:

Spring中的AOP代理由Spring的IOC容器負責生成、管理,其依賴關係也由IOC容器負責管理。AOP代理可以直接使用容器中的其他Bean例項作為目標,這種關係可以由IOC容器的依賴注入提供。Spring預設使用Java動態代理來建立AOP代理。Spring目前僅支援將方法呼叫作為連線點(Joinpoint),如果需要把對成員變數的訪問和更新也作為增強處理的連線點,則可以考慮使用AspectJ。Spring側重於AOP實現和IOC容器之間的整合,用於幫助解決企業級開發中常見問題。Spring AOP採用基於代理的AOP實現方案,而AspectJ則採用編譯時增強的解決方案。
AOP程式設計中需要程式設計師參與的只有三個部分:

  • 第一普通業務元件。
  • 定義切入點,一個切入點可以橫切多個業務元件。
  • 定義增強處理,增強處理就是在AOP框架為普通業務組織織入的處理動作。

AOP代理方法=增強處理+目標物件的方法

Spring有如下兩種選擇來定義切入點和增強處理:

  • 基於註解的“零配置”方式:使用@Aspect、@Pointcut等註解來標註切入點和增強處理。
  • 基於XML配置檔案的管理方式:使用Spring配置檔案來定義切入點和增強處理。

基於註解的“零配置”方式:

Spring依然採用執行時生成動態代理的方式來增強目標物件,所以它不需要增加額外的編譯,也不需要AspectJ的織入器支援;而AspectJ採用編譯時增強,所以AspectJ需要自己的編譯器來編譯Java檔案,還需要織入器。
Spring中啟用對@AspectJ切面配置的支援:

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">
        
        <!-- 啟動@AspectJ支援 -->
        <aop:aspectj-autoproxy/>
      
</beans>

以及

<!-- 啟動@AspectJ支援 -->
<bean class="org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator"/>

Spring應用中啟動@AspectJ支援還需要在應用的類載入路徑下新增AspectJ庫:

  • aspectjweaver.jar
  • aspectjrt.jar
    以上兩個jar檔案直接在AspectJ安裝目錄的lib中獲取,Spring AOP還需要依賴aopalliance.jar。

1、定義切面:

當啟動了@AspectJ支援後,只要在Spring容器中配置一個帶@AspectJ註解的Bean,Spring將會自動識別該Bean,並將該Bean作為切面處理。

//使用@AspectJ定義一個切面類
@Aspect
public class LogAspect{
   //定義該類的其他類容
   .......
}

當使用@AspectJ來修飾一個Java類之後,Spring不會把該Bean當成元件Bea處理,因此負責增強後處理的Bean將會略過該Bean,不會對該Bean進行任何增強處理。

2、定義Before增強處理:

在一個切面類裡使用@Before來修飾一個方法時,該方法將作為Before增強處理。使用@Before修飾時,通常需要指定一個value屬性,該屬性指定一個切入點表示式(既可以是一個已有的切入點,也可以直接定義切入點表示式),用於指定該增強處理將被織入哪些切入點。

AuthAspect.java

package entity;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class AuthAspect {

    //匹配entity包下所有的類的所有方法作為切入點
    @Before("execution(* entity.*.*(..))")
    public void authority(){
        System.out.println("模擬執行許可權檢查!");
    }
}

HelloImpl.java

package entity;



import org.springframework.stereotype.Component;

import inter.Hello;
@Component("hello")
public class HelloImpl implements Hello{

    //定義一個簡單方法,模擬應用中的業務邏輯方法
    public void foo() {
        System.out.println("執行Hello元件的foo()方法");
    }
    //定義一個addUser()方法,模擬應用中新增使用者的方法
    public void addUser(String name,String pass){
        System.out.println("執行Hello元件的addUser新增使用者:"+name);
    }

}

beans.xml

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">
        <!-- 啟動@AspectJ支援 -->
         <aop:aspectj-autoproxy/>
        <!-- 指定自動搜尋Bean檔案,自動搜尋切面檔案 -->
        <context:component-scan base-package="entity">
           <context:include-filter type="annotation" expression="org.aspectj.lang.annotation.Aspect"/>
        </context:component-scan>
</beans>

AspectjTest.java

package test;





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

import entity.HelloImpl;

public class AspecjTest {
    public static void main(String[] args) {
        ApplicationContext ctx=new ClassPathXmlApplicationContext("beans.xml");
        HelloImpl hello=(HelloImpl) ctx.getBean("hello");
        hello.foo();
        hello.addUser("張三", "123456");
    }
    
}

輸出:

模擬執行許可權檢查!
執行Hello元件的foo()方法
模擬執行許可權檢查!
執行Hello元件的addUser新增使用者:張三

使用Before增強處理只能在目標方法執行之前織入增強,如果Before增強處理沒有特殊處理,目標方法總會自動執行,如果Before處理需要阻止目標方法的執行,可通過丟擲一個異常來實現。Before增強處理時,目標方法還未獲得執行的機會,所以Before增強處理無法訪問目標方法的返回值。

3、定義AfterReturning增強處理:

AfterReturning增強處理將在目標方法正常完成後被織入。
@AfterReturning註解可指定如下兩個常用屬性:

  • pointcut/value:它們都用於指定該切入點對應的切入點表示式。一樣即可是一個已有的切入點,也可以直接定義切入點表示式。當指定了pointcut屬性值後,value屬性將被覆蓋。
  • returning:該屬性指定一個形參名,用於表示Advice方法中可定義與此同名的形參,該形參可用於訪問目標方法的返回值。除此之外,在Advice方法中定義該形參(代表目標方法的返回值)時指定的型別,會限制目標方法必須返回指定型別的值或沒有返回值。
//定義一個切面
@Aspect
public class LogAspect{
  //匹配entity包下所有類的所有方法的執行作為切入點
  @AfterReturning(returning="rvt",pointcut="execution(* entity.*.*())")
  //宣告rvt時指定的型別會限制目標方法必須返回指定型別的值或沒有返回值
  //此處將rvt的型別宣告為Object,意味著對目標方法的返回值不加限制
  public void log(Object rvt){
       System.out.println("獲得目標方法返回值"+rvt);
       System.out.println("模擬記錄日誌功能....");
   }
}

@AfterReturning註解的returning屬性所指定的形參名對應於增強處理中的一個形參名,當目標方法執行返回後,返回值作為相應的引數值傳入增強處理方法。使用returning屬性還有 一個額外的作用:它可用於限定切入點只匹配具有對應返回值型別的方法。

4、定義AfterThrowing增強處理:

AfterThrowing增強處理主要用於處理程式中未處理的異常。
使用@AfterThrowing註解時可指定如下兩個常用屬性:

  • pointcut/value:都用於指定該切入點對應的切入表示式。
  • throwing:該屬性值指定一個形參名,用於表示Advice方法中可定義與此同名的形參,該形參用於訪問目標方法丟擲的異常。在Advice方法中定義該形參(代表目標方法丟擲的異常)時指定的型別,會限制目標方法必須丟擲指定型別的異常。
//定義一個切面
@Aspect
public class RepairAspect{
  //匹配entity包下所有類的所有方法的執行作為切入點
  @AfterThrowing(throwing="ex",pointcut="execution(* entity.*.*())")
  //宣告ex時指定的型別會限制目標方法必須丟擲指定的型別的異常
  //此處將ex的型別宣告為Throwable,意味著對目標方法丟擲的異常不加限制
  public void doRecoveryActions(Throwable ex){
       System.out.println("目標方法中丟擲的異常"+ex);
       System.out.println("模擬Advice對異常的修復....");
   }
}

使用throwing屬性還用一個額外的作用:它可用於限定切入點只匹配指定型別的異常。

5、After增強處理:

  • AfterReturning增強處理只有在目標方法成功完成後才會被織入。
  • After增強處理不管目標方法如何結束(包括成功完成和遇到異常終止兩種情況),他都會被織入。

After增強處理必須準備處理正常返回或異常返回兩種情況,這種處理通常用於釋放資源。使用@After註解修飾一個方法,即可將該方法轉成After增強處理。使用@After註解時需要指定一個value屬性,該屬性值用於指定該增強處理被織入的切入點。

//定義一個切面
@Aspect
public class ReleaseAspect{
    //匹配entity包下所有的類的所有方法的執行作為切入點
    @After("execution(* entity.*.*())")
    public void release(){
        System.out.println("模擬方法結束後的釋放資源...");
    }
}

After增強處理的作用非常類似於異常處理中的finally塊的作用。

6、Around增強處理:

@Around註解用於修飾Around增強處理,Around增強處理是功能較強大得增強處理,它近似等於Before和AfterReturning增強處理的總和,Around增強處理即可在執行目標方法之前織入增強動作,也可以在執行目標方法之後織入增強處理。
Around增強處理可以決定目標方法在什麼時候執行,如何執行,甚至可以完全阻止目標方法的執行。Around增強處理的功能雖然強大,但通常需要線上程安全的環境下使用。如果需要目標方法執行之前和執行之後共享某種狀態資料,則該考慮使用Around增強處理;尤其是需要改變目標方法的返回值時,則只能使用Around增強處理。
當定義一個Around增強處理方法時,該方法的第一個形參必須是ProceedingJoinPoint型別(至少包含一個形參),在增強處理方法體內,呼叫ProceedingJoinPoint引數的proceed()方法才會執行目標方法---這就是Around增強處理可以完全控制目標方法的執行時機、如何執行的關鍵;如果程式沒有呼叫proceedingJoinPoint引數的proceed()方法,則目標方法不會被執行。
使用Around增強處理可以取得目標方法最大的控制權,既可以完全控制目標方法的執行,一刻改變執行目標方法的引數,還可改變目標方法的返回值。

7、訪問目標方法的引數:

訪問目標方法最簡單的做法是定義增強處理方法時將第一個引數定義為JoinPoint型別,當該增強處理方法被呼叫時,該JoinPoint引數就代表了織入增強的連線點。JoinPoint裡包含如下幾個常用的方法:

  • Object[] getArgs():返回執行目標方法時的引數。
  • Signature getSignature():返回被增強的方法的相關資訊。
  • Object getTarget():返回被增強處理的目標物件。
  • Object getThis():返回AOP框架為目標物件生成的代理物件。

當使用Around增強處理是,需要將第一個引數定義為ProceedingJoionPoint型別,該型別是JoinPoint型別的子類。

Spring AOP 採用和AspectJ一樣的優先順序來織入增強處理:在“進入連線點時,具有最高優先順序的增強處理將先被織入。在“退出”連線點時,具有最高優先順序的增強處理會最後被織入。
當不同切面的兩個增強處理需要在同一個連線點被織入時,Spring AOP將以隨機的順序來織入這兩個增強處理。如果應用需要指定不同切面類裡增強處理的優先順序,Spring提供瞭如下兩種解決方案:

  • 讓切面實現org.springframework.core.Ordered介面,實現該介面只需要實現一個int getOrder()方法,該方法的返回值越小,優先順序越高。
  • 直接使用@Order註解來修飾一個切面類,使用@Order註解時可指定一個int型的value屬性,該值越小,則優先順序越高。

8、定義切入點:

定義切入點,其實質就是為一個切入點表示式起一個名稱,從而允許在多個增強處理中重用該名稱。Spring AOP只支援將Spring Bean的方法執行作為連線點,所以可以吧切入點看成所有能和切入點表示式匹配的Bean方法,切入點定義包含兩個部分:

  • 一個切入點表示式。
  • 一個包含名字和任意引數的方法名。
    其中切入點表示式用於指定該切入點和哪些方法進行匹配,包含名字和任意引數的方法簽名將作為該切入點的名稱。
//使用@Pointcut註解定義切入點
@Pointcut("execution(* transfer(..))")
//使用一個返回值為void、方法體為空的方法來命名切入點
private void anyOldTransfer(){...}

如果需要使用本切面中的切入點,則可在使用@Before、@After、@Around等註解來定義Advice時,使用pointcut或value屬性值引入已有的切入點。

@AfterReturning(pointcut="myPointcut()",returning="retVal")
public void writeLog(String msg,Object retVal){...}

使用其他切面類中的切入點時,應該使用切面類作為字首來限制切入點。

9、切入點指示符:

  • execution:用於匹配執行方法的連線點,這是Spring AOP中最主要的切入點指示符。
  • within:用於限定匹配特定型別的連線點,當使用Spring AOP的時候,只能匹配方法執行的連線點。
//在entity包中的任意連線點(在Spring AOP中只是方法執行的連線點)
within(entity.*)
//在entity包或子包中的任意連線點(在Spring AOP中只是方法執行的連線點)
within(entity..*)
  • this:用於限定AOP代理必須必須是指定型別的例項,匹配該物件的所有連線點。當使用Spring AOP的時候,只能匹配方法執行的連線點。
  • target:用於限定目標物件必須是指定型別的例項,匹配該物件的所有連線點。當使用Spring AOP的時候,只能匹配方法執行的連線點。
  • args:用於對連線點的引數型別進行限制,要求引數型別是指定型別的例項。當使用Spring AOP的時候,只能匹配方法執行的連線點。
  • bean:用於限定只匹配指定Bean例項內的連線點,實際上只能使用方法執行作為連線點。定義表示式時需要傳入Bean的id或name屬性,表示值匹配該Bean例項內的連線點。支援使用“*”萬用字元。
//匹配tradeService Bean例項內方法執行的連線點
bean(tradeService)
//匹配名字以Service結尾的Bean例項內方法執行的連線點
bean(*Service)

10、組合切入點表示式:

  • &&:要求連線點同時匹配兩個切入點表示式。
  • ||:只要求連線點匹配任意一個切入點表示式。
  • !:要求切入點不匹配指定的切入點表示式。

基於XML配置檔案的管理方式:

相關文章