Spring基於註解實現 AOP 切面功能

[奋斗]發表於2024-12-02

前言

在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在效能監控、日誌記錄、事務管理等場景中的應用,展示了其在提高程式碼模組化和可維護性方面的強大能力。

相關文章