Spring入門(十):Spring AOP使用講解

1. 什麼是AOP?

AOP是Aspect Oriented Programming的縮寫,意思是:面向切面編程,它是經過預編譯方式和運行期動態代理實現程序功能的統一維護的一種技術。java

能夠認爲AOP是對OOP(Object Oriented Programming 面向對象編程)的補充,主要使用在日誌記錄,性能統計,安全控制等場景,使用AOP可使得業務邏輯各部分之間的耦合度下降,只專一於各自的業務邏輯實現,從而提升程序的可讀性及維護性。git

好比,咱們須要記錄項目中全部對外接口的入參和出參,以便出現問題時定位緣由,在每個對外接口的代碼中添加代碼記錄入參和出參固然也能夠達到目的,可是這種硬編碼的方式很是不友好,也不夠靈活,並且記錄日誌自己和接口要實現的核心功能沒有任何關係。github

此時,咱們能夠將記錄日誌的功能定義到1個切面中,而後經過聲明的方式定義要在什麼時候何地使用這個切面,而不用修改任何1個外部接口。正則表達式

在講解具體的實現方式以前,咱們先了解幾個AOP中的術語。spring

1.1 通知(Advice)

在AOP術語中,切面要完成的工做被稱爲通知,通知定義了切面是什麼以及什麼時候使用。編程

Spring切面有5種類型的通知,分別是:安全

  • 前置通知(Before):在目標方法被調用以前調用通知功能
  • 後置通知(After):在目標方法完成以後調用通知,此時不關心方法的輸出結果是什麼
  • 返回通知(After-returning):在目標方法成功執行以後調用通知
  • 異常通知(After-throwing):在目標方法拋出異常後調用通知
  • 環繞通知(Around):通知包裹了被通知的方法,在被通知的方法調用以前和調用以後執行自定義的行爲

1.2 鏈接點(Join point)

鏈接點是在應用執行過程當中可以插入切面的一個點,這個點能夠是調用方法時、拋出異常時、修改某個字段時。微信

1.3 切點(Pointcut)

切點是爲了縮小切面所通知的鏈接點的範圍,即切面在何處執行。咱們一般使用明確的類和方法名稱,或者利用正則表達式定義所匹配的類和方法名稱來指定切點。app

1.4 切面(Aspect)

切面是通知和切點的結合。通知和切點共同定義了切面的所有內容:它是什麼,在什麼時候和何處完成其功能。ide

1.5 引入(Introduction)

引入容許咱們在不修改現有類的基礎上,向現有類添加新方法或屬性。

1.6 織入(Weaving)

織入是把切面應用到目標對象並建立新的代理對象的過程。

切面在指定的鏈接點被織入到目標對象中,在目標對象的生命週期裏,有如下幾個點能夠進行織入:

  • 編譯期:切面在目標類編譯時被織入。這種方式須要特殊的編譯器。AspectJ的織入編譯器就是以這種方式織入切面的。
  • 類加載期:切面在目標類加載到JVM時被織入。這種方式須要特殊的類加載器(ClassLoader),它能夠在目標類被引入應用以前加強該目標類的字節碼。
  • 運行期:切面在應用運行的某個時刻被織入。通常狀況下,在織入切面時,AOP容器會爲目標對象動態地建立一個代理對象。Spring AOP就是以這種方式織入切面的。

2. Spring 對AOP的支持

2.1 動態代理

Spring AOP構建在動態代理之上,也就是說,Spring運行時會爲目標對象動態建立代理對象。

代理類封裝了目標類,並攔截被通知方法的調用,再把調用轉發給真正的目標bean。

當代理類攔截到方法調用時,在調用目標bean方法以前,會執行切面邏輯。

2.2 織入切面時機

經過在代理類中包裹切面,Spring在運行期把切面織入到Spring 管理的bean中,也就是說,直到應用須要被代理的bean時,Spring纔會建立代理對象。

由於Spring運行時才建立代理對象,因此咱們不須要特殊的編譯器來織入Spring AOP切面。

2.3 鏈接點限制

Spring只支持方法級別的鏈接點,若是須要字段級別或者構造器級別的鏈接點,能夠利用AspectJ來補充Spring AOP的功能。

3. Spring AOP使用

假設咱們有個現場表演的接口Performance和它的實現類SleepNoMore:

package chapter04.concert;

/** * 現場表演,如舞臺劇,電影,音樂會 */
public interface Performance {
    void perform();
}
複製代碼
package chapter04.concert;

import org.springframework.stereotype.Component;

/** * 戲劇:《不眠之夜Sleep No More》 */
@Component
public class SleepNoMore implements Performance {
    @Override
    public void perform() {
        System.out.println("戲劇《不眠之夜Sleep No More》");
    }
}
複製代碼

既然是演出,就須要觀衆,假設咱們的需求是:在看演出以前,觀衆先入座並將手機調整至靜音,在觀看演出以後觀衆鼓掌,若是演出失敗觀衆退票,咱們固然能夠把這些邏輯寫在上面的perform()方法中,但不推薦這麼作,由於這些邏輯理論上和演出的核心無關,就算觀衆不將手機調整至靜音或者看完演出不鼓掌,都不影響演出的進行。

針對這個需求,咱們可使用AOP來實現。

3.1 定義切面

首先,在pom.xml文件中添加以下依賴:

<!--spring aop支持-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.1.8.RELEASE</version>
</dependency>
<!--aspectj支持-->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.8.5</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.8.9</version>
</dependency>
複製代碼

而後,定義一個觀衆的切面以下:

package chapter04.concert;

import org.aspectj.lang.annotation.Aspect;

/** * 觀衆 * 使用@Aspect註解定義爲切面 */
@Aspect
public class Audience {
}
複製代碼

注意事項:@Aspect註解代表Audience類是一個切面。

3.2 定義前置通知

在Audience切面中定義前置通知以下所示:

/** * 表演以前,觀衆就座 */
@Before("execution(* chapter04.concert.Performance.perform(..))")
public void takeSeats() {
    System.out.println("Taking seats");
}

/** * 表演以前,將手機調至靜音 */
@Before("execution(* chapter04.concert.Performance.perform(..))")
public void silenceCellPhones() {
    System.out.println("Silencing cell phones");
}
複製代碼

這裏的重點代碼是@Before("execution(* chapter04.concert.Performance.perform(..))"),它定義了1個前置通知,其中execution(* chapter04.concert.Performance.perform(..))被稱爲AspectJ切點表達式,每一部分的講解以下:

  • @Before:該註解用來定義前置通知,通知方法會在目標方法調用以前執行
  • execution:在方法執行時觸發
  • *:代表咱們不關心方法返回值的類型,便可以是任意類型
  • chapter04.concert.Performance.perform:使用全限定類名和方法名指定要添加前置通知的方法
  • (..):方法的參數列表使用(..),代表咱們不關心方法的入參是什麼,便可以是任意類型

3.3 定義後置通知

在Audience切面中定義後置通知以下所示:

/** * 表演結束,無論表演成功或者失敗 */
@After("execution(* chapter04.concert.Performance.perform(..))")
public void finish() {
    System.out.println("perform finish");
}
複製代碼

注意事項:@After註解用來定義後置通知,通知方法會在目標方法返回或者拋出異常後調用

3.4 定義返回通知

在Audience切面中定義返回通知以下所示:

/** * 表演以後,鼓掌 */
@AfterReturning("execution(* chapter04.concert.Performance.perform(..))")
public void applause() {
    System.out.println("CLAP CLAP CLAP!!!");
}
複製代碼

注意事項:@AfterReturning註解用來定義返回通知,通知方法會在目標方法返回後調用

3.5 定義異常通知

在Audience切面中定義異常通知以下所示:

/** * 表演失敗以後,觀衆要求退款 */
@AfterThrowing("execution(* chapter04.concert.Performance.perform(..))")
public void demandRefund() {
    System.out.println("Demanding a refund");
}
複製代碼

注意事項:@AfterThrowing註解用來定義異常通知,通知方法會在目標方法拋出異常後調用

3.6 定義可複用的切點表達式

細心的你可能會發現,咱們上面定義的5個切點中,切點表達式都是同樣的,這顯然是很差的,好在咱們可使用@Pointcut註解來定義可重複使用的切點表達式:

/** * 可複用的切點 */
@Pointcut("execution(* chapter04.concert.Performance.perform(..))")
public void perform() {
}
複製代碼

而後以前定義的5個切點均可以引用這個切點表達式:

/** * 表演以前,觀衆就座 */
@Before("perform()")
public void takeSeats() {
    System.out.println("Taking seats");
}

/** * 表演以前,將手機調至靜音 */
@Before("perform()")
public void silenceCellPhones() {
    System.out.println("Silencing cell phones");
}

/** * 表演結束,無論表演成功或者失敗 */
@After("perform()")
public void finish() {
    System.out.println("perform finish");
}

/** * 表演以後,鼓掌 */
@AfterReturning("perform()")
public void applause() {
    System.out.println("CLAP CLAP CLAP!!!");
}

/** * 表演失敗以後,觀衆要求退款 */
@AfterThrowing("perform()")
public void demandRefund() {
    System.out.println("Demanding a refund");
}
複製代碼

3.7 單元測試

新建配置類ConcertConfig以下所示:

package chapter04.concert;

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
@ComponentScan
public class ConcertConfig {
    @Bean
    public Audience audience() {
        return new Audience();
    }
}
複製代碼

注意事項:和以往不一樣的是,咱們使用了@EnableAspectJAutoProxy註解,該註解用來啓用自動代理功能。

新建Main類,在其main()方法中添加以下測試代碼:

package chapter04.concert;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConcertConfig.class);

        Performance performance = context.getBean(Performance.class);
        performance.perform();

        context.close();
    }
}
複製代碼

運行代碼,輸出結果以下所示:

Silencing cell phones

Taking seats

戲劇《不眠之夜Sleep No More》

perform finish

CLAP CLAP CLAP!!!

稍微修改下SleepNoMore類的perform()方法,讓它拋出一個異常:

@Override
public void perform() {
    int number = 3 / 0;
    System.out.println("戲劇《不眠之夜Sleep No More》");
}
複製代碼

再次運行代碼,輸出結果以下所示:

Silencing cell phones

Taking seats

perform finish

Demanding a refund

Exception in thread "main" java.lang.ArithmeticException: / by zero

由此也能夠說明,無論目標方法是否執行成功,@After註解都會執行,但@AfterReturning註解只會在目標方法執行成功時執行。

值得注意的是,使用@Aspect註解的切面類必須是一個bean(無論以何種方式聲明),不然切面不會生效,由於AspectJ自動代理只會爲使用@Aspect註解的bean建立代理類。

也就是說,若是咱們將ConcertConfig配置類中的如下代碼刪除或者註釋掉:

@Bean
public Audience audience() {
    return new Audience();
}
複製代碼

運行結果將變爲:

戲劇《不眠之夜Sleep No More》

3.8 建立環繞通知

咱們可使用@Around註解建立環繞通知,該註解可以讓你在調用目標方法先後,自定義本身的邏輯。

所以,咱們以前定義的5個切點,如今能夠定義在一個切點中,爲不影響以前的切面,咱們新建切面AroundAudience,以下所示:

package chapter04.concert;

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 AroundAudience {
    /** * 可重用的切點 */
    @Pointcut("execution(* chapter04.concert.Performance.perform(..))")
    public void perform() {
    }

    @Around("perform()")
    public void watchPerform(ProceedingJoinPoint joinPoint) {
        try {
            System.out.println("Taking seats");
            System.out.println("Silencing cell phones");

            joinPoint.proceed();

            System.out.println("CLAP CLAP CLAP!!!");
        } catch (Throwable throwable) {
            System.out.println("Demanding a refund");
        } finally {
            System.out.println("perform finish");
        }
    }
}
複製代碼

這裏要注意的是,該方法有個ProceedingJoinPoint類型的參數,在方法中能夠經過調用它的proceed()方法來調用目標方法。

而後修改下ConcertConfig類的代碼:

package chapter04.concert;

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
@ComponentScan
public class ConcertConfig {
    /*@Bean public Audience audience() { return new Audience(); }*/

    @Bean
    public AroundAudience aroundAudience() {
        return new AroundAudience();
    }
}
複製代碼

運行結果以下所示:

Taking seats

Silencing cell phones

戲劇《不眠之夜Sleep No More》

CLAP CLAP CLAP!!!

perform finish

4. 源碼及參考

源碼地址:github.com/zwwhnly/spr…,歡迎下載。

Craig Walls 《Spring實戰(第4版)》

AOP(面向切面編程)_百度百科

最後,歡迎關注個人微信公衆號:「申城異鄉人」,全部博客會同步更新。

相關文章
相關標籤/搜索