AOP概念
AOP(Aspect Oriented Programming),即面向切面程式設計,可以說是OOP(Object Oriented Programming,物件導向程式設計)的補充和完善。OOP引入封裝、繼承、多型等概念來建立一種物件層次結構,用於模擬公共行為的一個集合。不過OOP允許開發者定義縱向的關係,但並不適合定義橫向的關係,例如日誌功能。日誌程式碼往往橫向地散佈在所有物件層次中,而與它對應的物件的核心功能毫無關係對於其他型別的程式碼,如安全性、異常處理和透明的持續性也都是如此,這種散佈在各處的無關的程式碼被稱為橫切(cross cutting),在OOP設計中,它導致了大量程式碼的重複,而不利於各個模組的重用。
AOP技術恰恰相反,它利用一種稱為"橫切"的技術,剖解開封裝的物件內部,並將那些影響了多個類的公共行為封裝到一個可重用模組,並將其命名為"Aspect",即切面。所謂"切面",簡單說就是那些與業務無關,卻為業務模組所共同呼叫的邏輯或責任封裝起來,便於減少系統的重複程式碼,降低模組之間的耦合度,並有利於未來的可操作性和可維護性。
使用"橫切"技術,AOP把軟體系統分為兩個部分:核心關注點和橫切關注點。業務處理的主要流程是核心關注點,與之關係不大的部分是橫切關注點。橫切關注點的一個特點是,他們經常發生在核心關注點的多處,而各處基本相似,比如許可權認證、日誌、事物。AOP的作用在於分離系統中的各種關注點,將核心關注點和橫切關注點分離開來。
AOP核心概念
Aspect(切面): Aspect 宣告類似於 Java 中的類宣告,在 Aspect 中會包含著一些 Pointcut 以及相應的 Advice。
Joint point(連線點):表示在程式中明確定義的點,典型的包括方法呼叫,對類成員的訪問以及異常處理程式塊的執行等等,它自身還可以巢狀其它 joint point。
Pointcut(切點):表示一組 joint point,這些 joint point 或是透過邏輯關係組合起來,或是透過通配、正規表示式等方式集中起來,它定義了相應的 Advice 將要發生的地方。
Advice(增強):Advice 定義了在 Pointcut 裡面定義的程式點具體要做的操作,它透過 before、after 和 around 來區別是在每個 joint point 之前、之後還是代替執行的程式碼。
Target(目標物件):織入 Advice 的目標物件.。
Weaving(織入):將 Aspect 和其他物件連線起來, 並建立 Adviced object 的過程
舉例理解
下面我以一個簡單的例子來比喻一下 AOP 中 Aspect, Joint point, Pointcut 與 Advice之間的關係.
讓我們來假設一下, 從前有一個叫爪哇的小縣城, 在一個月黑風高的晚上, 這個縣城中發生了命案. 作案的兇手十分狡猾, 現場沒有留下什麼有價值的線索. 不過萬幸的是, 剛從隔壁回來的老王恰好在這時候無意中發現了兇手行兇的過程, 但是由於天色已晚, 加上兇手蒙著面, 老王並沒有看清兇手的面目, 只知道兇手是個男性, 身高約七尺五寸. 爪哇縣的縣令根據老王的描述, 對守門計程車兵下命令說: 凡是發現有身高七尺五寸的男性, 都要抓過來審問. 士兵當然不敢違背縣令的命令, 只好把進出城的所有符合條件的人都抓了起來.
來讓我們看一下上面的一個小故事和 AOP 到底有什麼對應關係.
首先我們知道, 在 Spring AOP 中 Joint point 指代的是所有方法的執行點, 而 point cut 是一個描述資訊, 它修飾的是 Joint point, 透過 point cut, 我們就可以確定哪些 Joint point 可以被織入 Advice. 對應到我們在上面舉的例子, 我們可以做一個簡單的類比, Joint point 就相當於 爪哇的小縣城裡的百姓,pointcut 就相當於 老王所做的指控, 即兇手是個男性, 身高約七尺五寸, 而 Advice 則是施加在符合老王所描述的嫌疑人的動作: 抓過來審問.
為什麼可以這樣類比呢?
Joint point : 爪哇的小縣城裡的百姓: 因為根據定義, Joint point 是所有可能被織入 Advice 的候選的點, 在 Spring AOP中, 則可以認為所有方法執行點都是 Joint point. 而在我們上面的例子中, 命案發生在小縣城中, 按理說在此縣城中的所有人都有可能是嫌疑人.
Pointcut :男性, 身高約七尺五寸: 我們知道, 所有的方法(joint point) 都可以織入 Advice, 但是我們並不希望在所有方法上都織入 Advice, 而 Pointcut 的作用就是提供一組規則來匹配joinpoint, 給滿足規則的 joinpoint 新增 Advice. 同理, 對於縣令來說, 他再昏庸, 也知道不能把縣城中的所有百姓都抓起來審問, 而是根據兇手是個男性, 身高約七尺五寸, 把符合條件的人抓起來. 在這裡 兇手是個男性, 身高約七尺五寸 就是一個修飾謂語, 它限定了兇手的範圍, 滿足此修飾規則的百姓都是嫌疑人, 都需要抓起來審問.
Advice :抓過來審問, Advice 是一個動作, 即一段 Java 程式碼, 這段 Java 程式碼是作用於 point cut 所限定的那些 Joint point 上的. 同理, 對比到我們的例子中, 抓過來審問 這個動作就是對作用於那些滿足 男性, 身高約七尺五寸 的爪哇的小縣城裡的百姓.
Aspect::Aspect 是 point cut 與 Advice 的組合, 因此在這裡我們就可以類比: “根據老王的線索, 凡是發現有身高七尺五寸的男性, 都要抓過來審問” 這一整個動作可以被認為是一個 Aspect.
程式碼案例
我們首先建立配置類Config,在此類開啟AspectJ註解@EnableAspectJAutoProxy
注意:在最新版本的Spring框架中,@EnableAspectJAutoProxy
註解不是必需的。當你使用<aop:aspectj-autoproxy>
配置或者Spring Boot時,它會自動啟用AspectJ自動代理。
Spring框架會根據以下條件自動啟用AspectJ自動代理:
- 在類路徑下存在AspectJ織入器(例如,AspectJ的相關依賴已經被引入)。
- Spring上下文中存在至少一個
@Aspect
註解的切面類。
因此,如果你的專案滿足這些條件,你無需顯式地使用@EnableAspectJAutoProxy
註解。Spring框架會自動探測並啟用AspectJ自動代理。
Config類:
package com.xsh.springaop;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@ComponentScan
@Configuration
//開啟AspectJ註解
@EnableAspectJAutoProxy
public class Config {
}
下列程式碼是一個簡單的AOP切面示例,用於在目標方法執行之前和執行之後列印日誌。
@Aspect
註解表示這是一個切面類,用於宣告切面的功能。@Component
註解表示這個切面類是一個Spring元件,將被Spring容器管理。@Before("execution(* com.xsh.springaop.MyServer.fun1(..))")
註解表示這個方法將在目標方法執行之前執行。它使用了切點表示式來指定切入的連線點。在本例中,切點表示式execution(* com.xsh.springaop.MyServer.fun1(..))
表示匹配com.xsh.springaop.MyServer
類中的fun1
方法,並且方法引數任意。@After("execution(* com.xsh.springaop.MyServer.fun2(..))")
註解表示這個方法將在目標方法執行之後執行。切點表示式與上述相似,匹配com.xsh.springaop.MyServer
類中的fun2
方法。beforeMethodExecution()
方法是前置通知方法,它在目標方法執行之前被呼叫,列印了一條日誌資訊。afterAdvice()
方法是後置通知方法,它在目標方法執行之後被呼叫,列印了一條日誌資訊。
透過這種方式,可以在不修改原有業務邏輯的情況下,透過AOP切面對目標方法進行增強操作,例如記錄日誌、效能監控、事務管理等。在示例中,切面類LoggingAspect
將在目標方法執行前後輸出日誌資訊。
LoggingAspect:
package com.xsh.springaop;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.xsh.springaop.MyServer.fun1(..))")
public void beforeMethodExecution() {
System.out.println("Before executing method");
}
@After("execution(* com.xsh.springaop.MyServer.fun2(..))")
public void afterAdvice() {
System.out.println("After executing method");
}
}
下列程式碼是有2個方法,分別對應切面類中的所切方法。
MyServer:
package com.xsh.springaop;
import org.springframework.stereotype.Service;
@Service
public class MyServer {
public void fun1() {
System.out.println("我是方法1111");
}
public void fun2(){
System.out.println("我是方法2222");
}
}
下列程式碼是一個Spring Boot的應用程式啟動類。它實現了CommandLineRunner
介面,用於在應用程式啟動後執行一些初始化任務。
@Component
註解表示這個類是一個Spring元件,將被Spring容器管理。AppRunner
類實現了CommandLineRunner
介面,它定義了一個run
方法,在應用程式啟動後會被自動呼叫。- 在
AppRunner
類中,使用@Autowired
註解將MyServer
類的例項自動注入進來,即將MyServer
物件注入到myServer
欄位中。 - 在
run
方法中,呼叫了myServer
物件的fun1()
和fun2()
方法,即執行了目標方法。
透過這種方式,當應用程式啟動後,AppRunner
類的run
方法將會被自動呼叫,從而觸發執行MyServer
類中的目標方法fun1()
和fun2()
。這樣可以方便地測試和驗證切面是否生效,是否正確地增強了目標方法的功能。
AppRunner:
package com.xsh.springaop;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class AppRunner implements CommandLineRunner {
@Autowired
private MyServer myServer;
@Override
public void run(String... args) throws Exception {
myServer.fun1();
myServer.fun2();
}
}