[Spring 系列] Spring AOP 之詳解與基礎用法②

前言

Hi 我是來自西部大嫖客傑西html

上篇文章我從代理模式的概念,慢慢引入了靜態代理和動態代理,還講了一些關於動態代理的分類及概念的特別說明。若是你沒看到我上節的文章,我能夠送一個傳送門: Spring AOP 之代理模式① java

理解了設計模式,那麼咱們開始學習一下看 Spring 團隊利用了代理模式糅合到 Spring AOP 當中去的。web

AOP 介紹

什麼是 AOP ? AOP 其實跟 OOP (面向對象編程)同樣,都是一種編程思想。它的全稱是 Aspect-oriented Programming,也就是面向切面編程。spring

在應用上,AOP 實際上是 OOP 的一種補充。還記得,OOP 爲何這麼流行?由於 OOP 的出現是可讓人們將複雜的問題抽象化,簡單化;同時,OOP 的三大特性如封裝/繼承/多態,能夠封裝細節內容,使編程更加簡單,更符合人類的思惟。這些選項就已經優於古老的面向過程的思想(可是說面向過程並不優越,只是說明 OOP 更加受大衆歡迎)。編程

可是爲何 AOP 是一個補充呢?由於雖然 OOP 已是理想的編程模型,可是它任然有不足之處。在實際編程中,咱們經常會遇到一些普通,通用但又是不可缺乏的功能(代碼)。設計模式

那怎麼辦?咱們只能將涉及多業務流程的通用功能抽取並單獨封裝,造成獨立的切面,在合適的時機將這些切面橫向切入到業務流程指定的位置中。例如說,咱們經常使用的日誌功能,事務等等。緩存

總的來講,AOP 實際上是將散佈於不一樣業務但功能通用的代碼從業務邏輯中抽取出來,封裝成獨立的模塊(切面)。同時,當業務邏輯中執行的過程當中,AOP 把切面和關注點切入業務流程。性能優化

AOP 應用場景

切面應用場景大多數都會想到的是日誌或者事務。在單體應用中使用切面實現日誌是個不錯的選擇,可是分佈式後就不必定了,固然這是題外話。bash

下面我說些經常使用的 AOP 場景。app

  1. 緩存
  2. 異常處理,能夠用來捕捉異常
  3. 調試,性能優化,計算某個方法執行的時間進行精準優化
  4. 持久化
  5. 事務,這個咱們常用的 @Transactional 註解
  6. 日誌
  7. 記錄追蹤,能夠記錄執行了哪些功能(本質上和日誌差很少)

AOP 術語

想了解清楚 Spring AOP,那麼瞭解它們的組成很是重要。這對接下來的源碼分析也有必定的幫助。

名稱 說明
Aspect 一個關注點的模塊化。由於一個關注點能夠橫切多個對象,因此咱們能夠將它進行模塊管理。事務管理(@Transactinal)就是一個很好的例子。在 Spring AOP 中,切面能夠基於模式或者基於 @Aspect 註解的方式實現。
Join Point 程序執行過程當中的一個點,如方法的執行或異常的處理。在 Spring AOP 中,鏈接點老是表示方法執行。
Advice 在切面的某個特定的鏈接點上的執行的動做
Introduction 表明類型聲明其餘方法或字段。Spring AOP 容許您將新的接口(和相應的實現)引入任何被建議的對象。例如,您可使用一個介紹來讓一個 bean 實現一個 IsModified 接口,以簡化緩存。
Target Object 即將被代理的目標對象。
AOP proxy 爲了實現 Aspect 契約(通知方法執行等)而由 AOP框架建立的對象。在Spring框架中,AOP代理是JDK動態代理或CGLIB代理。
Weave aspect 與其餘應用程序類型或對象連接以建立 advised 對象。

關於 AOP 在 Spring 中的應用

咱們都知道了,Spring AOP 基於動態代理的 AOP 框架。其中動態代理包含了兩種模式:JDK Dynamic ProxyCglib Proxy

AspectJ 屬於靜態代理,由於它其實是在編譯期加強,與動態代理的編織時期不一樣,因此 Spring 雖然集成了 AspectJ,但 Spring 實際上僅僅用了 Aspect 提供的用於切入點解析和匹配的庫來解釋與 AspectJ5 相同的註解。但 AOP 運行時仍然純 Spring AOP 而且不依賴 AspectJ 編譯器或編織器。

可是實際上在 Spring 也可使用原生的 AspectJ(文末有連接傳送門)。 若是您的攔截須要包括目標類中的方法調用甚至構造函數,那麼考慮使用 Spring 驅動的本機 AspectJ 編織,而不是 Spring 的基於代理的 AOP 框架。這構成了具備不一樣特徵的 AOP 使用的不一樣模式,因此在作決定以前必定要熟悉編織。

舉個栗子

我決定從一個例子開始,講解 Spring AOP 在 Spring Boot 的經常使用實踐。下面是步驟說明:

  1. 初始化 Spring Boot
  2. 引入依賴
  3. 編寫 Controller
  4. 編寫切面
  5. 測試

初始化 Spring Boot

初始化方法我通常用如下兩種

  1. 經過 start.spring.io 自定義項目,而後下載/導入。
  2. 經過 IDEA 來新建 Spring Boot 項目。

引入依賴

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
複製代碼
  1. 由於我是經過 web 進行測試的,因此須要引入 web 依賴。
  2. 由於須要切面的依賴,因此引入了 aop

編寫 Controller

demoApplication.java 寫入代碼

@SpringBootApplication
@RestController
public class DemoV2020012901Application {

    public static void main(String[] args) {
        SpringApplication.run(DemoV2020012901Application.class, args);
    }

    @RequestMapping("helloworld")
    public String helloworld() {
        System.out.println("hello world");
        return "helloworld";
    }

}
複製代碼

編寫切面

添加文件夾 aspect,創建一個切面 DemoAspect.java

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/* @Component 註解是必須加的,由於 Spring 須要檢測到這個實體類; 僅僅一個 @Aspect 不足以讓 Spring 檢測獲得。 (XML 模式的話咱們通常是用了 <aop:aspectj-autoproxy/> 讓 Spring 檢測的) */
@Aspect
@Component      
public class DemoAspect {

    @Pointcut("execution(* helloworld(..))") // 切點表達式,屬於 AspectJ 5 標準表達式
    private void pointcut() {}   // 切點簽名

    @Before("pointcut()")
    public void before() {
        System.out.println("before");
    }
}
複製代碼

啓動測試

這時候咱們就能夠啓動應用測試了。測試地址訪問:http://localhost:8080/helloworld

控制檯會輸出

before
hello world
複製代碼

AspectJ 切入點指示器

在上面的例子當中,切點表達式我使用的是 execution。其實 Spring 還提供了許多切入點指示器,與強大的 AspectJ 的切點函數相匹配。

切入點類別
名稱
入參
說明
方法 execution 方法匹配模式串 匹配知足某一模式的全部目標類方法的鏈接點
方法 @annotation 方法註解類名 表示標註了特定註解的目標類方法鏈接點
方法入參 args 類名 經過判別目標類方法運行時入參對象的類型定義指定鏈接點。如 args(com.jc.Student) 表示全部有且僅有一個類型匹配於 Student 入參方法。
方法入參 @args 類型註解類名 經過判斷目標類方法運行時入參對象的類是否標準特定註解來指定鏈接點。如 @args(com.jc.Test) 表示任何這樣的一個目標方法,它有一個入參對象類標註了 @Test 註解。
目標類 within 類名匹配串 表示特定領域下全部的鏈接點。如 within(com.jc.service.*) 是指匹配 service 包下的全部鏈接點,即全部類的全部方法。
目標類 target 類名 目標類按類型匹配於指定類,則目標類的全部鏈接點匹配這個切點。如 @target(com.jc.Student) 定義的切點,StudentStudent 實現類 MaleStudent 中的全部鏈接點都匹配切點
目標類 @within 類名註解類名 假如目標類按照類型匹配於某個類A,且類A標註了特定的註解,則目標類的全部鏈接點匹配這個切點。例如 @within(com.jc.Test),假設 Student 類標註了 @Test 註解,則 Student 及實現累 MaleStudent 的全部鏈接點都匹配這個切點
目標類 @target 類名註解類名 假如目標類標註了特定註解,則目標類的全部鏈接點都匹配該切點。如 @target(com.jc.Test)。若是 Student 類標註了 @Test,則 Student 的全部鏈接點都匹配這個切點。
代理類 this 類名 代理類按類型匹配於指定類,則被代理的目標類的全部鏈接點都匹配該切點

各類指示器均可以試一下,可是咱們最經常使用的是 execution

Advice 的類型

Advice 的類型有五種,能知足大部分的企業應用需求,並且它們也很是好區分。如下就是它的類型:

名字
說明
Before advice 在鏈接點以前運行的通知。 Before advice 不能阻止鏈接點執行,除非代碼拋出異常。
AfterReturning advice 在鏈接點正常完成後運行的通知(例如,若是一個方法沒有拋出異常返回)。
After throwing advice 若是方法經過拋出異常退出,則執行通知。
After(finally) advice 無論鏈接點以何種方式退出(正常或異常返回),都要執行的通知
Around advice 圍繞鏈接點(如方法調用)的通知。這是最強大的advice。Around通知能夠在方法調用先後執行自定義行爲。它還負責選擇是繼續到鏈接點,仍是經過返回它本身的返回值或拋出異常來簡化建議的方法執行。

❤️ 下面是不一樣類型的用法示例:

  • After Returning advice
/* 1. @AfterReturning 有四個參數:value / pointcut / returning /        argNames
       2. 下面的 pointcut 填的也是切面表達式,若是與 value 同時存在,則會覆蓋 value 
       3. retVal 指的是方法返回值的對象 
    */
    @AfterReturning(value = "pointcut()", returning = "retVal")
    public void afterReturning(Object retVal) {
        System.out.println(retVal);
    }
複製代碼
  • After throwing advice
/*
      1. @AfterThrowing 有四個參數:value / pointcut / throwing /        argNames
      2. 使用 throwing 參數,這樣能夠限定異常才執行 advice。若是沒設置則拋出的全部異常都會執行 advice
    */
    @AfterThrowing(pointcut = "pointcut()")
    public void AfterThrowing() {
        System.out.println("AfterThrowing");
    }
複製代碼
  • After(finally) advice
/*
      1. @AfterThrowing 有二個參數:value / argNames
    */
    @After(value = "pointcut()")
    public void AfterThrowing() {
        System.out.println("AfterThrowing");
    }
複製代碼
  • Around advice
/*
      1. @AfterThrowing 有二個參數:value / argNames
    */
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        // start stopwatch
        Object retVal = pjp.proceed();
        // stop stopwatch
        return retVal;
    }
複製代碼

❤️ 關於 Advice 的使用選型,官方建議使用最特定的通知類型能夠提供更簡單的編程模型,減小出錯的可能性。例如,若是隻須要用方法的返回值更新緩存,則最好實現 After return 通知,而不是 Around 通知,儘管 Around 通知能夠完成相同的工做。

傳送門

⭐️ 關因而選擇 Spring AOP 仍是 FULL AspectJ?官網文檔告訴你: Spring AOP or Full AspectJ?

⭐️ 要想徹底使用 AspectJ 的編織器,能夠參考官方文檔: Using AspectJ with Spring Applications

⭐️ 更加了解 Advice?官網文檔告訴你: Declaring Advice

⭐️ 固然,假設你還在使用原生的 Spring XML 配置方式怎麼辦?一個例子帶你飛: Schema-based AOP Support

⭐️ 想了解 Spring 基於代理是怎麼實現 AOP 框架?想看官方是怎麼講解的: Proxying Mechanisms!++下文我會講關於 AOP 在 Spring Boot 的源碼解析喲++。

相關文章
相關標籤/搜索