Spring基于注解實現 AOP 切面功能
前言
在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在性能監控、日志記錄、事務管理等場景中的應用,展示了其在提高代碼模塊化和可維護性方面的強大能力。

浙公網安備 33010602011771號