AOP面向切面編程

一、AOP概述

1. AOP簡介

AOP(Aspect Orient Programming),面向切面編程。面向切面編程是從動態角度考慮程式運行過程
AOP 底層,就是採用動態代理模式實現的。採用了兩種代理:JDK 的動態代理,與 CGLIB的動態代理

可以看之前寫的動態代理,

//www.cnblogs.com/mengd/p/13429797.html

  1. jdk動態代理:使用jdk中的Proxy,Method,InvocaitonHanderl創建代理對象,jdk動態代理要求目標類必須實現介面
  2. cglib動態代理:第三方的工具庫,創建代理對象,原理是繼承。 通過繼承目標類,創建子類,子類就是代理對象。 要求目標類不能是final的, 方法也不能是final的

動態代理的作用:

  • 在目標類源程式碼不改變的情況下,增加功能
  • 減少程式碼的重複
  • 專註業務邏輯程式碼
  • 解耦合,讓你的業務功能和日誌分離,事務和非業務功能分離

2. 如何理解AOP

AOP(Aspect Orient Programming)面向切面編程

  • Aspect: 切面,給你的目標類增加的功能,就是切面,切面的特點: 一般都是非業務方法,獨立使用的
  • Orient:面向
  • oop: 面向對象編程

理解:

  1. 需要在分析項目功能時,找出切面
  2. 合理的安排切面的執行時間(在目標方法前, 還是目標方法後)
  3. 合理的安全切面執行的位置,在哪個類,哪個方法增加增強功能

二、AOP編程術語

1. 切面(Aspect)

表示增強的功能, 就是一堆程式碼,完成某個一個功能,非業務功能

常見的切面功能有日誌, 事務, 統計資訊, 參數檢查, 許可權驗證

2. 連接點(JoinPoint)

連接業務方法和切面的位置,就某類中的業務方法

3. 切入點(Pointcut)

指多個連接點方法的集合,多個方法

4. 目標對象(Target)

給哪個類的方法增加功能, 這個類就是目標對象

5. 通知(Advice)

通知表示切面功能執行的時間

一個切面有三個關鍵的要素:

  1. 切面的功能程式碼,切面是幹什麼
  2. 切面的執行位置,使用Pointcut表示切面執行的位置
  3. 切面的執行時間,使用Advice表示時間,在目標方法之前,還是目標方法之後

三、Aspectj對AOP的實現

aop是一個規範,是動態的一個規範化,一個標準

aop的技術實現框架:

  1. spring:spring在內部實現了aop規範,能做aop的工作,我們項目開發中很少使用spring的aop實現。 因為spring的aop比較笨重
  2. aspectJ: 一個開源的專門做aop的框架。spring框架中集成了aspectj框架,通過spring就能使用aspectj的功能

aspectJ框架實現aop有兩種方式:

  • 使用xml的配置文件 : 配置全局事務
  • 使用註解,我們在項目中要做aop功能,一般都使用註解, aspectj有5個註解

1. Aspectj的通知類型

AspectJ 中常用的通知有五種類型

  • 前置通知
  • 後置通知
  • 環繞通知
  • 異常通知
  • 最終通知

2. Aspectj的切入點表達式

以上表達式共4個部分

execution(訪問許可權 方法返回值 方法聲明(參數) 異常類型)

切入點表達式要匹配的對象就是目標方法的方法名。所以,execution 表達式中明顯就
是方法的簽名。

注意,表達式中黑色文字表示可省略部分,各部分間用空格分開

在其中可以使用以下符號:

常用的幾個:

execution(public * * (..))

指定切入點的位置:任意的公共方法

execution(* set*(..))

指定切入點的位置:任何一個以set開始的方法

execution(* com.xyz.service.*.*(..))

指定切入點的位置:定義在service包里的任意類的任意方法

execution(* com.xyz.service..*.*(..))

指定切入點的位置:定義在service包或者子包里的任意類的任意方法

.. 出現在類名中時,後面必須跟*,表示包、子包下的所有類

execution(* *..service.*.*(..))

指定所有包下的service子包下所有類中所有的方法為切入點

3. Aspectj的開發環境

1. maven依賴

<dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>

    <!--spring依賴-->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.2.5.RELEASE</version>
    </dependency>

    <!--aspectj依賴-->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-aspects</artifactId>
      <version>5.2.5.RELEASE</version>
    </dependency>

2. 引入AOP約束

在 AspectJ 實現 AOP 時,要引入 AOP 的約束。配置文件中使用的 AOP 約束中的標籤,均是 AspectJ 框架使用的,而非 Spring 框架本身在實現 AOP 時使用的
AspectJ 對於 AOP 的實現有註解和配置文件兩種方式,常用是註解方式

四、AspectJ基於註解的AOP實現

1. 實現步驟

1. 定義業務介面與實現類

package com.md.b1;

/**
 * @author MD
 * @create 2020-08-09 10:55
 */
public interface SomeService {
    void doSome(String name , Integer age);
}

//-------------------------

package com.md.b1;

/**
 * @author MD
 * @create 2020-08-09 10:55
 */

// 目標類
public class SomeServiceImpl implements SomeService {
    @Override
    public void doSome(String name, Integer age) {
        // 給doSome方法增加一個功能,在執行之前輸出時間
        System.out.println("目標方法doSome()");
    }
}

2. 定義切面類

類中定義了若干普通方法,將作為不同的通知方法,用來增強功能

注意點:

@Aspect

  • 這個註解是aspectj框架中的註解
  • 作用:表示當前類是切面類
  • 切面類:是用來給業務方法增加功能的類,在這個類中有切面的功能程式碼
  • 位置:類定義的上面

定義方法,方法是實現切面功能的

方法的要求:

  • 公共方法
  • 方法名稱自定義
  • 方法沒有返回值
  • 方法可以有或沒有參數,如果有參數,參數不是自定義的,有幾個參數類型可以使用
package com.md.b1;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import java.util.Date;

/**
 * @author MD
 * @create 2020-08-09 10:58
 */

@Aspect
public class MyAspect {

    // 前置通知,具體的在下變
   @Before(value = "execution(public void com.md.b1.SomeServiceImpl.doSome(String,Integer))")
    public void myBefore(){
        // 就是你切面要執行的功能程式碼
        System.out.println("前置通知,切面功能:在目標方法之前輸出時間:"+new Date());
    }
}

3. 定義目標對象切面類對象

還是在src/main/resources下建立applicationContext.xml

整體結構如下:

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

    <!--把對象交給spring容器,由spring容器統一創建,管理對象-->
    
    <!--聲明目標對象-->
    <bean id="someService" class="com.md.b1.SomeServiceImpl"/>

    <!--聲明切面類對象-->
    <bean id="myAspect" class="com.md.b1.MyAspect"/>

</beans>

4. 註冊AspectJ自動代理

在上面文件的基礎上添加

    <bean id="someService" class="com.md.b1.SomeServiceImpl"/>

    <bean id="myAspect" class="com.md.b1.MyAspect"/>


    <!--聲明自動代理生成器:
        使用的是aspectj框架內部的功能,創建目標對象的代理對象
        創建代理對象是在記憶體中實現的,修改目標對象的記憶體中的結構,
        創建為代理對象。所以,目標對象就是被修改後的代理對象

        aspectj-autoproxy:會把spring容器中的所有目標對象,一次性都生成代理對象
    -->

    <aop:aspectj-autoproxy  />

<aop:aspectj-autoproxy/>的底層是由 AnnotationAwareAspectJAutoProxyCreator 實現的。
從其類名就可看出,是基於 AspectJ 的註解適配自動代理生成器。
其工作原理是,<aop:aspectj-autoproxy/>通過掃描找到@Aspect 定義的切面類,再由切
面類根據切入點找到目標類的目標方法,再由通知類型找到切入的時間點

5. 測試類中的使用

package com.md;

import com.md.b1.SomeService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import sun.security.provider.Sun;

/**
 * @author MD
 * @create 2020-08-09 15:28
 */
public class MyTest01 {

    @Test
    public void test01(){
        String config = "applicationContext.xml";

        ApplicationContext ac = new ClassPathXmlApplicationContext(config);

        // 從容器中獲取目標對象,此時的目標對象是經過了aspectj修改後的代理對象
        SomeService proxy = (SomeService) ac.getBean("someService");

        //com.sun.proxy.$Proxy8 jdk動態代理
        //System.out.println(proxy.getClass().getName());

        // 通過代理的對象執行方法,實現目標方法執行,增強了功能
        proxy.doSome("張三",19);

//        前置通知,切面功能:在目標方法之前輸出時間:Sun Aug 09 15:33:24 CST 2020
//        目標方法doSome()
    }
}

2. @Before前置通知

在目標方法執行之前執行

被註解為前置通知的方法,可以包含一個 JoinPoint 類型參數。該類型的對象本身就是切入點表達式

通過該參數,可獲取切入點表達式、方法簽名、目標對象等
不光前置通知的方法,可以包含一個 JoinPoint 類型參數,所有的通知方法均可包含該參數。

這個JoinPoint參數的值是由框架賦予, 必須是第一個位置的參數

@Aspect
public class MyAspect {

//    @Before():前置通知註解
//    屬性:value 是切入點表達式,表示切面功能執行的位置
//    位置:在方法的上面
//    特點:
//    1. 在目標方法之前執行
//    2. 不會改變目標方法的執行結果
//    3. 不會影響目標方法的執行



//    @Before(value = "execution(public void com.md.b1.SomeServiceImpl.doSome(String,Integer))")
//    public void myBefore(){
//        // 就是你切面要執行的功能程式碼
//        System.out.println("前置通知,切面功能:在目標方法之前輸出時間:"+new Date());
//    }


//    @Before(value = "execution( * *..SomeServiceImpl.do*(..))")
//    public void myBefore(){
//        // 就是你切面要執行的功能程式碼
//        System.out.println("前置通知,切面功能:在目標方法之前輸出時間:"+new Date());
//    }


    /**
     * 指定通知方法中的參數 : JoinPoint
     * JoinPoint:業務方法,要加入切面功能的業務方法
     *    作用是:可以在通知方法中獲取方法執行時的資訊, 例如方法名稱,方法的實參。
     *    如果你的切面功能中需要用到方法的資訊,就加入JoinPoint.
     *    這個JoinPoint參數的值是由框架賦予, 必須是第一個位置的參數
     */
    @Before(value = "execution(void *..SomeServiceImpl.doSome(String,Integer))")
    public void myBefore(JoinPoint jp){
        // 獲取方法的完整定義
        System.out.println("方法的定義:"+jp.getSignature());
        System.out.println("方法的名稱:"+jp.getSignature().getName());

        // 獲取方法的實參
        Object[] args = jp.getArgs();
        for (Object arg:args){
            System.out.println("參數:"+arg);
        }


//        方法的定義:void com.md.b1.SomeService.doSome(String,Integer)
//        方法的名稱:doSome
//        參數:張三
//        參數:19

        // 就是你切面要執行的功能程式碼
        System.out.println("前置通知,切面功能:在目標方法之前輸出時間:"+new Date());
    }
}

3. @AfterReturning後置通知

在目標方法執行之後執行。由於是目標方法之後執行,所以可以獲取到目標方法的返回值

該註解的 returning 屬性就是用於指定接收方法返回值的變數名的

所以,被註解為後置通知的方法,除了可以包含 JoinPoint 參數外,還可以包含用於接收返回值的變數。該變數最好為 Object 類型,因為目標方法的返回值可能是任何類型

增加介面的方法

public interface SomeService {
    void doSome(String name, Integer age);
    String doOther(String name , Integer age);
}
//--------------------------------------------------
// 目標類
public class SomeServiceImpl implements SomeService {


    @Override
    public void doSome(String name, Integer age) {
        // 給doSome方法增加一個功能,在執行之前輸出時間
        System.out.println("目標方法doSome()");
    }

    @Override
    public String doOther(String name, Integer age) {
        System.out.println("目標方法doOther()");
        return "a";
    }
}

定義切面

@Aspect
public class MyAspect {
    /**
     * 後置通知定義方法,方法是實現切面功能的。
     * 方法的定義要求:
     * 1.公共方法 public
     * 2.方法沒有返回值
     * 3.方法名稱自定義
     * 4.方法有參數的,推薦是Object ,參數名自定義
     */

    /**
     * @AfterReturning:後置通知
     *    屬性:1.value 切入點表達式
     *         2.returning 自定義的變數,表示目標方法的返回值的。
     *          自定義變數名必須和通知方法的形參名一樣。
     *    位置:在方法定義的上面
     * 特點:
     *  1。在目標方法之後執行的。
     *  2. 能夠獲取到目標方法的返回值,可以根據這個返回值做不同的處理功能
     *      Object res = doOther();
     *  3. 可以修改這個返回值,但不影響最後的調用結果
     *
     *  後置通知的執行
     *    Object res = doOther();
     *    參數傳遞: 傳值, 傳引用
     *    myAfterReturing(res);
     *    System.out.println("res="+res)
     *
     */

    @AfterReturning(value = "execution(* *..SomeServiceImpl.doOther(..))",returning = "res")
    public void myAfterReturing(Object res){

        if (res.equals("a")){
            // 你可以做一些功能
            System.out.println("登陸成功");
        }else{
            System.out.println("登陸失敗");
        }

        
        // Object res:是目標方法執行後的返回值,可以根據返回值做切面功能處理
        System.out.println("後置通知:獲取的返回值是:"+res);

        // 修改目標方法的返回值,是否影響最後的方法調用結果
        // 無影響,
        if (res != null){
            res = "hello Aspectj";
        }
    }
}

4. @Around環繞通知

在目標方法執行之前之後執行。

被註解為環繞增強的方法要有返回值,Object 類型。並且方法可以包含一個 ProceedingJoinPoint 類型的參數。

介面 ProceedingJoinPoint 其有一個proceed()方法,用於執行目標方法。

若目標方法有返回值,則該方法的返回值就是目標方法的返回值。

最後,環繞增強方法將其返回值返回。該增強方法實際是攔截了目標方法的執行

首先增加方法和實現

public interface SomeService {
    void doSome(String name, Integer age);
    String doOther(String name, Integer age);
    String doFirst(String name,Integer age);
}
//-------------------------------------------
// 目標類
public class SomeServiceImpl implements SomeService {

    @Override
    public void doSome(String name, Integer age) {

        // 給doSome方法增加一個功能,在執行之前輸出時間
        System.out.println("目標方法doSome()");
    }


    @Override
    public String doOther(String name, Integer age) {
        System.out.println("目標方法doOther()");
        return "a";
    }

    @Override
    public String doFirst(String name, Integer age) {
        System.out.println("目標方法doFirst()");
        return "doFirst";
    }

}

切面類

@Aspect
public class MyAspect {
    /**
     * 環繞通知方法的定義格式
     *  1.public
     *  2.必須有一個返回值,推薦使用Object
     *  3.方法名稱自定義
     *  4.方法有參數,固定的參數 ProceedingJoinPoint
     */

    /**
     * @Around: 環繞通知
     *    屬性:value 切入點表達式
     *    位置:在方法定義的上面
     * 特點:
     *   1.它是功能最強的通知
     *   2.在目標方法的前和後都能增強功能。
     *   3.控制目標方法是否被調用執行
     *   4.修改原來的目標方法的執行結果。 影響最後的調用結果
     *
     
     *  環繞通知,等同於jdk動態代理的,InvocationHandler介面
     *
     *  參數:  ProceedingJoinPoint 就等同於 Method
     *         作用:執行目標方法的
     *  返回值: 就是目標方法的執行結果,可以被修改。
     *
     *  環繞通知: 經常做事務, 在目標方法之前開啟事務,執行目標方法, 在目標方法之後提交事務
     */

//    @Around(value = "execution(* *..SomeServiceImpl.doFirst(..))")
//    public Object myAround(ProceedingJoinPoint pjp) throws Throwable {
//
//        Object result = null;
//
//
//        System.out.println("環繞通知,在目標方法之前加入通知:現在時間:"+new Date());
//
//        // 1. 目標方法調用,等同於method.invoke(); 在這裡等同於Object result = doFirst();
//        result = pjp.proceed();
//
//        // 2. 在目標方法前後加入功能
//        System.out.println("環繞通知,在目標方法之後提交事務");
//
//        // 3. 返回目標方法的執行結果
//        return result;
//
//    }




    @Around(value = "execution(* *..SomeServiceImpl.doFirst(..))")
    public Object myAround(ProceedingJoinPoint pjp) throws Throwable {

	// 可以獲取到調用方法的參數
        String name = "";
        // 獲取第一個參數的值
        Object args[] = pjp.getArgs();
        if (args!=null && args.length > 1){
            Object arg = args[0];
            name = (String) arg;
        }


        // 實現環繞通知
        Object result = null;
        System.out.println("環繞通知,在目標方法之前加入通知:現在時間:"+new Date());


        // 1. 目標方法調用,等同於method.invoke(); 在這裡等同於Object result = doFirst();
        if ("張三".equals(name)){
            // 符合條件,調用目標方法
            result = pjp.proceed();
        }


        // 2. 在目標方法前後加入功能
        System.out.println("環繞通知,在目標方法之後提交事務");


        // 還可以修改目標方法的執行結果,影響方法最後的調用結果
        if (result != null){
            result = "修改了";
        }


        // 3. 返回目標方法的執行結果
        return result;

    }

}

測試:

public class MyTest03 {

    @Test
    public void test01(){
        String config = "applicationContext.xml";

        ApplicationContext ac = new ClassPathXmlApplicationContext(config);

        // 從容器中獲取目標對象
        SomeService proxy = (SomeService) ac.getBean("someService");



        // 通過代理的對象執行方法,實現目標方法執行,增強了功能
        String str = proxy.doFirst("張三",20);

        System.out.println(str);

    }
}

//環繞通知,在目標方法之前加入通知:現在時間:Mon Aug 10 20:52:07 CST 2020
//        目標方法doFirst()
//        環繞通知,在目標方法之後提交事務
//        修改了

5. @AfterThrowing 異常通知

在目標方法拋出異常後執行。該註解的 throwing 屬性用於指定所發生的異常類對象。當然,被註解為異常通知的方法可以包含一個參數 Throwable,參數名稱為 throwing 指定的名稱,表示發生的異常對象

相當於try-catch中catch裡面執行的

增加業務方法

public interface SomeService {
    void doSome(String name, Integer age);
    String doOther(String name, Integer age);
    String doFirst(String name, Integer age);

    void doSecond();
}


//實現類-----------------------------------------
@Override
    public void doSecond() {
         System.out.println("執行業務方法doSecond()" + (10/0));
    }

切面類:

@Aspect
public class MyAspect {
    /**
     * 異常通知方法的定義格式
     *  1.public
     *  2.沒有返回值
     *  3.方法名稱自定義
     *  4.方法有個一個Exception, 如果還有是JoinPoint,
     */

    /**
     * @AfterThrowing:異常通知
     *     屬性:1. value 切入點表達式
     *          2. throwinng 自定義的變數,表示目標方法拋出的異常對象。
     *             變數名必須和方法的參數名一樣
     * 特點:
     *   1. 在目標方法拋出異常時執行的
     *   2. 可以做異常的監控程式, 監控目標方法執行時是不是有異常。
     *      如果有異常,可以發送郵件,簡訊進行通知
     *
     *  執行就是:
     *   try{
     *       SomeServiceImpl.doSecond(..)
     *   }catch(Exception e){
     *       myAfterThrowing(e);
     *   }
     */
    @AfterThrowing(value = "execution(* *..SomeServiceImpl.doSecond(..))",
            throwing = "ex")
    public void myAfterThrowing(Exception ex) {
        System.out.println("異常通知:方法發生異常時,執行:"+ex.getMessage());
        //發送郵件,簡訊,通知開發人員
    }
}

6. @After最終通知

無論目標方法是否拋出異常,該增強均會被執行

相當於try-catch-finally中finally裡面執行的

增加方法及實現

public interface SomeService {
    void doSome(String name, Integer age);
    String doOther(String name, Integer age);
    String doFirst(String name, Integer age);
    void doSecond();

    void doThird();
}

//------------------------
  @Override
    public void doThird() {
        System.out.println("執行業務方法doThird()"+ (10/0));
    }

切面類

@Aspect
public class MyAspect {
    /**
     * 最終通知方法的定義格式
     *  1.public
     *  2.沒有返回值
     *  3.方法名稱自定義
     *  4.方法沒有參數,  如果還有是JoinPoint,
     */

    /**
     * @After :最終通知
     *    屬性: value 切入點表達式
     *    位置: 在方法的上面
     * 特點:
     *  1.總是會執行
     *  2.在目標方法之後執行的
     *
     *  try{
     *      SomeServiceImpl.doThird(..)
     *  }catch(Exception e){
     *
     *  }finally{
     *      myAfter()
     *  }
     *
     */
    @After(value = "execution(* *..SomeServiceImpl.doThird(..))")
    public  void  myAfter(){
        System.out.println("執行最終通知,總是會被執行的程式碼");
        //一般做資源清除工作的。
     }

}

7. @Pointcut定義切入點

當較多的通知增強方法使用相同的 execution 切入點表達式時,編寫、維護均較為麻煩AspectJ 提供了@Pointcut 註解,用於定義 execution 切入點表達式

其用法是,將@Pointcut 註解在一個方法之上,以後所有的 execution 的 value 屬性值均可使用該方法名作為切入點。代表的就是@Pointcut 定義的切入點。這個使用@Pointcut 註解的方法一般使用 private 的標識方法,即沒有實際作用的方法

五、總結

前面的概念有些繞,看了程式碼就比較清晰了,感覺和python的裝飾器很像,只不過py的沒有這麼繞,就是為已經存在的對象添加額外的功能

總的來說就是在一個方法前或一個方法後執行一些通用的方法,提高效率,把那些業務的方法寫一塊,那些非業務的方法或那些業務方法經常使用的方法寫成切面類,使用方便還便於管理

1. 使用aspectj框架實現aop

使用aop:目的是給已經存在的一些類和方法增加額外的功能,前提是不改變原來類的程式碼

  1. 新建maven項目
  2. 加入依賴:spring依賴和aspectj依賴以及junit單元測試
  3. 創建目標類
    • 介面和它的實現類,要做的是給類中的方法增加功能
  4. 創建切面類:普通類
    • 在類的上面加入@Aspect
    • 在類中定義方法,這個方法就是切面要執行的功能程式碼
    • 在方法的上面加入aspectj中的通知註解。例如:@Before
    • 還需要指定切入點表達式,execution()
  5. 創建spring的配置文件,聲明對象,把對象交給容器統一管理,聲明對象可以使用註解或者<bean>
    • 聲明目標對象
    • 聲明切面類對象
    • 聲明aspectj框架中的自動代理生成器標籤,自動代理生成器:用來完成代理對象的自動創建功能
  6. 創建測試類
    • 從spring容器中獲取目標對象(實際上就是代理對象),通過代理執行,實現aop的功能增強

2. Review



強制使用cglib代理

 目標類有介面,還想用cglib代理
        proxy-target-class="true" : 這句話就是告訴框架,要使用cglib動態代理
        <aop:aspectj-autoproxy proxy-target-class="true" />
Tags: