Spring之面向切面

麥冬發表於2017-12-25

面向切面的Spring

  • 面向切面程式設計的基本原理
  • 通過POJO建立切面
  • 使用@AspectJ註解
  • 為AspectJ切面注入依賴

定義AOP術語

  • 通知(Advice)

    • 前置通知(Before):在目標方法被呼叫之前呼叫通知功能
    • 後置通知(After):在目標方法完成之後呼叫通知,此時不會關心方法的輸出是什麼
    • 返回通知(After-returning):在目標方法成功執行之後呼叫通知
    • 異常通知(After-throwing):在目標方法丟擲異常後呼叫通知
    • 環繞通知(Around):通知包裹了被通知的方法,在被通知的方法呼叫之前和呼叫之後執行自定義的行為
  • 連線點(Join point),是在應用執行過程中能夠插入切面的一個點
  • 切點(Poincut),利用正規表示式定義所匹配的類和方法名稱來指定切點
  • 切面(Aspect),通知和切點的結合,它是什麼,在何時何處完成其功能
  • 引入(Introduction):允許向現有的類新增新方法或屬性,不影響現有的類
  • 織入(Weaving):

    • 把切面應用到目標物件並建立新的代理物件的過程
    • 在指定的連線點被織入到目標物件中
    • 在目標物件的生命週期裡有多個點可以進行織入:

      • 編譯期:切面在目標類編譯時被織入,如AspectJ
      • 類載入期:切面在目標類載入到JVM時被織入,如AspectJ 5的載入時織入(load-time weaving,LTW)
      • 執行期:切面在應用執行的某個時刻被織入,如Spring AOP

Spring對AOP的支援

  • 基於代理的經典Spring AOP
  • 純POJO切面
  • @AspectJ註解驅動的切面
  • 注入式AspectJ切面(適用於Spring各版本)
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.DeclareParents;

/**
 * 是一個切面,沒有提供前置、後置或環繞通知,而是使用了@DeclareParents註解
 * @DeclareParents註解詳解:
 * value屬性指定了哪種型別的bean要引入的介面,"+"表示Performance的所有子型別
 * defaultImpl屬性指定了引入功能提供實現的類
 * @DeclareParents註解所標註的靜態屬性指明瞭要引入了介面
 * 
 * 在Spring應用中把EncoreableIntroducer宣告為一個bean,當Spring發現@Aspect註解時會建立一個代理,然後將呼叫委託給被代理的bean或被引入的實現
 * 
 */
@Aspect
public class EncoreableIntroducer {

    @DeclareParents(value="com.leaf.u_spring.chapter04.Performance+",
            defaultImpl=DefaultEncoreable.class)
    public static Encoreable encoreable;
    
    
    
}

通過切點來選擇連線點

  • AspectJ的切點表示式語言

    • AspectJ指示器和描述
    • arg(),限制連線點匹配引數為指定型別的執行方法
    • @args(),限制連線點匹配引數由指定註解標註的執行方法
    • execution(),用於匹配是連線點的執行方法
    • this(),限制連線點匹配AOP代理的bean引用為指定型別的類
    • target(),限制連線點匹配目標物件為指定型別的類
    • @target(),限制連線點匹配特定的執行物件,這些物件對應的類要具有指定型別的註解
    • within(),限制連線點匹配指定的型別
    • @within(),限制連線點匹配指定註解所標註的型別(當使用Sping AOP時,方法定義在由指定的註解所標註的類裡)
    • @annotation,限定匹配帶有指定註解的連線點
  • 編寫切點 Performance
import java.util.Map;

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

import com.google.common.collect.Maps;

/**
 * 使用引數化的通知來記錄磁軌播放的次數
 */
@Aspect
public class TrackCounter {

    private Map<Integer, Integer> trackCounters = Maps.newHashMap();
    
    /**
     * 通知playTrack()方法
     * 
     * 
     * playTrack(int)指定int型別引數
     * && args(trackNumber)指定引數
     * 
     * 
     * @param trackNumber
     */
    @Pointcut("execution(* com.leaf.u_spring.chapter02.CompactDisc.playTrack(int)) && args(trackNumber)")
    public void trackPlayed(int trackNumber){}
    
    /**
     * 播放前為磁軌計數
     * @param trackNumber
     */
    @Before("trackPlayed(trackNumber)")
    public void countTrack(int trackNumber){
        int currentCount = getPlayCount(trackNumber);
        trackCounters.put(trackNumber, currentCount + 1);
    }

    public int getPlayCount(int trackNumber) {
        return trackCounters.containsKey(trackNumber)?trackCounters.get(trackNumber):0;
    }
    
}

import java.util.List;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

import com.google.common.collect.Lists;
import com.leaf.u_spring.chapter02.CompactDisc;
import com.leaf.u_spring.chapter03.BlankDisc;


/**
 * 將BlankDisc和TrackCounter定義為bean,並啟用AspectJ自動代理
 *
 */
@Configuration
@EnableAspectJAutoProxy
public class TrackCounterConfig {

    @Bean
    public CompactDisc sgtPeppers(){
        BlankDisc cd = new BlankDisc();
        cd.setTitle("陽光總在風雨後");
        cd.setArtist("許美靜");
        
        List<String> tracks = Lists.newArrayList();
        tracks.add("陽光總在風雨後");
        tracks.add("成都");
        tracks.add("一生所愛");
        tracks.add("我的中國心");
        tracks.add("Alone Yet Not Alone");
        cd.setTracks(tracks);
        
        return cd;
    }
    
    @Bean
    public TrackCounter trackCounter(){
        return new TrackCounter();
    }
    
}

使用註解建立切面

  • Spring使用AspectJ註解來宣告通知方法

    • @After 通知方法會在目標方法返回或丟擲異常後呼叫
    • @AfterReturning 通知方法會在目標方法返回後呼叫
    • @AfterThrowing 通知方法會在目標方法丟擲異常後呼叫
    • @Around 通知方法會將目標方法封裝起來
    • @Before 通知方法會在目標方法呼叫之前執行
  • 環繞通知是最為強大的通知型別
  • Java不是動態語言,編譯之後很難再新增新的功能,但是引入AOP,切面可以為Spring bean新增新方法,例EncoreableIntroducer
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

/**
 * 演出效果 
 * 
 * @Aspect註解表明Audience類不僅僅是個POJO,還是一個切面
 * Audience類中的方法都使用註解來定義切面的具體行為
 */
@Aspect
public class Audience {

    /**
     * 表演之前
     */
    @Before("execution(** com.leaf.u_spring.chapter04.Performance.perform(..))")
    public void silenceCellPhones(){
        System.out.println("silence Cell Phones");
    }
    
    /**
     * 表演之前
     */
    @Before("execution(** com.leaf.u_spring.chapter04.Performance.perform(..))")
    public void takeSeates(){
        System.out.println("taking seates");
    }
    
    
    /**
     * 表演之後
     */
    @AfterReturning("execution(** com.leaf.u_spring.chapter04.Performance.perform(..))")
    public void applause(){
        System.out.println("CLAP CLAP CLAP!!!");
    }
    
    /**
     * 表演失敗之後
     */
    @AfterThrowing("execution(** com.leaf.u_spring.chapter04.Performance.perform(..))")
    public void demandRefund(){
        System.out.println("Demanding a refund");
    }
    
}

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.aspectj.lang.annotation.Pointcut;

/**
 * 演出效果 
 * 相同的切點表示式重複了4遍
 * @Ponitcut註解能夠在一個@AspectJ切面內定義可重用的切點
 * 
 */
@Aspect
public class Audience2 {

    /**
     * 定義命名的切點
     */
    @Pointcut("execution(** com.leaf.u_spring.chapter04.Performance.perform(..))")
    public void performance(){}
    
    /**
     * 表演之前
     */
    @Before("performance())")
    public void silenceCellPhones(){
        System.out.println("silence Cell Phones");
    }
    
    /**
     * 表演之前
     */
    @Before("performance())")
    public void takeSeates(){
        System.out.println("taking seates");
    }
    
    
    /**
     * 表演之後
     */
    @AfterReturning("performance())")
    public void applause(){
        System.out.println("CLAP CLAP CLAP!!!");
    }
    
    /**
     * 表演失敗之後
     */
    @AfterThrowing("performance())")
    public void demandRefund(){
        System.out.println("Demanding a refund");
    }
    
}
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

/**
 * 演出效果 
 * 建立環繞通知
 * 
 */
@Aspect
public class Audience3 {

    /**
     * 定義命名的切點
     */
    @Pointcut("execution(** com.leaf.u_spring.chapter04.Performance.perform(..))")
    public void performance(){}
    
    /**
     * 環繞通知方法
     */
    @Around("performance())")
    public void watchPerformance(ProceedingJoinPoint jp){
        try {
            System.out.println("silence Cell Phones");
            System.out.println("taking seates");
            //不呼叫proceed方法,通知會阻塞對被通知方法的呼叫
            jp.proceed();
            System.out.println("CLAP CLAP CLAP!!!");
        } catch (Throwable e) {
            System.out.println("Demanding a refund");
        }
    }
    
}
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@EnableAspectJAutoProxy            //啟用AspectJ自動代理    XMl-->   <aop:aspectj-autoproxy/>
@ComponentScan
public class ConcertConfig {

    /**
     * 宣告Audience bean
     * @return
     */
    @Bean
    public Audience audience(){
        return new Audience();
    }
}

在XML中宣告註解

  • 原則:基於註解的配置優於基於Java的配置,基於Java的配置要優於基於XML的配置
  • 需要宣告切面,又不能為通知類新增註解時,須轉向XML的配置
  • Spring的AOP配置能夠以非侵入式的方式宣告切面:

    • <aop:advisor>:定義AOP通知器
    • <aop:after>:定義AOP後置通知(不管被通知的方法是否執行成功)
    • <aop:after-returning>:定義AOP返回通知
    • <aop:after-throwing>:定義AOP異常通知
    • <aop:around>:定義AOP環繞通知
    • <aop:aspect>:定義一個切面
    • <aop:aspectj-autoproxy>:啟用@AspectJ註解驅動的切面
    • <aop:before>:定義一個AOP前置通知
    • <aop:config>:頂層的AOP配置元素,大多數的<aop:*>元素必須包含在<aop:config>元素內
    • <aop:declare-parents>:以透明的方式為被通知的物件引入額外的介面
    • <aop:pointcut>:定義一個切點

引用:《Spring In Action 4》第4章

相關文章