《精通Spring4.x》閱讀筆記(二)- SpringAOP讀這一篇就夠了

基本概念

AOP(Aspect Oriented Programming): 面向切面編程,將重複性的橫切性質邏輯模塊化,織入到目標對象中。

產生背景

在程序設計中,會遇到一些不能通過縱向繼承解決的重複代碼,比如事務控制、性能監控等,這些代碼並非業務邏輯所需要關注、卻又不得不摻雜在業務邏輯中,造成了業務程序不夠清晰、簡單,並且需要重複去編寫。爲了解決這個問題,AOP的設計思路獨闢蹊徑,通過抽取這些橫向邏輯到獨立模塊,然後再在編譯、加載或運行時將這些橫向邏輯插入到原業務代碼中,實現了橫向統一邏輯與業務邏輯的解耦,這也是其名稱的由來。

應用場景

AOP有其特定應用場景,不是OOP的替代方案,而是OOP的有益補充。主要應用於橫切性邏輯的處理,包括事務控制、性能監控、訪問控制等等。

實現方案

AOP的實現方案有:

  • AspectJ
  • AspectWerkz(已與AspectJ合併)
  • JBoss AOP
  • Spring AOP
    本文主要是對Spring AOP進行介紹。

上一篇文章我們介紹了Spring IoC的具體細節,而Spring AOP是建立在Spring IoC的基礎上的,與Spring IoC、AspectJ有很好的整合,對AspectJ有部分功能的支持。

相關術語

  1. 連接點(Joinpoint)
    • 連接點是指一個類或者一段程序代碼擁有的一些具有邊界性質的程序執行的特定位置。
    • Spring只支持方法層面的連接點,比如方法執行前、執行後、拋出異常時、方法調用前後等。這些點可以理解爲我們要插入橫切邏輯的候選錨點。
    • 構成:1. 用方法表示的程序執行點;2. 用相對位置表示的方位。Spring中用切點表示前者,後者在增強中定義
  2. 切點(Pointcut)
    • 如前所述,一個程序中有很多連接點,我們用切點定位要插入橫切邏輯的指定連接點。類比一下,可以把連接點看作是數據庫中的具體數據記錄,而切點則可看作查詢語句,用於匹配特定的條目。
    • Spring中切點只定位到了方法上。也就是連接點中的執行點信息,不包括具體的方位
  3. 增強(Advice)
    • 增強是指織入目標類連接點上的一段程序代碼,也就是封裝了我們前文說的橫切邏輯的代碼。
    • Spring中的增強除了包含要織入的代碼外,還包含有定位連接點的方位信息
    • 引介(Introduction)是一類特殊的增強,爲類添加屬性和方法。即使一個原本沒有實現接口的類,通過引介增強也能動態爲該類添加接口的實現邏輯。
  4. 切面(Aspect)
    切面由切點和增強(引介)組成,定義有橫切的邏輯及定位連接點的信息。Spring AOP就是負責實施切面的框架。
  5. 目標對象(Target)
    等待織入橫切邏輯的目標類。即目標連接點附屬的類。
  6. 織入(Weaving)
    將橫切邏輯插入到目標對象中的過程稱爲織入。
    織入方式:
    • 編譯期織入
    • 加載期織入
    • 動態代理織入(運行期)
      編譯期織入需要提供特殊的編譯期,加載期織入需要提供特殊的類加載器。Spring AOP採用動態代理織入方式。AspectJ採用前兩種織入方式。
  7. 代理(Proxy)
    目標類被AOP織入增強後產生的新類,融合有原類和增強邏輯。根據不同代理方式,代理類可能是與目標類實現同一個接口的新類,也可能是目標類的子類。

理解AOP相關的術語是極其重要的,後面的具體實施其實就是定義切面(切點、增強)的過程,即兩個方面的內容:1.如何定位連接點;2.如何編寫增強代碼。

底層技術

Spring AOP底層原理:基於動態代理技術,具體使用JDK或者是CGLib動態代理

JDK的動態代理

適用場景:JDK爲目標類創建實現同一接口的代理對象,適用於接口實現類的代理場景,對於沒有實現接口的類代理則無計可施。

使用步驟:

  1. 定義InvocationHandler接口的實現類
  2. 通過Proxy的newProxyInstance方法創建代理實例
/**
 * InvocationHandler接口的實現類,包含有橫切邏輯
 */
public class PerformaceHandler implements InvocationHandler {
    private Object target;

    public PerformaceHandler(Object target) {
        this.target = target;
    }

    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        PerformanceMonitor.begin(target.getClass().getName() + "." + method.getName());
        Object obj = method.invoke(target, args);
        PerformanceMonitor.end();
        return obj;
    }
}
/**
 * 使用JDK動態代理
 */
@Test
public void proxy() {
    
    // 定義目標類實例
    ForumService target = new ForumServiceImpl();
    // 定義InvocationHandler的具體實現類,其包含有具體的橫切邏輯
    PerformaceHandler handler = new PerformaceHandler(target);
    // 通過Proxy創建代理對象,可強制轉換爲目標類的類型
    ForumService proxy = (ForumService) Proxy.newProxyInstance(target
                    .getClass().getClassLoader(),
            target.getClass().getInterfaces(), handler);
    // 調用代理對象的方法,其中已經包含有橫切邏輯
    proxy.removeForum(10);
    proxy.removeTopic(1012);

}

CGLib的動態代理

適用場景:採用爲目標類生成子類的方式進行代理,對於沒有實現接口的目標類進行代理。
底層技術:基於底層字節碼技術,在子類中採用方法攔截技術攔截所有父類方法的調用並順勢織入橫切邏輯。

/**
 * CGLib動態代理實現
 */
public class CglibProxy implements MethodInterceptor {
    private Enhancer enhancer = new Enhancer();

    public Object getProxy(Class clazz) {
        // 設置需要創建子類的類(目標類)
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(this);
        // 創建子類實例
        return enhancer.create();
    }

    /**
     * 攔截父類所有方法調用
     * @param obj 
     * @param method 
     * @param args
     * @param proxy 
     * @return
     * @throws Throwable
     */
    public Object intercept(Object obj, Method method, Object[] args,
                            MethodProxy proxy) throws Throwable {
        PerformanceMonitor.begin(obj.getClass().getName() + "." + method.getName());
        Object result = proxy.invokeSuper(obj, args);
        PerformanceMonitor.end();
        return result;
    }
}
/**
 * 使用CGLib動態代理
 */
@Test
public void proxy() {
    //使用CGLib動態代理
    CglibProxy cglibProxy = new CglibProxy();
    ForumService forumService = (ForumService)cglibProxy.getProxy(ForumServiceImpl.class);
    forumService.removeForum(10);
    forumService.removeTopic(1023);
}

對比

  1. 相較CGLib,使用JDK動態代理在創建代理對象時性能較高,但執行對象方法時性能不高。
  2. 與之相反,使用CGLib時創建代理對象性能不高,但執行對象方法時性能要高。

使用建議:在代理單例對象或具有實例池的對象時,可採用CGLib動態代理,而使用prototype類型對象代理時,使用JDK動態代理。(在SpringAOP中可通過參數配置)

優劣勢

  • 優勢
    動態代理技術實現AOP功能,不需要額外的Java編譯器或者類加載器,直接依賴JDK或CGLib庫就可完成;
  • 劣勢
    直接使用動態代理,存在一些不足:
    1. 作用範圍上,爲目標類的所有方法都添加了增強邏輯,除了在增強邏輯中添加一些方法名稱或簽名的判斷外,無法實現對某個目標類部分方法的增強;
    2. 通用性上,硬編碼的方式指定具體目標類和增強邏輯,無法實現通用、統一的處理目標;
    3. 連接點定位上,也是採用的硬編碼方式,無法做到通用。

Spring AOP的使用

根據不同場景,Spring AOP有多種使用方式:Advisor、@AspectJ、Schema。

Advisor

實現增強

Spring AOP中的增強包含有連接點的方位信息,同時目前只支持方法層面的增強,因此根據不同方位提供了五種類型的增強接口,通過實現不同的接口得到橫切邏輯不同的觸發時機。
增強主要接口的繼承關係如圖:
AOP增強接口體系

  1. 前置增強:MethodBeforeAdvice
  2. 後置增強:AfterReturningAdvice
  3. 環繞增強:MethodInterceptor
  4. 拋出異常增強:ThrowsAdvice
  5. 引介增強:IntroductionInterceptor
    (接口方法聲明中包括了目標類的各項信息:方法、入參、目標類實例等)

編碼方式使用增強
測試一個增強或通過編碼方式使用增強,可通過ProxyFactory類將增強織入到目標類中創建代理:

@Test
public void before() {
    Waiter target = new NaiveWaiter();
    BeforeAdvice  advice = new GreetingBeforeAdvice();
    ProxyFactory pf = new ProxyFactory();
    //pf.setInterfaces(target.getClass().getInterfaces());
    //pf.setOptimize(true);
    pf.setTarget(target);
    pf.addAdvice(advice);

    Waiter proxy = (Waiter)pf.getProxy(); 
    proxy.greetTo("John");
    proxy.serveTo("Tom");
    System.out.println(proxy.getClass());
}

ProxyFactory類中依賴了AopProxy接口,該接口的實現類有CglibAopProxy、JdkDynamicAopProxy,分別對應不同的代理技術,具體使用哪一個,通過以下策略控制:

  • setInterfaces方法設置代理類要實現的接口,此時使用JDK動態代理;
  • setOptimize方法啓用優化,設置爲true時,忽略interfaces屬性,使用CGLib動態代理
    通過addAdvice方法可添加多個增強,具體增強順序與調用順序一致,也可通過重載方法決定其增強執行的順序。

Spring配置方式使用增強
也可在xml配置文件中爲目標類添加增強生成代理Bean:

<bean id="greetingBefore" class="com.smart.advice.GreetingBeforeAdvice" />
<bean id="target" class="com.smart.advice.NaiveWaiter" />
<bean id="waiter"
        class="org.springframework.aop.framework.ProxyFactoryBean"
        p:proxyInterfaces="com.smart.advice.Waiter" p:target-ref="target"
        p:interceptorNames="greetingBefore" />

配置代理Bean時,class屬性指定爲ProxyFactoryBean類,實際是使用SpringIoC中的FactoryBean接口功能,爲複雜Bean提供靈活的代碼實例化的功能。屬性包括:

  • proxyInterfaces 目標對象實現的接口
  • target 具體目標對象
  • interceptorNames 增強對象信息(Advice或Advisor接口的實現類,可配置多個)

另外,還有singleton、optimize、proxyTargetClass布爾屬性值,分別是定是否爲單例(默認)、啓用CGLib動態代理優化、對類進行代理。

配置好後,啓動Spring容器,從容器獲取對應Bean即爲代理對象的Bean,爲此需要將目標Bean配置爲其他名稱。

後置增強、環繞增強的使用方式大體相似,不同的是接口方法參數定義有所不同,但也都包含了必要的信息。下面簡單介紹下拋出異常增強、引介增強的不同之處。

拋出異常時增強
主要應用於事務管理的場景,在拋出異常的情況下回滾事務。ThrowsAdvice是一個標籤接口,運行時Spring通過反射機制,查找符合以下規則的方法:

  1. 方法名爲afterThrowing;
  2. 前三個入參要麼都提供,要麼都省略,具體有Method method、Object[] args、Object target;
  3. 最後一個入參爲Throwable或其子類,必須提供。

引介增強
引介增強的連接點爲類級別,增強類需要實現目標接口的方法(提供增強的實現,也是目標類動態實現接口的默認實現),同時,直接繼承DelegatingIntroductionInterceptor,覆蓋invoke方法。由於只能通過生成子類的方式創建代理,必須指定proxyTargetClass爲true。

創建切面

前面我們只實現了增強的邏輯,通過Spring提供的FactoryBean爲目標類創建代理,此時爲目標類所有方法織入了橫切邏輯。對應AOP術語,我們尚未指定具體的切點,執行更個性化的增強動作。

切點在Spring中通過Pointcut表示,查看一下接口定義

public interface Pointcut {
    Pointcut TRUE = TruePointcut.INSTANCE;

    ClassFilter getClassFilter();

    MethodMatcher getMethodMatcher();
}

可以看到Pointcut引用了ClassFilter、MethodMatcher兩個接口,通過這兩個接口描述要對哪些目標類進行過濾。其中,MethodMatcher分爲靜態和動態方法匹配,靜態僅對方法簽名進行匹配,而動態則支持檢查運行時方法的入參值,動態匹配對運行時的性能影響很大。靜態、動態可通過isRuntime方法進行區分。

切點劃分

  1. 靜態方法切點
  2. 動態。。。。
  3. 註解切點
  4. 表達式切點
  5. 流程。。
  6. 複合。。

切點類型也體現在了切面類型的劃分中,下面通過介紹切面,使用具體的切點。

切面劃分
切面的概念包含有增強和切點,因爲增強中包含了部分連接點的配置信息、也有橫切代碼邏輯,因此可以將增強也看作一個切面,這也就是在配置代理類時interceptorNames屬性可以直接引用advice的原因

大的分類上,除了只包含Advice的切面外(因爲匹配目標類所有方法,一般不會使用),切面還包括切點切面、引介切面,其中切點切面時最常用的切面類型。切點切面具體有6個具體的實現類,都可以對應到Pointcut上,最常用的切面類型爲DefaultPointcutAdvisor,動態切點、流程切點、複合切點都通過該實現類得到具體的切面。

具體使用上,雖然每種類型的切面屬性上稍有差異,但與只使用advice類似,增加了Advisor的定義、配置,在代理工廠Bean配置時,interceptorNames屬性要指定我們配置的advisorBean即可。

自動代理創建
配置代理時,還有一個痛點,目前只能通過ProxyFactoryBean爲指定的目標類創建代理,如果我們需要創建代理的目標類很多,豈不是要配置很多類似的Bean,即使通過Spring的父子配置簡化,但工作量還是很大的,Spring爲我們提供了三類自動創建代理的策略:

  1. 基於Bean配置名規則創建代理BeanNameAutoProxyCreator(beanNames指定匹配規則)
  2. 基於Advisor匹配機制DefaultAdvisorAutoProxyCreator
  3. 基於Bean中AspectJ註解AnnotationAwareAspectJAutoProxyCreator(@AspectJ使用)

類自身方法代理
書中還介紹類自身方法之間調用時無法通過代理對象執行,只能在目標類中直接調用,也就是無法插入增強的邏輯,書中給出了作者的解決方案,思路是通過注入自身Bean調用自己的方法實現,有興趣的讀者可自行查閱。

@AspectJ

@AspectJ是我們最常使用也是優先使用的方式,其配置的內容與Advisor本質上是相同的,只不過對我們的程序侵入性更低。

定義切面

先看使用@AspectJ定義一個切面的示例:

@Aspect
public class OperationLogAspect {

    @Pointcut("execution(public * com.iic.service.*.*(..)) && @annotation(com.iic.service.OperationLogCatcher)")
    public void recordLog() {
    }

    @Around("recordLog() && @annotation(com.doumi.logmanager.service.oplog.OperationLogCatcher)")
    public Object saveOptLog(ProceedingJoinPoint pjp) throws Throwable {
        Object result = pjp.proceed(); // 執行目標類方法
        // operationLogService.addOperationLog(“記錄操作歷史”);
    }
}

示例中應用了以下幾個註解:

  • @Aspect 表示該類爲切面的定義
  • @Pointcut 定義一個切點
  • @Around 定義環繞增強,對應的方法體爲橫切邏輯的執行

聲明後,類似於ProxyFactory,可以通過AspectJProxyFactory對象編程方式使用生成代理;也可通過配置AnnotationAwareAspectJAutoProxyCreator自動代理創建Bean的方式生成代理Bean(或使用簡潔配置方式:aop:aspectj-autoproxy

切點表達式

切點表達式包括有函數和入參,同時入參支持通配符的使用,表達式之間可通過邏輯運算符構建更復雜的表達式,所支持的切點函數明細如下:
切點表達式函數

增強類型

  • @Before
  • @AfterReturning
  • @Around
  • @AfterThrowing
  • @After(Final增強,相當於try…finally控制,即使拋出異常也會得到執行)
  • @DeclareParents(引介增強)

參數綁定

另外,在對於連接點方法入參、返回值、拋出異常的綁定,本文沒有詳細介紹,簡單來說是可以通過這種機制實現切點函數入參的精簡,需要保證參數名稱的一致,規則引擎會自動解析對應參數的類型。

<aop:aspect>

如果不使用@Aspect註解定義切面,也能通過Schema配置的方式使用切點表達式。在配置文件中應用<aop:aspect ref=“引用增強方法所在的bean”>節點配置切面,其中子節點<aop:before pointcut=“xxx” method=“xxx”>配置切點和增強方位。pointcut屬性定義表達式,method屬性指定具體使用的增強方法。

<aop:advisor>

使用切點表達式的同時,想引用基於Spring增強接口實現類的方式配置Advisor可使用<aop:advisor>。

小結

Spring AOP爲我們提供了多種配置、使用的方式,有基於實現接口的、基於註解配置以及基於XML Schema。新項目中,可採用簡潔的註解配置的方式,爲了對老項目兼容,可採用Schema的配置。由於考慮到實現Spring提供的接口導致一定程度的代碼與框架耦合,所以一般不採用基於Advisor類的配置方式,但有一種情況除外:基於ControlFlowPointcut的流程切面只能使用基於Advisor類的方式。

雖然配置形式很多,但本質上需要指定的內容還是切面、切點、增強這些,理解了AOP的這些基本概念,相信對無論使用何種配置都會做到心中有數。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章