前言
在Spring AOP(Aspect-Oriented Programming)中,動態代理是常用的技術之一,用於在執行時動態地為目標物件生成代理物件,
並攔截其方法呼叫。Spring AOP 預設使用兩種型別的動態代理機制:JDK 動態代理和 CGLIB 代理。
JDK 動態代理:
JDK 動態代理是 Java 原生提供的動態代理機制,它只能代理介面。如果你的目標物件實現了某個介面,Spring AOP 會預設使用 JDK 動態代理。
JDK 動態代理機制透過 java.lang.reflect.Proxy 類來建立代理物件,並將方法呼叫委託給 InvocationHandler 實現。
CGLIB 代理:
如果目標物件沒有實現介面,Spring AOP 會使用 CGLIB(Code Generation Library)來生成代理物件。CGLIB 是一個強大的庫,
可以生成目標物件的子類,並覆蓋其方法以實現代理功能。透過 CGLIB,Spring AOP 能夠代理沒有實現介面的類(即具體的類)。
預設代理機制的選擇
Spring AOP 在選擇使用哪種代理機制時,遵循以下原則:
如果目標物件實現了至少一個介面,則預設使用 JDK 動態代理。
如果目標物件沒有實現任何介面,則預設使用 CGLIB 代理。
配置示例
在大多數情況下,你不需要顯式地指定使用哪種代理機制,因為 Spring 會自動為你選擇。但是,如果你有特殊需求,可以透過配置來強制使用某種代理機制。
一、Spring AOP 註解概述
1.Spring 的 AOP 功能除了在配置檔案中配置一大堆的配置,比如切入點、表示式、通知等等以外,
使用註解的方式更為方便快捷,特別是 Spring boot 出現以後,基本不再使用原先的 beans.xml 等配置檔案了,而都推薦註解程式設計
@Aspect | 切面宣告,標註在類、介面(包括註解型別)或列舉上。 |
@Pointcut |
切入點宣告,即切入到哪些目標類的目標方法。既可以用 execution 切點表示式, 也可以是 annotation 指定攔截擁有指定註解的方法. value 屬性指定切入點表示式,預設為 "",用於被通知註解引用,這樣通知註解只需要關聯此切入點宣告即可,無需再重複寫切入點表示式 |
@Before |
前置通知, 在目標方法(切入點)執行之前執行。 value 屬性繫結通知的切入點表示式,可以關聯切入點宣告,也可以直接設定切入點表示式 注意:如果在此回撥方法中丟擲異常,則目標方法不會再執行,會繼續執行後置通知 -> 異常通知。 |
@After | 後置通知, 在目標方法(切入點)執行之後執行 |
@AfterReturning |
返回通知, 在目標方法(切入點)返回結果之後執行. pointcut 屬性繫結通知的切入點表示式,優先順序高於 value,預設為 "" |
@AfterThrowing |
異常通知, 在方法丟擲異常之後執行, 意味著跳過返回通知 pointcut 屬性繫結通知的切入點表示式,優先順序高於 value,預設為 "" 注意:如果目標方法自己 try-catch 了異常,而沒有繼續往外拋,則不會進入此回撥函式 |
@Around |
環繞通知:目標方法執行前後分別執行一些程式碼,類似攔截器,可以控制目標方法是否繼續執行。 通常用於統計方法耗時,引數校驗等等操作。 |
2、上面這些 AOP 註解都是位於 aspectjweaver 依賴中;對於習慣了 Spring 全家桶程式設計的人來說,並不是需要直接引入 aspectjweaver 依賴,因為 spring-boot-starter-aop 元件預設已經引用了 aspectjweaver 來實現 AOP 功能。換句話說 Spring 的 AOP 功能就是依賴的 aspectjweaver !
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> <version>2.1.4.RELEASE</version> </dependency>
3.AOP 底層是透過 Spring 提供的的動態代理技術實現的,在執行期間動態生成代理物件,代理物件方法執行時進行增強功能的介入,再去呼叫目標物件的方法,從而完成功能的增強。主要使用 JDK 動態代理與 Cglib 動態代理;所以如果目標類不是 Spring 元件,則無法攔截,如果是 類名.方法名 方式呼叫,也無法攔截。
二、@Aspect 快速入門
1、@Aspect 常見用於記錄日誌、異常集中處理、許可權驗證、Web 引數校驗、事務處理等等 2、要想把一個類變成切面類,只需3步: 1)在類上使用 @Aspect 註解使之成為切面類 2)切面類需要交由 Spring 容器管理,所以類上還需要有 @Service、 @Repository、@Controller、@Component 等註解 2)在切面類中自定義方法接收通知 3、AOP 的含義就不再累述了,下面直接上示例:
/** * 切面類,用於處理日誌、引數校驗等 * * @author songwp * @date 2020-04-27 */ @Aspect @Component @Slf4j public class HandleAspect { /** * @Pointcut :切入點宣告,即切入到哪些目標方法。value 屬性指定切入點表示式,預設為 ""。 * 用於被下面的通知註解引用,這樣通知註解只需要關聯此切入點宣告即可,無需再重複寫切入點表示式 * <p> * 切入點表示式常用格式舉例如下: * - * com.songwp.aspect.EmpService.*(..)):表示 com.songwp.aspect.EmpService 類中的任意方法 * - * com.songwp.aspect.*.*(..)):表示 com.songwp.aspect 包(不含子包)下任意類中的任意方法 * - * com.songwp.aspect..*.*(..)):表示 com.songwp.aspect 包及其子包下任意類中的任意方法 * </p> * value 的 execution 可以有多個,使用 || 隔開. */ @Pointcut("execution(public * com.songwp.controller.*.*(..))") public void aopPointCut() {} /** * 前置通知:目標方法執行之前執行以下方法體的內容。 * value:繫結通知的切入點表示式。可以關聯切入點宣告,也可以直接設定切入點表示式 * <br/> * * @param joinPoint:提供對連線點處可用狀態和有關它的靜態資訊的反射訪問<br/> <p> * * * Object[] getArgs():返回此連線點處(目標方法)的引數,目標方法無引數時,返回空陣列 * * * Signature getSignature():返回連線點處的簽名。 * * * Object getTarget():返回目標物件 * * * Object getThis():返回當前正在執行的物件 * * * StaticPart getStaticPart():返回一個封裝此連線點的靜態部分的物件。 * * * SourceLocation getSourceLocation():返回與連線點對應的源位置 * * * String toLongString():返回連線點的擴充套件字串表示形式。 * * * String toShortString():返回連線點的縮寫字串表示形式。 * * * String getKind():返回表示連線點型別的字串 * * * </p> */ @Before("aopPointCut()") public void beforeAdvice() { System.out.println("前置通知執行"); } /** * 後置通知:目標方法執行之後執行以下方法體的內容,不管目標方法是否發生異常。 * value:繫結通知的切入點表示式。可以關聯切入點宣告,也可以直接設定切入點表示式 */ @After("aopPointCut()") public void afterAdvice() { System.out.println("後置通知執行"); } /** * 返回通知:目標方法返回後執行以下程式碼 * value 屬性:繫結通知的切入點表示式。可以關聯切入點宣告,也可以直接設定切入點表示式 * pointcut 屬性:繫結通知的切入點表示式,優先順序高於 value,預設為 "" * returning 屬性:通知簽名中要將返回值繫結到的引數的名稱,預設為 "" * * @param joinPoint :提供對連線點處可用狀態和有關它的靜態資訊的反射訪問 */ @AfterReturning("execution(* com.songwp.service.impl.OperateLogServiceImpl.*(..))") public void logAfterReturning(JoinPoint joinPoint) { System.out.println("返回後通知: " + joinPoint.getSignature().getName()); } /** * 異常通知:目標方法發生異常的時候執行以下程式碼,此時返回通知不會再觸發 * value 屬性:繫結通知的切入點表示式。可以關聯切入點宣告,也可以直接設定切入點表示式 * pointcut 屬性:繫結通知的切入點表示式,優先順序高於 value,預設為 "" * throwing 屬性:與方法中的異常引數名稱一致, * * @param ex:捕獲的異常物件,名稱與 throwing 屬性值一致 */ @AfterThrowing(pointcut = "execution(* com.songwp.service.impl.OperateLogServiceImpl.*(..))", throwing = "ex") public void logAfterThrowing(JoinPoint joinPoint, Throwable ex) { System.out.println("異常後通知: " + joinPoint.getSignature().getName() + ", Exception: " + ex); } /** * 環繞通知 * 1、@Around 的 value 屬性:繫結通知的切入點表示式。可以關聯切入點宣告,也可以直接設定切入點表示式 * 2、Object ProceedingJoinPoint.proceed(Object[] args) 方法:繼續下一個通知或目標方法呼叫,返回處理結果,如果目標方法發生異常,則 proceed 會拋異常. * 3、假如目標方法是控制層介面,則本方法的異常捕獲與否都不會影響目標方法的事務回滾 * 4、假如目標方法是控制層介面,本方法 try-catch 了異常後沒有繼續往外拋,則全域性異常處理 @RestControllerAdvice 中不會再觸發 * * @param joinPoint * @return * @throws Throwable */ @Around("execution(* com.songwp.service.impl.OperateLogServiceImpl.*(..))") public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { this.checkRequestParam(joinPoint); System.out.println("環繞通知: " + joinPoint.getSignature().getName()); // 繼續執行方法 Object result = joinPoint.proceed(); System.out.println("環繞通知: " + joinPoint.getSignature().getName()); return result; } /** * 引數校驗,防止 SQL 注入 * * @param joinPoint */ private void checkRequestParam(ProceedingJoinPoint joinPoint) { Object[] args = joinPoint.getArgs(); if (args == null || args.length <= 0) { return; } String params = Arrays.toString(joinPoint.getArgs()).toUpperCase(); String[] keywords = {"DELETE ", "UPDATE ", "SELECT ", "INSERT ", "SET ", "SUBSTR(", "COUNT(", "DROP ", "TRUNCATE ", "INTO ", "DECLARE ", "EXEC ", "EXECUTE ", " AND ", " OR ", "--"}; for (String keyword : keywords) { if (params.contains(keyword)) { log.error("引數存在SQL隱碼攻擊風險,其中包含非法字元 {}.", keyword); throw new RuntimeException("引數存在SQL隱碼攻擊風險:params=" + params); } } } }
三、@Aspect 切面不生效原因
確保切面類被Spring管理:在切面類上新增 @Service、@Repository、@Controller、@Component 等註解 |
檢查路徑設定:確保切面類被 @ComponentScan 註解掃描到。即有沒有被Spring容器管理。可以使用 @PostConstruct註解測試。 |
檢查切面表示式:確保切面表示式正確無誤,能夠匹配到目標方法。 |
特別注意: 比如定義了一個 AOP 切面(@Pointcut)攔截 ServiceA 中的方法 B,當從其他類呼叫方法 B 時(比如 Controller 層),會正常切入攔截,而從本類其他方法中呼叫方法 B 時,無法切入攔截,因為此時預設並不是透過代理物件呼叫的,而是直接透過 this 物件來調的。可以參考@EnableAspectJAutoProxy註解。 |
總結:AOP的高階特性使得開發者能夠以宣告式的方式處理複雜的應用場景。透過靈活使用切入點表示式和正規表示式,可以在Spring AOP中實現精確的連線點匹配。此外,AOP在效能監控、日誌記錄、事務管理等場景中的應用,展示了其在提高程式碼模組化和可維護性方面的強大能力。