天天看點

Spring系列(四) 面向切面的Spring

除了IOC外, AOP是Spring的另一個核心. Spring利用AOP解決應用橫切關注點(cross-cutting concern)與業務邏輯的分離, 目的是解耦合. 橫切關注點是指散布于代碼多處的同一種功能, 比如日志, 事務, 安全, 緩存等.

AOP程式設計的基本概念

在OOP中, 如果要複用同一功能, 一般的做法是使用繼承或委托. 繼承容易導緻脆弱的對象體系, 而委托實作起來比較麻煩, 需要對委托對象進行複雜調用. AOP提供了另外一種思路, 使用AOP我們仍然可以在一個地方定義功能, 并通過聲明的方式告知何處以哪種方式使用這個功能. 這樣我們既可以對功能做統一管理和維護, 同時也簡化了業務邏輯子產品, 使其更關注自身的業務邏輯. 此外, AOP還可以将新的功能行為添加到現有對象.

Spring中AOP的術語

  • 切面(Aspect): 切面定義了橫切關注點的功能以及使用該功能的聲明. 它包含了另外兩個術語, 通知(Advice, 功能邏輯代碼)和切點(Pointcut,聲明). 切面定義了它是什麼(what), 以及在何時何處(when,where)完成其功能.
  • 通知(Advice): 通知定義了切面的具體功能, 以及何時使用.

when,何時使用? 前置(Before), 後置(After), 傳回(After-returning), 異常(After-throwing), 環繞(Around)

  • 切點(Pointcut): 定義了切面定義的功能在哪裡(Where)發生作用, 看起來就像從某個點把切面插入進去一樣. 切點應該屬于連接配接點中的一個或多個.
  • 連接配接點(Join point): 定義了程式執行過程中可以應用切面的具體時機, 比如方法調用前, 調用後, 結果傳回時, 異常抛出時等, 通常某個具體切面隻會選擇其中一個或幾個連接配接點作為切點.
  • 引入(Introduction): 為現有的類添加新的方法或屬性叫引入.
  • 織入(Weaving): 織入是把切面應用到目标對象并建立新代理對象的過程.

織入的方式有三種:

  1. 編譯期: 需要特殊的編譯器支援, 如AspectJ的織入編譯器
  2. 類加載期: 需要特殊的類加載器ClassLoader
  3. 運作時: Spring AOP 使用該方式織入. AOP容器為對象動态建立一個代理對象.

Spring 對 AOP的支援

Spring對AOP的支援很多借鑒了AspectJ的方式.

Spring支援四種方式的織入:

  1. 基于代理的經典AOP; (方式太老舊, 不建議使用)
  2. 純POJO切面;(需要XML配置)
  3. @AspectJ 注解驅動的切面; (沒啥說的,很好用)
  4. 注入式AspectJ切面;
  • 前三種都是基于動态代理實作, 是以Spring對AOP的支援局限于方法攔截. 如果前三種滿足不了需求(比如攔截構造器方法或者字段修改), 可以使用第四種.
  • 與AspectJ不同, Spring的切面就是Java類, Spring使用運作時動态代理, 而AspectJ需要學習特殊的文法以支援特殊的編譯器織入.

通過切點來選擇連接配接點

Spring 借鑒了AspectJ的切點表達式語言. 如前所述, Spring基于動态代理,隻能在方法上攔截, 是以Spring隻支援這個層面的表達式來定義.

spring支援的AspectJ訓示器如下, 其中execution來執行比對, 其他均為限制比對的.

Spring系列(四) 面向切面的Spring

切點表達式更多使用可以參考

官方文檔
  • spring新增了個bean()訓示器

使用注解建立切面

一. 定義切面類, 并用

@Aspect

注解, 該注釋用來标記這個類是個切面

二. 定義切面的方法(what), 并使用注解标記方法(when), 可用的注解:

@Before

,

@After

@AfterReturning

@AfterThrowing

@Around

(功能最強大,後面将單獨使用這種通知)

一,二步完成後的代碼:

@Aspect
public class Audience{

    @Before("execution(** com.xlx.Performance.perform(...))")
    public void silencephone(){
        System.out.println("silencephone");
    }

    @Before("execution(** com.xlx.Performance.perform(...))")
    public void takeSeats(){
        System.out.println("takeSeats");
    }

    @AfterReturning("execution(** com.xlx.Performance.perform(...))")
    public void applause(){
        System.out.println("applause");
    }

    @AfterThrowing("execution(** com.xlx.Performance.perform(...))")
    public void refund(){
        System.out.println("refund");
    }
}           

上面的代碼中切面表達式被重複定義了四次, 無論如何這已經是重複代碼了, 下一步優化一下.

三. 使用注解

@Pointcut

定義切點

@Aspect
public class Audience{

    //定義切點并修改其他方法重用該切點
    @Pointcut("execution(** com.xlx.Performance.perform(...))")
    public void performance(){

    }

    @Before("performance()")
    public void silencephone(){
        System.out.println("silencephone");
    }

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

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

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

@Aspect

注解的類依然是個普通java類, 它可以被裝配為bean

@Bean
public Audience getAudience(){
    return new Audience();
}           

四. 使用

@EnableAspectJAutoProxy

注解啟用自動代理功能, 如果是XML Config ,對應的節點是

<aop:aspectj-autoproxy />

@Configuration
@ComponentScan // 包掃描
@EnableAspenctJAutoProxy // 啟動自動代理
public class MyConfig{

    // 如果Audience上加了@Component就不需要這個代碼了
    @Bean
    public Audience getAudience(){
        return new Audience();
    }
}           

五. 使用環繞通知

@Around

, 環繞通知同時兼具了

@Before

@After

... 等注解的方法的功能, 下面代碼示範了這種能力. 如可以使用它記錄方法執行時長.

@Aspect
public class Audience{

    //定義切點并修改其他方法重用該切點
    @Pointcut("execution(** com.xlx.Performance.perform(...))")
    public void performance(){

    }

    @Around("performance()")
    public void silencephone(ProcdedingJoinPoint jp){
        System.out.println("silencephone");
        System.out.println("takeSeats");
        try{
            // 如果不是刻意為之, 一定要記得調用jp.proceed();否則實際的方法Performance.perform()将會阻塞
            jp.proceed();
            System.out.println("applause");
        }catch(Exception e){
            System.out.println("refund");
        }
    }
}           

六. 參數傳遞 , 在切點表達式中使用

args(paramName)

結合切點方法可以為切面方法傳遞參數

@Aspect
public class Audience{

    //定義切點并修改其他方法重用該切點
    @Pointcut("execution(** com.xlx.Performance.perform(int) && args(actornum)))")
    public void performance(int actornum){

    }

    @Before("performance(actornum)")
    public void countActor(int actornum){
        System.out.println("countActor"+actornum);
    }
}           

通過注解引用新功能

除了攔截對象已有的方法調用, 還可以使用AOP來為對象添加新的屬性和行為(引入). 其實作就是通過動态代理生成代理類來實作.

一. 定義要添加的功能接口

public interface Encoreable{}           

二. 定義切面(引入)

@Aspect

注解切面類.

@DeclareParents

注解功能接口靜态變量

@Aspect
public class EncoreableIntroducer{
    // 可以解釋為: 為Performace的所有子類引入接口Encoreable, 并使用預設實作類DefaultEncoreableImpl
    @DeclareParents(value="xlx.Performace+",defaultImpl=DefaultEncoreableImpl.class)
    public static Encoreable encoreable;
}           

基于XML配置的切面

如果沒有辦法為類添加注解, 比如沒有源代碼, 那就不得不使用xml來配置了.

  • 示例1
<aop:config>
    <aop:aspect ref="aspectBean">
        <aop:pointcut id="pcId" expression="execution(** com.xlx.Performance.perform(int) and args(actornum)))" />
        <aop:before pointcut-ref="pcId" method="count" />
    </aop:aspect>
</aop:config>           
  • 示例2
<aop:config>
    <aop:aspect>
        <aop:declare-parents type-matching="xlx.Performace+" implement-interface="xlx.Encoreable" delegate-ref="defaultImpl" />
    </aop:aspect>
</aop:config>           

AspectJ 注入

使用AspectJ注入的方式可以解決使用動态代理無法解決的問題(應該比較少見,大多應用使用Spring AOP就可以實作了), 但需要使用AspectJ的特殊文法. 定義好的類需要用xml配置為bean, 使用

factory-method="aspectOf"

屬性來制定bean的産生方式.

<bean factory-method="aspectOf" class="...ClassName">
    <property name="other" ref="otherref"/>
</bean>