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


1. 什么是AOP?

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

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

比如,我們需要記錄項目中所有對外接口的入參和出參,以便出現問題時定位原因,在每一個對外接口的代碼中添加代碼記錄入參和出參當然也可以達到目的,但是這種硬編碼的方式非常不友好,也不夠靈活,而且記錄日志本身和接口要實現的核心功能沒有任何關系。

此時,我們可以將記錄日志的功能定義到1個切面中,然后通過聲明的方式定義要在何時何地使用這個切面,而不用修改任何1個外部接口。

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

1.1 通知(Advice)

在AOP術語中,切面要完成的工作被稱為通知,通知定義了切面是什么以及何時使用。

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

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

1.2 連接點(Join point)

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

1.3 切點(Pointcut)

切點是為了縮小切面所通知的連接點的范圍,即切面在何處執行。我們通常使用明確的類和方法名稱,或者利用正則表達式定義所匹配的類和方法名稱來指定切點。

1.4 切面(Aspect)

切面是通知和切點的結合。通知和切點共同定義了切面的全部內容:它是什么,在何時和何處完成其功能。

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. 源碼及參考

源碼地址:https://github.com/zwwhnly/spring-action.git,歡迎下載。

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

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

原創不易,如果覺得文章能學到東西的話,歡迎點個贊、評個論、關個注,這是我堅持寫作的最大動力。

如果有興趣,歡迎添加我的微信:zwwhnly,等你來聊技術、職場、工作等話題(PS:我是一名奮斗在上海的程序員)。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM