Spring AOP 基礎

AOP

使用面向切面編程時,咱們仍然在一個地方定義通用功能,可是能夠經過聲明的方式定義該功能要以何種方式在何處應用,而無需修改受影響的類。php

術語

橫切關注點

影響應用多處的功能(日誌、事務、安全)java

加強(Advice)

加強定義了切面要完成的功能以及何時執行這個功能。spring

Spring 切面能夠應用 5 種類型的加強:express

  • 前置加強(Before) 在目標方法被調用前調用加強功能
  • 後置加強(After) 在目標方法完成以後調用加強,不關注方法輸出是什麼
  • 返回加強(After-returning) 在目標方法成功執行以後調用加強
  • 異常加強(After-throwing) 在目標方法拋出異常後調用加強
  • 環繞加強(Around) 在被加強的方法調用以前和調用以後執行自定義行爲,即包括前置加強和後置加強。

鏈接點(Join Point)

應用中每個有可能會被加強的點被稱爲鏈接點。編程

切點(Pointcut)

切點是規則匹配出來的鏈接點。安全

切面(Aspect)

切面是加強和切點的結合,定義了在什麼時候和何處完成其功能。app

引入(Introduction)

引入容許咱們向現有的類中添加新方法和屬性。能夠在不修改現有的類的狀況下,讓類具備新的行爲和狀態。框架

織入(Weaving)

織入是把切面應用到目標對象中並建立新的代理對象的過程。在目標對象的生命週期裏有多個點能夠進行織入:dom

  • 編譯器:切面在目標類編譯時織入。這種方式須要特殊的編譯器。AspectJ 的織入編譯器就是以這種方式織入切面的。
  • 類加載器:切面在目標類加載到 JVM 時被織入。這種方式須要特殊的類加載器(ClassLoader),它能夠在目標類被引入應用以前加強該目標類的字節碼。AspectJ5 的加載時織入(LTW)支持以這種方式織入。
  • 運行期:切面在應用運行時的某個時刻被織入。通常狀況下,在織入切面時,AOP 容器會爲目標對象動態地建立一個代理對象。Spring AOP 就是以這種方式織入切面的。

Spring 對 AOP 的支持

Spring 對 AOP 的支持在不少方面借鑑了 AspectJ 項目。目前 Spring 提供了 4 種類型的 AOP 支持:this

  • 基於代理的經典 AOP
  • 純 POJO 切面
  • @AspectJ 註解驅動的切面
  • 注入式 AspectJ 切面

Spring AOP 構建在動態代理基礎之上,所以 Spring 對 AOP 的支持侷限於方法攔截。

運行時加強

經過在代理中包裹切面,Spring 在運行期把切面織入到 Spring 管理的 bean 中。代理類封裝了目標類,並攔截被加強方法的調用,再把調用轉發給真正的目標 bean。在代理攔截到方法調用時,在調用目標 bean 方法以前,會執行切面邏輯。

直到應用須要代理的 bean 時,Spring 才建立代理對象。若是使用 ApplicationContext 的話,在 ApplicationContextBeanFactory 中加載全部 bean 的時候,Spring 纔會建立被代理的對象。

方法級別的鏈接點

Spring 基於動態代理實現 AOP,因此 Spring 只支持方法鏈接點。其餘的 AOP 框架好比 AspectJ 與 JBoss,都提供了字段和構造器接入點,容許建立細粒度的加強。

切點表達式

Spring AOP 中,使用 AspectJ 的切點表達式來定義切點。Spring 只支持 AspectJ 切點指示器(pointcut designator)的一個子集。

指示器

AspectJ 指示器 描述
arg( ) 限制鏈接點匹配參數爲指定類型的執行方法
execution( ) 用於匹配鏈接點
this 指定匹配 AOP 代理的 bean 引用的類型
target 指定匹配對象爲特定的類
within( ) 指定鏈接點匹配的類型
@annotation 匹配帶有指定註解的鏈接點

編寫切點

package concert;

public interface Performance {
    public void perform();
}
複製代碼

Performance 類能夠表明任何類型的現場表演,好比電影、舞臺劇等。如今編寫一個切點表達式來限定 perform() 方法執行時觸發的加強。

execution(* concert.Performance.perform(..))
複製代碼

每一個部分的意義以下圖所示:

切點表達式

也能夠引入其餘註解對匹配規則作進一步限制。好比

execution(* concert.Performance.perform(..)) && within(concert.*)
複製代碼

within() 指示器限制了切點僅匹配 concert 包。

Spring 還有一個 bean() 指示器,容許咱們在切點表達式中使用 bean 的 ID 表示 bean。

execution(* concert.Performance.perform(..)) && bean('woodstock')
複製代碼

以上的切點就表示限定切點的 bean 的 ID 爲 woodstock

使用註解建立切面

定義切面

在一場演出以前,咱們須要讓觀衆將手機靜音且就座,觀衆在表演以後鼓掌,在表演失敗以後能夠退票。在觀衆類中定義這些功能。

@Aspect
public class Audience {
    
    @Pointcut("execution(* concert.Performance.perform(..)))")
    public void performance(){}

    @Before("performance()")
    public void silenceCellPhones() {
        System.out.println("Silencing cell phones");
    }

    @Before("performance()")
    public void takeSeats() {
        System.out.println("Taking seats");
    }

    @AfterReturning("performance()")
    public void applause() {
        System.out.println("CLAP CLAP CLAP!!!");
    }

    @AfterThrowing("performance()")
    public void demandRefund() {
        System.out.println("Demanding a refund");
    }

}
複製代碼

@AspectJ註解表名了該類是一個切面。 @Pointcut定義了一個類中可重用的切點,寫切點表達式時,若是切點相同,能夠重用該切點。 其他方法上的註解定義了加強被調用的時間,根據註解名能夠知道具體調用時間。

到目前爲止,Audience仍然只是 Spring 容器中的一個 bean。即便使用了 AspectJ 註解,可是這些註解仍然不會解析,由於目前還缺少代理的相關配置。

若是使用 JavaConfig,在配置類的類級別上使用 @EnableAspectJAutoProxy註解啓用自動代理功能。

@Configuration
@EnableAspectJAutoProxy
@ComponentScan
public class ConcertConfig {

    @Bean
    public Audience audience() {
        return new Audience();
    }
   
}
複製代碼

若是使用 xml ,那麼須要引入 <aop:aspectj-autoproxy> 元素。

<?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:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" 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.xsd">

  <context:component-scan base-package="concert"/>

  <aop:aspectj-autoproxy/>

  <bean class="concert.Audience"/>
</beans>
複製代碼

環繞加強

環繞加強就像在一個加強方法中同時編寫了前置加強和後置加強。

@Aspect
public class Audience {

    @Pointcut("execution(* concert.Performance.perform(..)))")
    public void performance(){}

    @Around("performance()")
    public void watchPerformance(ProceedingJoinPoint joinPoint) {
        try {
            System.out.println("Silencing cell phones");
            System.out.println("Taking seats");
            joinPoint.proceed();
            System.out.println("CLAP CLAP CLAP!!!");
        } catch (Throwable throwable) {
            System.out.println("Demanding a refund");
        }
    }
}
複製代碼

能夠看到,這個加強達到的效果與分開寫前置加強與後置加強是同樣的,可是如今全部的功能都位於同一個方法內。 注意該方法接收 ProceedingJoinPoint 做爲參數,這個對象必需要有,由於須要經過它來調用被加強的方法。 注意,在這個方法中,咱們能夠控制不調用 proceed() 方法,從而阻塞對加強方法的訪問。一樣,咱們也能夠在加強方法失敗後,屢次調用 proceed() 進行重試。

加強方法參數

修改 Perform#perform() 方法,添加參數

package concert;

public interface Performance {
    public void perform(int audienceNumbers);
}
複製代碼

咱們能夠經過切點表達式來獲取被加強方法中的參數。

@Pointcut("execution(* concert.Performance.perform(int)) && args(audienceNumbers)))")
    public void performance(int audienceNumbers){}
複製代碼

注意,此時方法接收的參數爲 int 型,args(audienceNumbers) 指定參數名爲 audienceNumbers,與切點方法簽名中的參數匹配,該參數不必定與加強方法的參數名一致。

引入加強

切面不只僅可以加強現有方法,也能爲對象新增新的方法。 咱們能夠在代理中暴露新的接口,當引入接口的方法被調用時,代理會把此調用委託給實現了新接口的某個其餘對象。實際上,就是一個 bean 的實現被拆分到多個類中了。 定義 Encoreable 接口,將其引入到 Performance 的實現類中。

public interface Encoreable {

    void performEncore();

}
複製代碼

建立一個新的切面

@Aspect
public class EncoreableIntroducer {

    @DeclareParents(value = "concert.Performance+",defaultImpl = DefaultEncoreable.class)
    public static Encoreable encoreable;
}
複製代碼

咱們使用了 @AspectEncoreableIntroducer 標記爲一個切面,可是它沒有提供前置、後置或環繞加強。經過@DeclareParents註解將Encoreable接口引入到了 Performance bean 中。

@DeclareParents 註解由三部分組成:

  • value 屬性指定了哪一種類型的 bean 要引入該接口。在上述代碼中,類名後面的 + 號表示是 Performance 的全部子類型,而不是它自己。
  • defaultImpl 屬性指定了爲引入功能提供實現的類。
  • @DeclareParents 註解所標註的靜態屬性指明瞭要引入的接口。

一樣地,咱們在 Spring 應用中將該類聲明爲一個 bean:

<bean class="concert.EncoreableIntroducer" />
複製代碼

Spring 的自動代理機制將會獲取到它的聲明,並建立相應的代理。而後將調用委託給被代理的 bean 或者被引入的實現,具體取決於調用的方法屬於被代理的 bean 仍是屬於被引入的接口。

在 XML 中聲明切面

更新一下 Audience 類,將它的 AspectJ 註解所有移除。

public class Audience {
    
  
    public void silenceCellPhones() {
        System.out.println("Silencing cell phones");
    }

    public void takeSeats() {
        System.out.println("Taking seats");
    }

    public void applause() {
        System.out.println("CLAP CLAP CLAP!!!");
    }

    public void demandRefund() {
        System.out.println("Demanding a refund");
    }

}
複製代碼

聲明前置與後置加強

<?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:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

  <aop:config>
    <aop:aspect ref="audience">
      <aop:before pointcut="execution(* concert.Performance.perform(..))" method="silenceCellPhone"/>

      <aop:before pointcut="execution(* concert.Performance.perform(..))" method="takeSeats"/>

      <aop:after-returning pointcut="execution(* concert.Performance.perform(..))" method="applause"/>

      <aop:after-throwing pointcut="execution(* concert.Performance.perform(..))" method="demandRefund"/>
    </aop:aspect>
  </aop:config>
</beans>
複製代碼

如上所示,就將一個普通方法變爲了加強。 大多數的 AOP 配置元素都必須在 <aop:config> 元素的上下文內使用。元素名基本上都與註解名相對應。 這裏,咱們一樣將同一個切點表達式寫了四遍,將它提取出來。

<?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:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

  <aop:config>
    <aop:aspect ref="audience">
      <aop:pointcut id="performance" expression="execution(* concert.Performance.perform(..))"/>
      <aop:before pointcut-ref="performance" method="silenceCellPhone"/>

      <aop:before pointcut-ref="performance" method="takeSeats"/>

      <aop:after-returning pointcut-ref="performance" method="applause"/>

      <aop:after-throwing pointcut-ref="performance" method="demandRefund"/>
    </aop:aspect>
  </aop:config>
</beans>
複製代碼

注意,此時 <aop:pointcut> 標籤位於 <aop:aspect> 下層,故只能在該切面中引用。若是想要一個切點可以被多個切面引用,能夠將 <aop:aspect> 元素放在 <aop:config> 下第一層。

環繞加強

定義環繞加強方法

public class Audience {

    public void performance(int audienceNumbers){}

    public void watchPerformance(ProceedingJoinPoint joinPoint) {
        try {
            System.out.println("Silencing cell phones");
            System.out.println("Taking seats");
            joinPoint.proceed();
            System.out.println("CLAP CLAP CLAP!!!");
        } catch (Throwable throwable) {
            System.out.println("Demanding a refund");
        }
    }
}
複製代碼

在 xml 中使用 <aop:around> 指定方法名與切點便可。

<?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:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

  <aop:config>
    <aop:aspect ref="audience">
      <aop:pointcut id="performance" expression="execution(* concert.Performance.perform(..))"/>

      <aop:around pointcut-ref="performance" method="watchPerformance"/>
    </aop:aspect>
  </aop:config>
</beans>
複製代碼

爲加強傳遞參數

獲取參數主要就在於切點表達式。

<aop:pointcut id="performance" expression="execution(* concert.Performance.perform(int)) and args(audienceNumbers)"/>
複製代碼

這樣能在 xml 中定位到一個參數類型爲 int ,參數名爲 audienceNumbers 的切點。 注意在 xml 中使用了 and 代替 &&(在 XML 中,&符號會被解析爲實體的開始)。

引入加強

<aop:declare-parents types-matching="concert.Performance+"
        implement-interface="concert.Encoreable"
        default-impl="concert.DefaultEncoreable"/>
複製代碼

types-matching指定了要匹配的類型,與註解中的 value 值功能相同。

注入 AspectJ 切面

AspectJ 切面提供了 Spring AOP 所不能支持的許多類型的切點。 切面頗有可能依賴其餘類來完成它們的工做。咱們能夠藉助 Spring 的依賴注入把 bean 裝配進 AspectJ 切面中。

建立一個新切面。

public aspect CriticAspect {

    private CriticismEngine criticismEngine;

    public CriticAspect() {
    }

    pointcut performance():execution(* perform(..));

    afterReturning() : performance() {
        System.out.println(criticismEngine.getCriticism());
    }

    public void setCriticismEngine(CriticismEngine criticismEngine) {
        this.criticismEngine = criticismEngine;
    }
}
複製代碼

注入的 CritismEngine 的實現類

public class CriticismEngineImple implements CriticismEngine {

    public CriticismEngineImple() {
    }

    public String getCriticism() {
        int i = (int) (Math.random() * criticismPool.length);
        return criticismPool[i];
    }
    
    private String[] criticismPool;

    public void setCriticismPool(String[] criticismPool) {
        this.criticismPool = criticismPool;
    }
}
複製代碼

CriticAspect主要做用是在表演結束後爲表演發表評論。 實際上,CriticAspect 是調用了 CriticismEngine的方法來發表評論。經過 setter 依賴注入爲 CriticAspect 設置 CriticismEngine

切面注入

在配置文件中將 CriticismEngine bean 注入到 CriticAspect 中。

<bean class="om.springinaction.springidol.CriticAspect" factory-method="aspectOf">
   <property name="criticismEngine" ref="criticismEngine"/>
 </bean>
複製代碼

通常狀況下,Spring bean 由 Spring 容器初始化,可是 AspectJ 切面是由 AspectJ 在運行期建立的。因此在運行期間,AspectJ 建立好了 CriticAspect 實例,每一個 AspectJ 都會提供一個靜態的 aspectOf() 方法,返回切面的的單例。 使用factory-method 調用 aspectOf()方法向 CriticAspect 中注入 CriticismEngine

相關文章
相關標籤/搜索