Spring系列:基於Spring-AOP和Spring-Aspects實現AOP切面程式設計

Code技術分享發表於2023-12-14

一、概念及相關術語

概念

AOP(Aspect Oriented Programming)是一種設計思想,是軟體設計領域中的面向切面程式設計,它是物件導向程式設計的一種補充和完善,它以透過預編譯方式和執行期動態代理方式實現,在不修改原始碼的情況下,給程式動態統一新增額外功能的一種技術。利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程式的可重用性,同時提高了開發的效率。

相關術語

①橫切關注點

分散在每個各個模組中解決同一樣的問題,如使用者驗證、日誌管理、事務處理、資料快取都屬於橫切關注點。

從每個方法中抽取出來的同一類非核心業務。在同一個專案中,我們可以使用多個橫切關注點對相關方法進行多個不同方面的增強。

這個概念不是語法層面的,而是根據附加功能的邏輯上的需要:有十個附加功能,就有十個橫切關注點。

image

②通知(增強)

增強,通俗說,就是你想要增強的功能,比如 安全,事務,日誌等。

每一個橫切關注點上要做的事情都需要寫一個方法來實現,這樣的方法就叫通知方法。

  • 前置通知:在被代理的目標方法執行
  • 返回通知:在被代理的目標方法成功結束後執行(壽終正寢
  • 異常通知:在被代理的目標方法異常結束後執行(死於非命
  • 後置通知:在被代理的目標方法最終結束後執行(蓋棺定論
  • 環繞通知:使用try...catch...finally結構圍繞整個被代理的目標方法,包括上面四種通知對應的所有位置

image

③切面

封裝通知方法的類。

image

④目標

被代理的目標物件。

⑤代理

向目標物件應用通知之後建立的代理物件。

⑥連線點

這也是一個純邏輯概念,不是語法定義的。

把方法排成一排,每一個橫切位置看成x軸方向,把方法從上到下執行的順序看成y軸,x軸和y軸的交叉點就是連線點。通俗說,就是spring允許你使用通知的地方

image

⑦切入點

定位連線點的方式。

每個類的方法中都包含多個連線點,所以連線點是類中客觀存在的事物(從邏輯上來說)。

如果把連線點看作資料庫中的記錄,那麼切入點就是查詢記錄的 SQL 語句。

Spring 的 AOP 技術可以透過切入點定位到特定的連線點。通俗說,要實際去增強的方法

切點透過 org.springframework.aop.Pointcut 介面進行描述,它使用類和方法作為連線點的查詢條件。

作用

  • 簡化程式碼:把方法中固定位置的重複的程式碼抽取出來,讓被抽取的方法更專注於自己的核心功能,提高內聚性。

  • 程式碼增強:把特定的功能封裝到切面類中,看哪裡有需要,就往上套,被套用了切面邏輯的方法就被切面給增強了。

二、基於註解的AOP

技術說明

image

image

  • 動態代理分為JDK動態代理和cglib動態代理
  • 當目標類有介面的情況使用JDK動態代理和cglib動態代理,沒有介面時只能使用cglib動態代理
  • JDK動態代理動態生成的代理類會在com.sun.proxy包下,類名為$proxy1,和目標類實現相同的介面
  • cglib動態代理動態生成的代理類會和目標在在相同的包下,會繼承目標類
  • 動態代理(InvocationHandler):JDK原生的實現方式,需要被代理的目標類必須實現介面。因為這個技術要求代理物件和目標物件實現同樣的介面(兄弟兩個拜把子模式)。
  • cglib:透過繼承被代理的目標類(認乾爹模式)實現代理,所以不需要目標類實現介面。
  • AspectJ:是AOP思想的一種實現。本質上是靜態代理,將代理邏輯“織入”被代理的目標類編譯得到的位元組碼檔案,所以最終效果是動態的。weaver就是織入器。Spring只是借用了AspectJ中的註解

準備工作

①新增依賴

在IOC所需依賴基礎上再加入下面依賴即可:

        <!--spring context依賴-->
        <!--當你引入Spring Context依賴之後,表示將Spring的基礎依賴引入了-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>6.1.1</version>
        </dependency>
        <!--spring aop依賴-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>6.1.1</version>
        </dependency>
        <!--spring aspects依賴-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>6.1.1</version>
        </dependency>
        <!--junit5-->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.9.3</version>
        </dependency>
        <!--log4j2的依賴-->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.20.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j2-impl</artifactId>
            <version>2.20.0</version>
        </dependency>

②準備被代理的目標資源

介面:

package com.mcode.annotationaop;

/**
 * ClassName: Calculator
 * Package: com.mcode.annotationaop
 * Description:
 *
 * @Author: robin
 * @Version: v1.0
 */
public interface Calculator {
    int add(int i, int j);

    int sub(int i, int j);

    int mul(int i, int j);

    int div(int i, int j);
}

實現類:

package com.mcode.annotationaop;

import org.springframework.stereotype.Component;

/**
 * ClassName: CalculatorImpl
 * Package: com.mcode.annotationaop
 * Description:
 *
 * @Author: robin
 * @Version: v1.0
 */
@Component
public class CalculatorImpl implements Calculator {
    @Override
    public int add(int i, int j) {

        int result = i + j;

        System.out.println("方法內部 result = " + result);

        //為了測試,模擬異常出現
        //int a = 1/0;
        return result;
    }

    @Override
    public int sub(int i, int j) {

        int result = i - j;

        System.out.println("方法內部 result = " + result);

        return result;
    }

    @Override
    public int mul(int i, int j) {

        int result = i * j;

        System.out.println("方法內部 result = " + result);

        return result;
    }

    @Override
    public int div(int i, int j) {

        int result = i / j;

        System.out.println("方法內部 result = " + result);

        return result;
    }
}

建立切面類並配置

package com.mcode.annotationaop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.util.Arrays;

/**
 * ClassName: LogAspect
 * Package: com.mcode.annotationaop
 * Description: 切面類
 *
 * @Author: robin
 * @Version: v1.0
 */
@Aspect //切面類
@Component //ioc容器
public class LogAspect {

    //設定切入點和通知型別
    //切入點表示式: execution(訪問修飾符 增強方法返回型別 增強方法所在類全路徑.方法名稱(方法引數))
    //通知型別:
    // 前置 @Before(value="切入點表示式配置切入點")
    //@Before(value = "execution(* com.mcode.annotationaop.CalculatorImpl.*(..))")
    @Before("execution(public int  com.mcode.annotationaop.CalculatorImpl.*(..))")
    public void beforeMethod(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        System.out.println("Logger-->前置通知,方法名稱:" + methodName + ",引數:" + Arrays.toString(args));
    }

    // 後置 @After()
    @After("execution(* com.mcode.annotationaop.CalculatorImpl.*(..))")
    public void afterMethod(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        System.out.println("Logger-->後置通知,方法名稱:" + methodName + ",引數:" + Arrays.toString(args));
    }

    // 返回 @AfterReturning
    @AfterReturning(value = "execution(* com.mcode.annotationaop.CalculatorImpl.*(..))",
            returning = "result")
    public void afterReturningMethod(JoinPoint joinPoint, Object result) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Logger-->返回通知,方法名稱:" + methodName + ",返回結果:" + result);
    }

    // 異常 @AfterThrowing 獲取到目標方法異常資訊
    //目標方法出現異常,這個通知執行
    @AfterThrowing(value = "execution(* com.mcode.annotationaop.CalculatorImpl.*(..))", throwing =
            "ex")
    public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Logger-->異常通知,方法名稱:" + methodName + ",異常資訊:" + ex);
    }

    // 環繞 @Around()
    @Around("execution(* com.mcode.annotationaop.CalculatorImpl.*(..))")
    public Object aroundMethod(ProceedingJoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        String argString = Arrays.toString(args);
        Object result = null;
        try {
            System.out.println("環繞通知-->目標物件方法執行之前");
            //呼叫目標方法
            result = joinPoint.proceed();
            System.out.println("環繞通知-->目標物件方法返回值之後");
        } catch (Throwable e) {
           e.printStackTrace();
            System.out.println("環繞通知-->目標物件方法出現異常時");
        } finally {
            System.out.println("環繞通知-->目標物件方法執行完畢");
        }
        return result;
    }

}

新增Spring配置類

package com.mcode.annotationaop;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

/**
 * ClassName: SpringConfig
 * Package: com.mcode.annotationaop
 * Description:
 *  基於註解的AOP的實現:
 *   1、將目標物件和切面交給IOC容器管理(註解+掃描)
 *   2、開啟AspectJ的自動代理,為目標物件自動生成代理
 *   3、將切面類透過註解@Aspect標識
 * @Author: robin
 * @Version: v1.0
 */
@Configuration
@ComponentScan("com.mcode.annotationaop")
@EnableAspectJAutoProxy //開啟AspectJ的自動代理,為目標物件自動生成代理
public class SpringConfig {
}

執行測試:

package com.mcode.annotationaop;

import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

/**
 * ClassName: TestAop
 * Package: com.mcode.annotationaop
 * Description:
 *
 * @Author: robin
 * @Version: v1.0
 */
public class TestAop {
    @Test
    public void testAdd(){
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
        Calculator calculator = context.getBean(Calculator.class);
        calculator.add(1,2);
    }
}

執行結果:

image

各種通知

  • 前置通知:使用@Before註解標識,在被代理的目標方法執行
  • 返回通知:使用@AfterReturning註解標識,在被代理的目標方法成功結束後執行(壽終正寢
  • 異常通知:使用@AfterThrowing註解標識,在被代理的目標方法異常結束後執行(死於非命
  • 後置通知:使用@After註解標識,在被代理的目標方法最終結束後執行(蓋棺定論
  • 環繞通知:使用@Around註解標識,使用try...catch...finally結構圍繞整個被代理的目標方法,包括上面四種通知對應的所有位置

各種通知的執行順序:

  • Spring版本5.3.x以前:
    • 前置通知
    • 目標操作
    • 後置通知
    • 返回通知或異常通知
  • Spring版本5.3.x以後:
    • 前置通知
    • 目標操作
    • 返回通知或異常通知
    • 後置通知

切入點表示式語法

①作用

image

②語法細節

  • 用*號代替“許可權修飾符”和“返回值”部分表示“許可權修飾符”和“返回值”不限

  • 在包名的部分,一個“*”號只能代表包的層次結構中的一層,表示這一層是任意的。

    • 例如:*.Hello匹配com.Hello,不匹配com.atguigu.Hello
  • 在包名的部分,使用“*..”表示包名任意、包的層次深度任意

  • 在類名的部分,類名部分整體用*號代替,表示類名任意

  • 在類名的部分,可以使用*號代替類名的一部分

    • 例如:*Service匹配所有名稱以Service結尾的類或介面
  • 在方法名部分,可以使用*號表示方法名任意

  • 在方法名部分,可以使用*號代替方法名的一部分

    • 例如:*Operation匹配所有方法名以Operation結尾的方法
  • 在方法引數列表部分,使用(..)表示引數列表任意

  • 在方法引數列表部分,使用(int,..)表示引數列表以一個int型別的引數開頭

  • 在方法引數列表部分,基本資料型別和對應的包裝型別是不一樣的

    • 切入點表示式中使用 int 和實際方法中 Integer 是不匹配的
  • 在方法返回值部分,如果想要明確指定一個返回值型別,那麼必須同時寫明許可權修飾符

    • 例如:execution(public int ..Service.(.., int)) 正確
      例如:execution(
      int ..Service.*(.., int)) 錯誤

image

重用切入點表示式

①宣告

@Pointcut("execution(* com.mcode.annotationaop.CalculatorImpl.*(..))")
public void pointCut(){}

②在同一個切面中使用

@Before("pointCut()")
public void beforeMethod(JoinPoint joinPoint){
    String methodName = joinPoint.getSignature().getName();
    String args = Arrays.toString(joinPoint.getArgs());
    System.out.println("Logger-->前置通知,方法名:"+methodName+",引數:"+args);
}

③在不同切面中使用

@Before("com.mcode.annotationaop.LogAspect.pointCut()")
public void beforeMethod(JoinPoint joinPoint){
    String methodName = joinPoint.getSignature().getName();
    String args = Arrays.toString(joinPoint.getArgs());
    System.out.println("Logger-->前置通知,方法名:"+methodName+",引數:"+args);
}

獲取通知的相關資訊

①獲取連線點資訊

獲取連線點資訊可以在通知方法的引數位置設定JoinPoint型別的形參

    @Before("execution(public int  com.mcode.annotationaop.CalculatorImpl.*(..))")
    public void beforeMethod(JoinPoint joinPoint) {
        //獲取連線點的簽名資訊
        String methodName = joinPoint.getSignature().getName();
        //獲取目標方法到的實參資訊
        Object[] args = joinPoint.getArgs();
        System.out.println("Logger-->前置通知,方法名稱:" + methodName + ",引數:" + Arrays.toString(args));
    }

②獲取目標方法的返回值

@AfterReturning中的屬性returning,用來將通知方法的某個形參,接收目標方法的返回值

    // 返回 @AfterReturning
    @AfterReturning(value = "execution(* com.mcode.annotationaop.CalculatorImpl.*(..))",returning = "result")
    public void afterReturningMethod(JoinPoint joinPoint, Object result) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Logger-->返回通知,方法名稱:" + methodName + ",返回結果:" + result);
    }

③獲取目標方法的異常

@AfterThrowing中的屬性throwing,用來將通知方法的某個形參,接收目標方法的異常

    @AfterThrowing(value = "execution(* com.mcode.annotationaop.CalculatorImpl.*(..))", throwing ="ex")
    public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Logger-->異常通知,方法名稱:" + methodName + ",異常資訊:" + ex);
    }

環繞通知

    @Around("execution(* com.mcode.annotationaop.CalculatorImpl.*(..))")
    public Object aroundMethod(ProceedingJoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        String argString = Arrays.toString(args);
        Object result = null;
        try {
            System.out.println("環繞通知-->目標物件方法執行之前");
            //目標方法的執行,目標方法的返回值一定要返回給外界呼叫者,否則會報錯
            result = joinPoint.proceed();
            System.out.println("環繞通知-->目標物件方法返回值之後");
        } catch (Throwable e) {
           e.printStackTrace();
            System.out.println("環繞通知-->目標物件方法出現異常時");
        } finally {
            System.out.println("環繞通知-->目標物件方法執行完畢");
        }
        return result;
    }

切面的優先順序

相同目標方法上同時存在多個切面時,切面的優先順序控制切面的內外巢狀順序。

  • 優先順序高的切面:外面
  • 優先順序低的切面:裡面

使用@Order註解可以控制切面的優先順序:

  • @Order(較小的數):優先順序高
  • @Order(較大的數):優先順序低
@Order(1) //優先順序
public class LogAspect {
}

image

三、基於XML的AOP

準備工作

在Spring的配置檔案中配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--開啟元件掃描-->
    <context:component-scan base-package="com.mcode.xmlaop"/>

    <!--配置aop五種通知型別-->
    <aop:config>
        <!--配置切面類-->
        <aop:aspect ref="logAspect">
            <!--配置切入點-->
            <aop:pointcut id="pointcut"
                          expression="execution(* com.mcode.xmlaop.CalculatorImpl.*(..))"/>
            <!--配置五種通知型別-->
            <!--前置通知-->
            <aop:before method="beforeMethod"
                        pointcut="execution(public int com.mcode.xmlaop.CalculatorImpl.*(..))"/>
            <!--後置通知-->
            <aop:after method="afterMethod" pointcut-ref="pointcut"/>
            <!--返回通知-->
            <aop:after-returning method="afterReturningMethod" pointcut-ref="pointcut" returning="result"/>
            <!--異常通知-->
            <aop:after-throwing method="afterThrowingMethod" pointcut-ref="pointcut" throwing="ex"/>
            <!--環繞通知-->
            <aop:around method="aroundMethod" pointcut-ref="pointcut"/>
        </aop:aspect>
    </aop:config>
</beans>

實現

建立切面類並配置:

package com.mcode.xmlaop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.util.Arrays;

/**
 * ClassName: LogAspect
 * Package: com.mcode.xmlaop
 * Description: 切面類
 *
 * @Author: robin
 * @Version: v1.0
 */
@Component //ioc容器
public class LogAspect {

    // 前置
    public void beforeMethod(JoinPoint joinPoint) {
        //獲取連線點的簽名資訊
        String methodName = joinPoint.getSignature().getName();
        //獲取目標方法到的實參資訊
        Object[] args = joinPoint.getArgs();
        System.out.println("Logger-->前置通知,方法名稱:" + methodName + ",引數:" + Arrays.toString(args));
    }

    // 後置
    public void afterMethod(JoinPoint joinPoint) {
        //獲取連線點的簽名資訊
        String methodName = joinPoint.getSignature().getName();
        //獲取目標方法到的實參資訊
        Object[] args = joinPoint.getArgs();
        System.out.println("Logger-->後置通知,方法名稱:" + methodName + ",引數:" + Arrays.toString(args));
    }

    // 返回
    public void afterReturningMethod(JoinPoint joinPoint, Object result) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Logger-->返回通知,方法名稱:" + methodName + ",返回結果:" + result);
    }

    // 異常
    public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Logger-->異常通知,方法名稱:" + methodName + ",異常資訊:" + ex);
    }

    // 環繞
    public Object aroundMethod(ProceedingJoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        String argString = Arrays.toString(args);
        Object result = null;
        try {
            System.out.println("環繞通知-->目標物件方法執行之前");
            //目標方法的執行,目標方法的返回值一定要返回給外界呼叫者,否則會報錯
            result = joinPoint.proceed();
            System.out.println("環繞通知-->目標物件方法返回值之後");
        } catch (Throwable e) {
           e.printStackTrace();
            System.out.println("環繞通知-->目標物件方法出現異常時");
        } finally {
            System.out.println("環繞通知-->目標物件方法執行完畢");
        }
        return result;
    }
}

執行測試:

package com.mcode.xmlaop;

import com.mcode.annotationaop.Calculator;
import org.junit.jupiter.api.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * ClassName: TestAop
 * Package: com.mcode.annotationaop
 * Description:
 *
 * @Author: robin
 * @Version: v1.0
 */
public class TestAop {
    @Test
    public void testAdd(){
        ClassPathXmlApplicationContext context =
                new ClassPathXmlApplicationContext("beans.xml");
        Calculator calculator = context.getBean(Calculator.class);
        calculator.add(1,2);
    }
}

執行結果:

image

相關文章