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

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處理請求的過程的源碼解析。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章