Spring框架系列(4) - 深入淺出Spring核心之面向切面程式設計(AOP)

pdai發表於2022-06-23

Spring基礎 - Spring簡單例子引入Spring的核心中向你展示了AOP的基礎含義,同時以此發散了一些AOP相關知識點; 本節將在此基礎上進一步解讀AOP的含義以及AOP的使用方式。@pdai

引入

我們在Spring基礎 - Spring簡單例子引入Spring的核心中向你展示了AOP的基礎含義,同時以此發散了一些AOP相關知識點。

  1. Spring 框架通過定義切面, 通過攔截切點實現了不同業務模組的解耦,這個就叫面向切面程式設計 - Aspect Oriented Programming (AOP)
  2. 為什麼@Aspect註解使用的是aspectj的jar包呢?這就引出了Aspect4J和Spring AOP的歷史淵源,只有理解了Aspect4J和Spring的淵源才能理解有些註解上的相容設計
  3. 如何支援更多攔截方式來實現解耦, 以滿足更多場景需求呢? 這就是@Around, @Pointcut... 等的設計
  4. 那麼Spring框架又是如何實現AOP的呢? 這就引入代理技術,分靜態代理和動態代理,動態代理又包含JDK代理和CGLIB代理等

本節將在此基礎上進一步解讀AOP的含義以及AOP的使用方式;後續的文章還將深入AOP的實現原理:

如何理解AOP

AOP的本質也是為了解耦,它是一種設計思想; 在理解時也應該簡化理解。

AOP是什麼

AOP為Aspect Oriented Programming的縮寫,意為:面向切面程式設計

AOP最早是AOP聯盟的組織提出的,指定的一套規範,spring將AOP的思想引入框架之中,通過預編譯方式執行期間動態代理實現程式的統一維護的一種技術,

  • 先來看一個例子, 如何給如下UserServiceImpl中所有方法新增進入方法的日誌,
/**
 * @author pdai
 */
public class UserServiceImpl implements IUserService {

    /**
     * find user list.
     *
     * @return user list
     */
    @Override
    public List<User> findUserList() {
        System.out.println("execute method: findUserList");
        return Collections.singletonList(new User("pdai", 18));
    }

    /**
     * add user
     */
    @Override
    public void addUser() {
        System.out.println("execute method: addUser");
        // do something
    }

}

我們將記錄日誌功能解耦為日誌切面,它的目標是解耦。進而引出AOP的理念:就是將分散在各個業務邏輯程式碼中相同的程式碼通過橫向切割的方式抽取到一個獨立的模組中!

OOP物件導向程式設計,針對業務處理過程的實體及其屬性和行為進行抽象封裝,以獲得更加清晰高效的邏輯單元劃分。而AOP則是針對業務處理過程中的切面進行提取,它所面對的是處理過程的某個步驟或階段,以獲得邏輯過程的中各部分之間低耦合的隔離效果。這兩種設計思想在目標上有著本質的差異。

AOP術語

首先讓我們從一些重要的AOP概念和術語開始。這些術語不是Spring特有的

  • 連線點(Jointpoint):表示需要在程式中插入橫切關注點的擴充套件點,連線點可能是類初始化、方法執行、方法呼叫、欄位呼叫或處理異常等等,Spring只支援方法執行連線點,在AOP中表示為在哪裡幹

  • 切入點(Pointcut): 選擇一組相關連線點的模式,即可以認為連線點的集合,Spring支援perl5正規表示式和AspectJ切入點模式,Spring預設使用AspectJ語法,在AOP中表示為在哪裡乾的集合

  • 通知(Advice):在連線點上執行的行為,通知提供了在AOP中需要在切入點所選擇的連線點處進行擴充套件現有行為的手段;包括前置通知(before advice)、後置通知(after advice)、環繞通知(around advice),在Spring中通過代理模式實現AOP,並通過攔截器模式以環繞連線點的攔截器鏈織入通知;在AOP中表示為幹什麼

  • 方面/切面(Aspect):橫切關注點的模組化,比如上邊提到的日誌元件。可以認為是通知、引入和切入點的組合;在Spring中可以使用Schema和@AspectJ方式進行組織實現;在AOP中表示為在哪乾和幹什麼集合

  • 引入(inter-type declaration):也稱為內部型別宣告,為已有的類新增額外新的欄位或方法,Spring允許引入新的介面(必須對應一個實現)到所有被代理物件(目標物件), 在AOP中表示為幹什麼(引入什麼)

  • 目標物件(Target Object):需要被織入橫切關注點的物件,即該物件是切入點選擇的物件,需要被通知的物件,從而也可稱為被通知物件;由於Spring AOP 通過代理模式實現,從而這個物件永遠是被代理物件,在AOP中表示為對誰幹

  • 織入(Weaving):把切面連線到其它的應用程式型別或者物件上,並建立一個被通知的物件。這些可以在編譯時(例如使用AspectJ編譯器),類載入時和執行時完成。Spring和其他純Java AOP框架一樣,在執行時完成織入。在AOP中表示為怎麼實現的

  • AOP代理(AOP Proxy):AOP框架使用代理模式建立的物件,從而實現在連線點處插入通知(即應用切面),就是通過代理來對目標物件應用切面。在Spring中,AOP代理可以用JDK動態代理或CGLIB代理實現,而通過攔截器模型應用切面。在AOP中表示為怎麼實現的一種典型方式

通知型別

  • 前置通知(Before advice):在某連線點之前執行的通知,但這個通知不能阻止連線點之前的執行流程(除非它丟擲一個異常)。

  • 後置通知(After returning advice):在某連線點正常完成後執行的通知:例如,一個方法沒有丟擲任何異常,正常返回。

  • 異常通知(After throwing advice):在方法丟擲異常退出時執行的通知。

  • 最終通知(After (finally) advice):當某連線點退出的時候執行的通知(不論是正常返回還是異常退出)。

  • 環繞通知(Around Advice):包圍一個連線點的通知,如方法呼叫。這是最強大的一種通知型別。環繞通知可以在方法呼叫前後完成自定義的行為。它也會選擇是否繼續執行連線點或直接返回它自己的返回值或丟擲異常來結束執行。

環繞通知是最常用的通知型別。和AspectJ一樣,Spring提供所有型別的通知,我們推薦你使用盡可能簡單的通知型別來實現需要的功能。例如,如果你只是需要一個方法的返回值來更新快取,最好使用後置通知而不是環繞通知,儘管環繞通知也能完成同樣的事情。用最合適的通知型別可以使得程式設計模型變得簡單,並且能夠避免很多潛在的錯誤。比如,你不需要在JoinPoint上呼叫用於環繞通知的proceed()方法,就不會有呼叫的問題。

我們把這些術語串聯到一起,方便理解

Spring AOP和AspectJ是什麼關係

  • 首先AspectJ是什麼

AspectJ是一個java實現的AOP框架,它能夠對java程式碼進行AOP編譯(一般在編譯期進行),讓java程式碼具有AspectJ的AOP功能(當然需要特殊的編譯器)

可以這樣說AspectJ是目前實現AOP框架中最成熟,功能最豐富的語言,更幸運的是,AspectJ與java程式完全相容,幾乎是無縫關聯,因此對於有java程式設計基礎的工程師,上手和使用都非常容易。

  • 其次,為什麼需要理清楚Spring AOP和AspectJ的關係

我們看下@Aspect以及增強的幾個註解,為什麼不是Spring包,而是來源於aspectJ呢?

  • Spring AOP和AspectJ是什麼關係
  1. AspectJ是更強的AOP框架,是實際意義的AOP標準
  2. Spring為何不寫類似AspectJ的框架? Spring AOP使用純Java實現, 它不需要專門的編譯過程, 它一個重要的原則就是無侵入性(non-invasiveness); Spring 小組完全有能力寫類似的框架,只是Spring AOP從來沒有打算通過提供一種全面的AOP解決方案來與AspectJ競爭。Spring的開發小組相信無論是基於代理(proxy-based)的框架如Spring AOP或者是成熟的框架如AspectJ都是很有價值的,他們之間應該是互補而不是競爭的關係
  3. Spring小組喜歡@AspectJ註解風格更勝於Spring XML配置; 所以在Spring 2.0使用了和AspectJ 5一樣的註解,並使用AspectJ來做切入點解析和匹配但是,AOP在執行時仍舊是純的Spring AOP,並不依賴於AspectJ的編譯器或者織入器(weaver)
  4. Spring 2.5對AspectJ的支援:在一些環境下,增加了對AspectJ的裝載時編織支援,同時提供了一個新的bean切入點。
  • 更多關於AspectJ

瞭解AspectJ應用到java程式碼的過程(這個過程稱為織入),對於織入這個概念,可以簡單理解為aspect(切面)應用到目標函式(類)的過程。

對於這個過程,一般分為動態織入靜態織入

  1. 動態織入的方式是在執行時動態將要增強的程式碼織入到目標類中,這樣往往是通過動態代理技術完成的,如Java JDK的動態代理(Proxy,底層通過反射實現)或者CGLIB的動態代理(底層通過繼承實現),Spring AOP採用的就是基於執行時增強的代理技術
  2. ApectJ採用的就是靜態織入的方式。ApectJ主要採用的是編譯期織入,在這個期間使用AspectJ的acj編譯器(類似javac)把aspect類編譯成class位元組碼後,在java目標類編譯時織入,即先編譯aspect類再編譯目標類。

AOP的配置方式

Spring AOP 支援對XML模式和基於@AspectJ註解的兩種配置方式。

XML Schema配置方式

Spring提供了使用"aop"名稱空間來定義一個切面,我們來看個例子(例子程式碼):

  • 定義目標類
package tech.pdai.springframework.service;

/**
 * @author pdai
 */
public class AopDemoServiceImpl {

    public void doMethod1() {
        System.out.println("AopDemoServiceImpl.doMethod1()");
    }

    public String doMethod2() {
        System.out.println("AopDemoServiceImpl.doMethod2()");
        return "hello world";
    }

    public String doMethod3() throws Exception {
        System.out.println("AopDemoServiceImpl.doMethod3()");
        throw new Exception("some exception");
    }
}
  • 定義切面類
package tech.pdai.springframework.aspect;

import org.aspectj.lang.ProceedingJoinPoint;

/**
 * @author pdai
 */
public class LogAspect {

    /**
     * 環繞通知.
     *
     * @param pjp pjp
     * @return obj
     * @throws Throwable exception
     */
    public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("-----------------------");
        System.out.println("環繞通知: 進入方法");
        Object o = pjp.proceed();
        System.out.println("環繞通知: 退出方法");
        return o;
    }

    /**
     * 前置通知.
     */
    public void doBefore() {
        System.out.println("前置通知");
    }

    /**
     * 後置通知.
     *
     * @param result return val
     */
    public void doAfterReturning(String result) {
        System.out.println("後置通知, 返回值: " + result);
    }

    /**
     * 異常通知.
     *
     * @param e exception
     */
    public void doAfterThrowing(Exception e) {
        System.out.println("異常通知, 異常: " + e.getMessage());
    }

    /**
     * 最終通知.
     */
    public void doAfter() {
        System.out.println("最終通知");
    }

}
  • XML配置AOP
<?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"
       xmlns:context="http://www.springframework.org/schema/context"
       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
 http://www.springframework.org/schema/context
 http://www.springframework.org/schema/context/spring-context.xsd
">

    <context:component-scan base-package="tech.pdai.springframework" />

    <aop:aspectj-autoproxy/>

    <!-- 目標類 -->
    <bean id="demoService" class="tech.pdai.springframework.service.AopDemoServiceImpl">
        <!-- configure properties of bean here as normal -->
    </bean>

    <!-- 切面 -->
    <bean id="logAspect" class="tech.pdai.springframework.aspect.LogAspect">
        <!-- configure properties of aspect here as normal -->
    </bean>

    <aop:config>
        <!-- 配置切面 -->
        <aop:aspect ref="logAspect">
            <!-- 配置切入點 -->
            <aop:pointcut id="pointCutMethod" expression="execution(* tech.pdai.springframework.service.*.*(..))"/>
            <!-- 環繞通知 -->
            <aop:around method="doAround" pointcut-ref="pointCutMethod"/>
            <!-- 前置通知 -->
            <aop:before method="doBefore" pointcut-ref="pointCutMethod"/>
            <!-- 後置通知;returning屬性:用於設定後置通知的第二個引數的名稱,型別是Object -->
            <aop:after-returning method="doAfterReturning" pointcut-ref="pointCutMethod" returning="result"/>
            <!-- 異常通知:如果沒有異常,將不會執行增強;throwing屬性:用於設定通知第二個引數的的名稱、型別-->
            <aop:after-throwing method="doAfterThrowing" pointcut-ref="pointCutMethod" throwing="e"/>
            <!-- 最終通知 -->
            <aop:after method="doAfter" pointcut-ref="pointCutMethod"/>
        </aop:aspect>
    </aop:config>

    <!-- more bean definitions for data access objects go here -->
</beans>
  • 測試類
/**
  * main interfaces.
  *
  * @param args args
  */
public static void main(String[] args) {
    // create and configure beans
    ApplicationContext context = new ClassPathXmlApplicationContext("aspects.xml");

    // retrieve configured instance
    AopDemoServiceImpl service = context.getBean("demoService", AopDemoServiceImpl.class);

    // use configured instance
    service.doMethod1();
    service.doMethod2();
    try {
        service.doMethod3();
    } catch (Exception e) {
        // e.printStackTrace();
    }
}
  • 輸出結果
-----------------------
環繞通知: 進入方法
前置通知
AopDemoServiceImpl.doMethod1()
環繞通知: 退出方法
最終通知
-----------------------
環繞通知: 進入方法
前置通知
AopDemoServiceImpl.doMethod2()
環繞通知: 退出方法
最終通知
後置通知, 返回值: hello world
-----------------------
環繞通知: 進入方法
前置通知
AopDemoServiceImpl.doMethod3()
最終通知
異常通知, 異常: some exception

AspectJ註解方式

基於XML的宣告式AspectJ存在一些不足,需要在Spring配置檔案配置大量的程式碼資訊,為了解決這個問題,Spring 使用了@AspectJ框架為AOP的實現提供了一套註解。

註解名稱 解釋
@Aspect 用來定義一個切面。
@pointcut 用於定義切入點表示式。在使用時還需要定義一個包含名字和任意引數的方法簽名來表示切入點名稱,這個方法簽名就是一個返回值為void,且方法體為空的普通方法。
@Before 用於定義前置通知,相當於BeforeAdvice。在使用時,通常需要指定一個value屬性值,該屬性值用於指定一個切入點表示式(可以是已有的切入點,也可以直接定義切入點表示式)。
@AfterReturning 用於定義後置通知,相當於AfterReturningAdvice。在使用時可以指定pointcut / value和returning屬性,其中pointcut / value這兩個屬性的作用一樣,都用於指定切入點表示式。
@Around 用於定義環繞通知,相當於MethodInterceptor。在使用時需要指定一個value屬性,該屬性用於指定該通知被植入的切入點。
@After-Throwing 用於定義異常通知來處理程式中未處理的異常,相當於ThrowAdvice。在使用時可指定pointcut / value和throwing屬性。其中pointcut/value用於指定切入點表示式,而throwing屬性值用於指定-一個形參名來表示Advice方法中可定義與此同名的形參,該形參可用於訪問目標方法丟擲的異常。
@After 用於定義最終final 通知,不管是否異常,該通知都會執行。使用時需要指定一個value屬性,該屬性用於指定該通知被植入的切入點。
@DeclareParents 用於定義引介通知,相當於IntroductionInterceptor (不要求掌握)。

Spring AOP的實現方式是動態織入,動態織入的方式是在執行時動態將要增強的程式碼織入到目標類中,這樣往往是通過動態代理技術完成的;如Java JDK的動態代理(Proxy,底層通過反射實現)或者CGLIB的動態代理(底層通過繼承實現),Spring AOP採用的就是基於執行時增強的代理技術。所以我們看下如下的兩個例子(例子程式碼 中05模組):

  • 基於JDK代理例子
  • 基於Cglib代理例子

介面使用JDK代理

  • 定義介面
/**
 * Jdk Proxy Service.
 *
 * @author pdai
 */
public interface IJdkProxyService {

    void doMethod1();

    String doMethod2();

    String doMethod3() throws Exception;
}
  • 實現類
/**
 * @author pdai
 */
@Service
public class JdkProxyDemoServiceImpl implements IJdkProxyService {

    @Override
    public void doMethod1() {
        System.out.println("JdkProxyServiceImpl.doMethod1()");
    }

    @Override
    public String doMethod2() {
        System.out.println("JdkProxyServiceImpl.doMethod2()");
        return "hello world";
    }

    @Override
    public String doMethod3() throws Exception {
        System.out.println("JdkProxyServiceImpl.doMethod3()");
        throw new Exception("some exception");
    }
}
  • 定義切面
package tech.pdai.springframework.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;

/**
 * @author pdai
 */
@EnableAspectJAutoProxy
@Component
@Aspect
public class LogAspect {

    /**
     * define point cut.
     */
    @Pointcut("execution(* tech.pdai.springframework.service.*.*(..))")
    private void pointCutMethod() {
    }


    /**
     * 環繞通知.
     *
     * @param pjp pjp
     * @return obj
     * @throws Throwable exception
     */
    @Around("pointCutMethod()")
    public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("-----------------------");
        System.out.println("環繞通知: 進入方法");
        Object o = pjp.proceed();
        System.out.println("環繞通知: 退出方法");
        return o;
    }

    /**
     * 前置通知.
     */
    @Before("pointCutMethod()")
    public void doBefore() {
        System.out.println("前置通知");
    }


    /**
     * 後置通知.
     *
     * @param result return val
     */
    @AfterReturning(pointcut = "pointCutMethod()", returning = "result")
    public void doAfterReturning(String result) {
        System.out.println("後置通知, 返回值: " + result);
    }

    /**
     * 異常通知.
     *
     * @param e exception
     */
    @AfterThrowing(pointcut = "pointCutMethod()", throwing = "e")
    public void doAfterThrowing(Exception e) {
        System.out.println("異常通知, 異常: " + e.getMessage());
    }

    /**
     * 最終通知.
     */
    @After("pointCutMethod()")
    public void doAfter() {
        System.out.println("最終通知");
    }

}
  • 輸出
-----------------------
環繞通知: 進入方法
前置通知
JdkProxyServiceImpl.doMethod1()
最終通知
環繞通知: 退出方法
-----------------------
環繞通知: 進入方法
前置通知
JdkProxyServiceImpl.doMethod2()
後置通知, 返回值: hello world
最終通知
環繞通知: 退出方法
-----------------------
環繞通知: 進入方法
前置通知
JdkProxyServiceImpl.doMethod3()
異常通知, 異常: some exception
最終通知

非介面使用Cglib代理

  • 類定義
/**
 * Cglib proxy.
 *
 * @author pdai
 */
@Service
public class CglibProxyDemoServiceImpl {

    public void doMethod1() {
        System.out.println("CglibProxyDemoServiceImpl.doMethod1()");
    }

    public String doMethod2() {
        System.out.println("CglibProxyDemoServiceImpl.doMethod2()");
        return "hello world";
    }

    public String doMethod3() throws Exception {
        System.out.println("CglibProxyDemoServiceImpl.doMethod3()");
        throw new Exception("some exception");
    }
}
  • 切面定義

和上面相同

  • 輸出
-----------------------
環繞通知: 進入方法
前置通知
CglibProxyDemoServiceImpl.doMethod1()
最終通知
環繞通知: 退出方法
-----------------------
環繞通知: 進入方法
前置通知
CglibProxyDemoServiceImpl.doMethod2()
後置通知, 返回值: hello world
最終通知
環繞通知: 退出方法
-----------------------
環繞通知: 進入方法
前置通知
CglibProxyDemoServiceImpl.doMethod3()
異常通知, 異常: some exception
最終通知

AOP使用問題小結

這裡總結下實際開發中會遇到的一些問題:

切入點(pointcut)的申明規則?

Spring AOP 使用者可能會經常使用 execution切入點指示符。執行表示式的格式如下:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
  • ret-type-pattern 返回型別模式, name-pattern名字模式和param-pattern引數模式是必選的, 其它部分都是可選的。返回型別模式決定了方法的返回型別必須依次匹配一個連線點。 你會使用的最頻繁的返回型別模式是*它代表了匹配任意的返回型別
  • declaring-type-pattern, 一個全限定的型別名將只會匹配返回給定型別的方法。
  • name-pattern 名字模式匹配的是方法名。 你可以使用*萬用字元作為所有或者部分命名模式。
  • param-pattern 引數模式稍微有點複雜:()匹配了一個不接受任何引數的方法, 而(..)匹配了一個接受任意數量引數的方法(零或者更多)。 模式()匹配了一個接受一個任何型別的引數的方法。 模式(,String)匹配了一個接受兩個引數的方法,第一個可以是任意型別, 第二個則必須是String型別。

對應到我們上面的例子:

下面給出一些通用切入點表示式的例子。

// 任意公共方法的執行:
execution(public * *(..))

// 任何一個名字以“set”開始的方法的執行:
execution(* set*(..))

// AccountService介面定義的任意方法的執行:
execution(* com.xyz.service.AccountService.*(..))

// 在service包中定義的任意方法的執行:
execution(* com.xyz.service.*.*(..))

// 在service包或其子包中定義的任意方法的執行:
execution(* com.xyz.service..*.*(..))

// 在service包中的任意連線點(在Spring AOP中只是方法執行):
within(com.xyz.service.*)

// 在service包或其子包中的任意連線點(在Spring AOP中只是方法執行):
within(com.xyz.service..*)

// 實現了AccountService介面的代理物件的任意連線點 (在Spring AOP中只是方法執行):
this(com.xyz.service.AccountService)// 'this'在繫結表單中更加常用

// 實現AccountService介面的目標物件的任意連線點 (在Spring AOP中只是方法執行):
target(com.xyz.service.AccountService) // 'target'在繫結表單中更加常用

// 任何一個只接受一個引數,並且執行時所傳入的引數是Serializable 介面的連線點(在Spring AOP中只是方法執行)
args(java.io.Serializable) // 'args'在繫結表單中更加常用; 請注意在例子中給出的切入點不同於 execution(* *(java.io.Serializable)): args版本只有在動態執行時候傳入引數是Serializable時才匹配,而execution版本在方法簽名中宣告只有一個 Serializable型別的引數時候匹配。

// 目標物件中有一個 @Transactional 註解的任意連線點 (在Spring AOP中只是方法執行)
@target(org.springframework.transaction.annotation.Transactional)// '@target'在繫結表單中更加常用

// 任何一個目標物件宣告的型別有一個 @Transactional 註解的連線點 (在Spring AOP中只是方法執行):
@within(org.springframework.transaction.annotation.Transactional) // '@within'在繫結表單中更加常用

// 任何一個執行的方法有一個 @Transactional 註解的連線點 (在Spring AOP中只是方法執行)
@annotation(org.springframework.transaction.annotation.Transactional) // '@annotation'在繫結表單中更加常用

// 任何一個只接受一個引數,並且執行時所傳入的引數型別具有@Classified 註解的連線點(在Spring AOP中只是方法執行)
@args(com.xyz.security.Classified) // '@args'在繫結表單中更加常用

// 任何一個在名為'tradeService'的Spring bean之上的連線點 (在Spring AOP中只是方法執行)
bean(tradeService)

// 任何一個在名字匹配萬用字元表示式'*Service'的Spring bean之上的連線點 (在Spring AOP中只是方法執行)
bean(*Service)

此外Spring 支援如下三個邏輯運算子來組合切入點表示式

&&:要求連線點同時匹配兩個切入點表示式
||:要求連線點匹配任意個切入點表示式
!::要求連線點不匹配指定的切入點表示式

多種增強通知的順序?

如果有多個通知想要在同一連線點執行會發生什麼?Spring AOP遵循跟AspectJ一樣的優先規則來確定通知執行的順序。 在“進入”連線點的情況下,最高優先順序的通知會先執行(所以給定的兩個前置通知中,優先順序高的那個會先執行)。 在“退出”連線點的情況下,最高優先順序的通知會最後執行。(所以給定的兩個後置通知中, 優先順序高的那個會第二個執行)。

當定義在不同的切面裡的兩個通知都需要在一個相同的連線點中執行, 那麼除非你指定,否則執行的順序是未知的。你可以通過指定優先順序來控制執行順序。 在標準的Spring方法中可以在切面類中實現org.springframework.core.Ordered 介面或者用Order註解做到這一點。在兩個切面中, Ordered.getValue()方法返回值(或者註解值)較低的那個有更高的優先順序。

當定義在相同的切面裡的兩個通知都需要在一個相同的連線點中執行, 執行的順序是未知的(因為這裡沒有方法通過反射javac編譯的類來獲取宣告順序)。 考慮在每個切面類中按連線點壓縮這些通知方法到一個通知方法,或者重構通知的片段到各自的切面類中 - 它能在切面級別進行排序。

Spring AOP 和 AspectJ 之間的關鍵區別?

AspectJ可以做Spring AOP幹不了的事情,它是AOP程式設計的完全解決方案,Spring AOP則致力於解決企業級開發中最普遍的AOP(方法織入)。

下表總結了 Spring AOP 和 AspectJ 之間的關鍵區別:

Spring AOP AspectJ
在純 Java 中實現 使用 Java 程式語言的擴充套件實現
不需要單獨的編譯過程 除非設定 LTW,否則需要 AspectJ 編譯器 (ajc)
只能使用執行時織入 執行時織入不可用。支援編譯時、編譯後和載入時織入
功能不強-僅支援方法級編織 更強大 - 可以編織欄位、方法、建構函式、靜態初始值設定項、最終類/方法等......。
只能在由 Spring 容器管理的 bean 上實現 可以在所有域物件上實現
僅支援方法執行切入點 支援所有切入點
代理是由目標物件建立的, 並且切面應用在這些代理上 在執行應用程式之前 (在執行時) 前, 各方面直接在程式碼中進行織入
比 AspectJ 慢多了 更好的效能
易於學習和應用 相對於 Spring AOP 來說更復雜

Spring AOP還是完全用AspectJ?

以下Spring官方的回答:(總結來說就是 Spring AOP更易用,AspectJ更強大)。

  • Spring AOP比完全使用AspectJ更加簡單, 因為它不需要引入AspectJ的編譯器/織入器到你開發和構建過程中。 如果你僅僅需要在Spring bean上通知執行操作,那麼Spring AOP是合適的選擇
  • 如果你需要通知domain物件或其它沒有在Spring容器中管理的任意物件,那麼你需要使用AspectJ。
  • 如果你想通知除了簡單的方法執行之外的連線點(如:呼叫連線點、欄位get或set的連線點等等), 也需要使用AspectJ。

當使用AspectJ時,你可以選擇使用AspectJ語言(也稱為“程式碼風格”)或@AspectJ註解風格。 如果切面在你的設計中扮演一個很大的角色,並且你能在Eclipse等IDE中使用AspectJ Development Tools (AJDT), 那麼首選AspectJ語言 :- 因為該語言專門被設計用來編寫切面,所以會更清晰、更簡單。如果你沒有使用 Eclipse等IDE,或者在你的應用中只有很少的切面並沒有作為一個主要的角色,你或許應該考慮使用@AspectJ風格 並在你的IDE中附加一個普通的Java編輯器,並且在你的構建指令碼中增加切面織入(連結)的段落。

參考文章

http://shouce.jb51.net/spring/aop.html#aop-ataspectj

https://www.cnblogs.com/linhp/p/5881788.html

https://www.cnblogs.com/bj-xiaodao/p/10777914.html

更多文章

首先, 從Spring框架的整體架構和組成對整體框架有個認知。

  • Spring基礎 - Spring和Spring框架組成
    • Spring是什麼?它是怎麼誕生的?有哪些主要的元件和核心功能呢? 本文通過這幾個問題幫助你構築Spring和Spring Framework的整體認知。

其次,通過案例引出Spring的核心(IoC和AOP),同時對IoC和AOP進行案例使用分析。

基於Spring框架和IOC,AOP的基礎,為構建上層web應用,需要進一步學習SpringMVC。

  • Spring基礎 - SpringMVC請求流程和案例
    • 前文我們介紹了Spring框架和Spring框架中最為重要的兩個技術點(IOC和AOP),那我們如何更好的構建上層的應用呢(比如web 應用),這便是SpringMVC;Spring MVC是Spring在Spring Container Core和AOP等技術基礎上,遵循上述Web MVC的規範推出的web開發框架,目的是為了簡化Java棧的web開發。 本文主要介紹SpringMVC的請求流程和基礎案例的編寫和執行。

Spring進階 - IoC,AOP以及SpringMVC的原始碼分析

  • Spring進階 - Spring IOC實現原理詳解之IOC體系結構設計
    • 在對IoC有了初步的認知後,我們開始對IOC的實現原理進行深入理解。本文將幫助你站在設計者的角度去看IOC最頂層的結構設計
  • Spring進階 - Spring IOC實現原理詳解之IOC初始化流程
    • 上文,我們看了IOC設計要點和設計結構;緊接著這篇,我們可以看下原始碼的實現了:Spring如何實現將資源配置(以xml配置為例)通過載入,解析,生成BeanDefination並註冊到IoC容器中的
  • Spring進階 - Spring IOC實現原理詳解之Bean例項化(生命週期,迴圈依賴等)
    • 上文,我們看了IOC設計要點和設計結構;以及Spring如何實現將資源配置(以xml配置為例)通過載入,解析,生成BeanDefination並註冊到IoC容器中的;容器中存放的是Bean的定義即BeanDefinition放到beanDefinitionMap中,本質上是一個ConcurrentHashMap<String, Object>;並且BeanDefinition介面中包含了這個類的Class資訊以及是否是單例等。那麼如何從BeanDefinition中例項化Bean物件呢,這是本文主要研究的內容?
  • Spring進階 - Spring AOP實現原理詳解之切面實現
    • 前文,我們分析了Spring IOC的初始化過程和Bean的生命週期等,而Spring AOP也是基於IOC的Bean載入來實現的。本文主要介紹Spring AOP原理解析的切面實現過程(將切面類的所有切面方法根據使用的註解生成對應Advice,並將Advice連同切入點匹配器和切面類等資訊一併封裝到Advisor,為後續交給代理增強實現做準備的過程)。
  • Spring進階 - Spring AOP實現原理詳解之AOP代理
    • 上文我們介紹了Spring AOP原理解析的切面實現過程(將切面類的所有切面方法根據使用的註解生成對應Advice,並將Advice連同切入點匹配器和切面類等資訊一併封裝到Advisor)。本文在此基礎上繼續介紹,代理(cglib代理和JDK代理)的實現過程。
  • Spring進階 - Spring AOP實現原理詳解之Cglib代理實現
    • 我們在前文中已經介紹了SpringAOP的切面實現和建立動態代理的過程,那麼動態代理是如何工作的呢?本文主要介紹Cglib動態代理的案例和SpringAOP實現的原理。
  • Spring進階 - Spring AOP實現原理詳解之JDK代理實現
    • 上文我們學習了SpringAOP Cglib動態代理的實現,本文主要是SpringAOP JDK動態代理的案例和實現部分。
  • Spring進階 - SpringMVC實現原理之DispatcherServlet初始化的過程
    • 前文我們有了IOC的原始碼基礎以及SpringMVC的基礎,我們便可以進一步深入理解SpringMVC主要實現原理,包含DispatcherServlet的初始化過程和DispatcherServlet處理請求的過程的原始碼解析。本文是第一篇:DispatcherServlet的初始化過程的原始碼解析。
  • Spring進階 - SpringMVC實現原理之DispatcherServlet處理請求的過程
    • 前文我們有了IOC的原始碼基礎以及SpringMVC的基礎,我們便可以進一步深入理解SpringMVC主要實現原理,包含DispatcherServlet的初始化過程和DispatcherServlet處理請求的過程的原始碼解析。本文是第二篇:DispatcherServlet處理請求的過程的原始碼解析。

相關文章