面向切面的Spring


在軟件開發中,散布於應用中多處的功能被稱為橫切關注點。通常來說,這些橫切關注點從概念上是與應用的業務邏輯相分離的。把這些橫切關注點與業務邏輯相分離正是面向切面編程(AOP)所要解決的問題。DI有助於應用對象之間的解耦,而AOP可以實現橫切關注點與他們所影響的對象之間解耦。

AOP的術語

切面(Aspect)

橫切關注點可以被模塊化為特殊的類,這些類可以稱為切面(aspect)。這樣做有兩個好處:首先,現在每個關注點都集中於一個地方,而不是分散到多處代碼中;其次,服務模塊更加簡潔,因為它們主要關注業務代碼,而次要關注的代碼被移入切面中。

通知(Advice)

在AOP術語中,切面的工作就被稱為通知。除了描述切面要完成的工作,通知還解決了何時執行這個工作的問題。Spring切面可以應用5種類型的通知:

  • 前置通知(Before):在目標方法被調用之前調用通知功能
  • 后置通知(After):在目標方法完成之后調用通知,不關心方法的輸出是什么
  • 返回通知(After-returning):在目標方法成功執行之后調用
  • 異常通知(After-throwing):在目標方法拋出異常之后調用
  • 環繞通知(Around):通知包裹被通知方法,在被通知的繁華調用之前和之后執行自定義的行為

連接點(Join point)

連接點是應用執行過程中能夠插入切面的一個點,這個點可以是調用方法時,拋出異常時,甚至修改一個字段時。切點代碼可以利用這些點插入到應用的正常流程中,並添加新的行為。

切點(Poincut)

一個切點並不需要通知應用的所有連接點,切點有助於縮小切面所通知的連接點的范圍。切點的定義,我們需要使用明確的類和方法名稱或者利用正則表達式來指定切點(切點表達式)

織入(Weaving)

織入是把切面應用到目標對象並創建新的代理的過程。切面在指定的連接點被織入到目標對象中。目標對象的生命周期你有多個階段可以被織入:

  • 編譯期:切面在目標類編譯階段被織入。這種方式需要特殊的編譯器。
  • 類加載期:切面在目標類加載到JVM時被織入。這種方式需要特殊的類加載器。
  • 運行期:切面在應用運行的某個階段被織入。一般情況下,在織入切面時,AOP容器會為目標對象動態創建一個代理對象。

AspectJ這三種方式方式都支持,Spring AOP只支持在運行期織入切面。

引入(introduction)

AOP的作用就在於增強目標對象現有的屬性或方法,而引入允許我們向現有的類中添加新方法或屬性。

Spring 對AOP的支持

並不是所有AOP框架都是相同的,它們在連接點模型上可能有強弱之分,它們織入切面的方式和時機也會有不同。但是無論如何,創建切點來定義切面所織入的連接點是所有AOP框架的基本功能。Spring AOP構建在動態代理基礎上,因此,Spring對AOP的支持局限於方法攔截,這是Spring作為AOP框架的局限性。

如果AOP的需求超過了簡單的方法調用(如構造器或屬性攔截),那么就需要考慮使用AspectJ來實現切面。

Spring只支持方法級別的連接點

Spring基於動態代理,所以Spring只支持方法級別的連接點,缺少對字段連接點的支持,無法讓我們創建細顆粒度的通知,例如攔截對象字段的修改;而且它不支持構造器連接點,我們無法在bean創建的時候應用通知。

雖然方法攔截可以滿足大部分的需求,但要攔截其他,就需要利用AspectJ來補充Spring AOP的功能。

通過切點來選擇連接點

在Spring AOP中,要使用AspectJ的切點表達式語言來定義切點。AspectJ的切點指示器只有execution是實際用於執行匹配的,其他的只是限制匹配的。execution指示器是我們在編寫切點表達式時最主要使用的。

編寫切點

execution(返回類型 全限定的類.方法(參數))

例如: execution(* cn.lynu.Performance.perform(..))

表達式以"*"號開始,表明可以返回任意類型,然后我們使用全限定的類名和方法名,對於方法參數列表,我們使用兩個點號(..)表明該方法可以使用任意入參

我們還可以使用限制的指示器來匹配,例如使用execution()和within()限制切點。使用的是“&&”操作符進行連接:

 execution(* cn.lynu.Performance.perform(..) && within(cn.*))

類似的還可以使用“||”運算符表示或的關系,“!”運算符表示非的關系。

但是因為“&&”在XML中有特殊含義,Spring的XML配置里面使用切點可以使用and替代“&&”,or和not分別替代“||” 和“!”.

使用Java代碼定義切面

使用AspectJ的@Aspect注解表明一個Jav類作為切面,這個類中的方法都可以使用注解來定義切面的具體行為。AspectJ使用5個注解來對應5中通知方式:

  • @After 后置通知
  • @Before 前置通知
  • @AfterReturning 返回通知
  • @AfterThrowing 異常通知
  • @Around 環繞通知

所有的這些通知注解都可以使用一個切點表達式作為它的值。

@Aspect
public class Audience {
    
    @Before("execution(* test04.Performance.perform(..))")
    public void silenceCellPhone() {
        System.out.println("將手機調置靜音");
    }
    
    @Before("execution(* test04.Performance.perform(..))")
    public void taskSeats() {
        System.out.println("觀眾就坐");
    }
    
    @AfterReturning("execution(* test04.Performance.perform(..))")
    public void applause() {
        System.out.println("鼓掌");
    }
    
    @AfterThrowing("execution(* test04.Performance.perform(..))")
    public void demandRefund() {
        System.out.println("表演失敗,觀眾要求退款");
    }

}

如果所有的這些切點表達式都是相同的,我們可以使用@Pointcut注解定義個可重用的切點:

    @Pointcut("execution(* test04.Performance.perform(..))")
    public void performance() {}
    
    @Before("performance()")
    public void silenceCellPhone() {
        System.out.println("將手機調置靜音");
    }

performance方法是一個空方法,其本身只是作為一個標識,供@Pointcut注解依附。其實這個已經是切面的Audience,我們依然可以像其他Java類那樣使用它的方法,它的方法也可以獨立地進行測試,這與其他Java類並沒有什么不同。只是使用了@Aspect注解,並不會被視為切面,這些注解也不會解析,也不會轉換為切面的代理,還需要啟動自動代理功能。

如果使用的是JavaConfig的話,可以在配置類的類級別上使用@EnableAspectJAutoProxy注解啟用:

@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackageClasses= {Performance.class})
public class Config {
    @Bean
    public Audience audience() {
        return new Audience();
    }
}    

如果使用XML來裝配bean,就需要使用aop命名空間的<aop:aspect-autoproxy>元素

<!--啟用AspectJ自動代理-->
<aop:aspectj-autoproxy />

<bean class="cn.Audience"/>

不要忘了將切面聲明為一個Spring bean,不論是用JavaConfig還是XML。

雖然我們使用了AspectJ的注解來創建切面,但是這個切面依然是基於代理的,它依然是Spring基於代理的切面,仍然受限於代理方法的調用,並不能利用AspectJ所有的能力。

接下來,我們可以測試這個切面的效果了:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes= {Config.class})
public class Test {

    @Autowired
    private Performance performance;
    
    @org.junit.Test
    public void test01() {
        performance.perform();
    }
}

創建環繞通知

環繞通知是最為強大的通知類型,它能夠讓所編寫的邏輯將被通知的目標方法完全包裝起來,事實上就像在一個通知方法中同時編寫前置和后置通知。

@Aspect
public class Audience {
    
    @Pointcut("execution(* test04.Performance.perform(..))")
    public void performance() {}
    
    //環繞通知
    @Around("performance()")
    public void watchPerformance(ProceedingJoinPoint pj) {
        try {
            System.out.println("關閉手機");
            System.out.println("就坐");
            //調用被通知方法
            pj.proceed();
            System.out.println("鼓掌");
        } catch (Throwable e) {
            System.out.println("表演失敗,觀眾要求退款");
        } 
    }
}

注意環繞通知方法的參數是ProceedingJoinPoint作為入參的,這個對象是必須的,因為需要在環繞通知方法中通過它來調用目標方法,使用的是它的proceed()方法,不要忘記調用這個方法,如果不調這個方法,則會阻塞被通知方法的調用。

處理通知中的參數

切面所通知的法拉伐確實有參數該怎么辦?如何在切面中訪問和使用傳遞給被通知方法的參數?

我們換一個有參數的切點,並改造切點表達式:

@Pointcut("execution(* cn.lynu.CompactDisc.playTrack(int)) && args(trackNumber)")
public void trackPlayed(int trackNumber){}

@Before("trackPlayed(trackNumber)")
public void before(int trackNumber){
  system.out.print(trackNumber);  
}

被通知的方法入參是int類型,並使用args限制器,參數的名稱是與切點方法簽名中的參數名相匹配的。這樣一來,就可以在通知方法中使用傳遞給切點方法的參數了

通過注解引入新功能

之前,我們一直是為目標對象以擁有的方法添加新功能,實際上,利用引入的概念,AOP可以為對象添加新的方法。在Spring中,切面只是實現了它們所包裹的bean所現有接口的代理。如果這些代理可以暴露新的接口,那么目標類看起來也實現了新的接口,即使底層實現類並沒有實現這些接口。但調用這個新引入的方法時,代理會把調用傳遞給實現了新接口的某個對象。

@Aspect
public class EncoreableIntroducer {
    @DeclareParents(value="test04.Performance+",defaultImpl=DefaultEncorable.class)
    public static Encoreable encoreable;
}

通過@DeclareParents注解,將Encoredable接口引入到Performance bean中。這個注解有三個部分組成:value屬性指定了哪種類型bean要引入該接口,標記符后面的加號表示是Performance的所有子類型,而不是Performance本身。defaultImpl屬性指定為引入功能提供實現的類。注解所標注的靜態屬性指明了要引入的接口。

接下來,我們可以測試調用這個引入的新方法:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes= {Config.class})
public class Test {

    @Autowired
    private Performance performance;
    
    @org.junit.Test
    public void test01() {
        //需要先強轉為引入的接口類型,再調用新方法
        Encoreable encoreable=(Encoreable) performance;
        encoreable.performEncore();
    }
    
}

運行之后,可以正常使用這個新方法,如果不通過引入的方法,直接強轉會出現ClassCaseException。

使用注解的方式真的太方便了,但是這種方式由一個明顯的缺點:必須可以看到和修改源碼。如果沒有源碼,或不想將AspectJ的注解放在代碼中,我們就需要使用XML的方式。

在XML中聲明切面

在Spring aop命名空間中,提供了多個元素用在XML中聲明切面:

AOP配置元素 用途
<aop:advisor> 定義AOP通知器
<aop:after> 定義AOP后置通知
<aop:after-returning> 定義AOP返回通知
<aop:after-throwing> 定義AOP異常通知
<aop:around> 定義環繞通知
<aop:aspect> 定義切面
<aop:before> 定義前置通知
<aop:aspectj-autoproxy> 這個之前就見過,是為啟用@Aspec注解
<aop:config> 頂層到的AOP配置,大多數<aop:*>元素必須包裹在<aop:config>元素內
<aop:declare-parents> 引入
<aop:pointcut> 定義切點
<bean id="audience" class="test04.Audience"></bean>    

<aop:config>
     <aop:aspect ref="audience">
         <aop:pointcut expression="execution(* test04.Performance.perform(..))" id="pointcut"/>
         <aop:before pointcut-ref="pointcut" method="silenceCellPhone"/>
         <aop:before pointcut-ref="pointcut" method="taskSeats"/>
         <aop:after-returning pointcut-ref="pointcut" method="applause"/>
         <aop:after-throwing pointcut-ref="pointcut" method="demandRefund"/>
         <aop:around pointcut-ref="pointcut" method="watchPerformance"/>
     </aop:aspect>
    </aop:config>

關於Spring AOP配置元素,注意的是大多數AOP配置元素必須在<aop:config>元素上下文內使用.這里使用<aop:pointcut>將相同的切點抽取出來,如果通知的切點不一致,在通知中使用pointcut屬性而不是pointcut-ref。<aop:pointcut>元素還可以放在<aop:config>元素范圍內,提供其他切面使用。

為通知傳遞參數

在AspectJ注解的方式中,我們可以獲得目標方法的參數,使用XML的方式也可以:

<aop:pointcut expression="execution(* test04.CompactDisc.playTrack(int)) and args(trackNumber)" id="pointcut"/>

只不過在XML中用and or not表示與或非,而不是&& || !

在XML中引入新功能

AspectJ中使用的是@DeclareParents注解,在XML中對應的就是Spring aop命名空間中的<aop:declare-parents>元素:

    <bean id="audience" class="test04.Audience"></bean>
    <aop:config>
     <aop:aspect ref="audience">
         <aop:declare-parents types-matching="test04.Performance+" implement-interface="test04.Encoreable" delegate-ref="myPerformance"/>
     </aop:aspect>
    </aop:config>

最后再說一點,相比較AspectJ,SpringAOP只局限與對方法的增強,AOP的功能較弱,如果需要對對於構造器,屬性等類型的切點,就需要直接使用AspectJ。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM