Spring5框架之AOP-ProxyFactory底層實現(五)

1. 前言

Spring aop 是Spring核心組件之一,通過aop可以簡化編程。本文接下來將開始介紹spring aop入門學習

2. aop重要概念

  • (Aspect) 切面

切面是封裝在類中通知與切入點的集合,在Spring中可以由 @Aspect 註解或者xml配置定義一個切面類。

  • (joinPoint) 連接點

應用程序中定義的一個點,對於Spring而言每一個執行的方法就是一個連接點。

  • (advice) 通知

在連接點可以執行特定的代碼邏輯,spring定義了before、after、around通知。

  • (pointcut) 切點

切入點可以理解爲一種表達式去匹配在特定的位置上執行運行特定的代碼。

  • (weaing) 織入

就是在適當的位置上將切面插入到應用程序代碼的過程,然後織入的時候可以完成通知的執行。spring提供的通知由如下幾種:

  1. before:在某個連接點之前執行程序邏輯。
  2. after returning:連接點正常後執行的程序邏輯,需要注意的是如果程序拋出異常該通知並不會執行。
  3. after throwing :當程序出現異常時候執行的程序邏輯。
  4. after:當連接點結束執行的程序邏輯(無論是否出現異常都會執行)
  5. around:spring中最強大的通知功能,它可以完成並實現上面4種功能的實現。

3.Spring AOP 架構

Spring Aop核心架構是基於代理模式,spring中提供了兩種代理模式的創建。一是使用ProxyFactory純程序方法創建AOP代理另一種就是通過使用藉助於@Aspect註解或者xml進行聲明式創建代理,Spring底層可以使用兩種代理方法即JDK動態代理、CGLIB動態代理。默認情況下當被通知的目標實現了接口時,Spring將會採用JDK動態代理,若目標對象沒有實現任何接口將會採用CGLIB動態代理。這是因爲JDK僅提供了基於接口的代理方法實現。

3.1 切面

Spring的切面由實現了Advisor類的實例表示,這個接口由兩個實現PointcutAdvisor、PointcutAdvisor 。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-lkSebXlv-1590596132183)(/Users/codegeekgao/Library/Application Support/typora-user-images/image-20200523235247693.png)]

3.2 ProxyFactory

Spring中實現織入、代理創建過程。創建代理之前需指定被通知對象,在底層ProxyFactory將代理過程委託給DefaultAopProxyFactory,然後該類又將代理委託給 CglibAopProxy 或 JdkDynamicAopProxy 實現。

3.3 Spring通知

Spring AOP中關於通知相關通知接口的繼承樹如下所示:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-XG2b4HwQ-1590596132185)(/Users/codegeekgao/Library/Application Support/typora-user-images/image-20200524002344814.png)]

3.3.1 創建前置通知

通知名稱 接口實現 功能描述
前置通知 org.springframework.aop.MethodBeforeAdvice 可以理解爲連接點(具體的執行方法)執行之前的預處理方法。前置通知可以拿到方法執行目標以及傳遞給目標的方法參數,但是無法控制目標方法的執行。若前置方法拋出異常目標方法執行將會被終止。

前置通知是Spring中比較有用的通知,它在目標方法執行執行執行。可以獲得方法的參數並可以對方法參數進行修改,當前置通知拋出異常時可以阻止目標方法的執行。下面簡單的使用案例演示前置通知的使用。

新增一個員工接口方法:

public interface EmployeeService {

    String getEmployeeName(int type);
}

@Service
public class EmployeeServiceImpl implements EmployeeService {
    @Override
    public String getEmployeeName(int type) {
      	 System.out.println("開始執行getEmployeeName方法.......");
        if(type==1) return "經理";
        if(type==0) return "普通員工";
        return null;
    }
}

xml中配置可以掃描到此接口:

   <context:component-scan base-package="com.codegeek.aop.day2"/>

新增MethodBeforeAdvice 接口實現:

public class SimpleBeforeAdvice implements MethodBeforeAdvice {

    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println("\n" + "before......advice");
        System.out.println("執行的方法是:" + method.getName());
        System.out.println("執行的參數是:" + Arrays.asList(args));
        System.out.println("執行的對象是:" + target);
    }
}

測試類測試代碼如下:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(value = {"classpath:/aop/day2/*.xml"})
public class AdviceTest {

    @Autowired
    private ApplicationContext applicationContext;

    /**
     * 前置通知前需要給proxyFactory設置通知以及代理對象
     */
    @Test
    public void testBefore() {
        ProxyFactory proxyFactory = new ProxyFactory();
        SimpleBeforeAdvice simpleBeforeAdvice = new SimpleBeforeAdvice();
        proxyFactory.addAdvice(simpleBeforeAdvice);
        proxyFactory.setTarget(applicationContext.getBean(EmployeeService.class));
        // 獲取代理對象
        EmployeeService proxy = (EmployeeService) proxyFactory.getProxy();
        System.out.println(proxy.getEmployeeName(1));
    }
}

執行結果如下:

before......advice
執行的方法是:getEmployeeName
執行的參數是:[1]
執行的對象是:com.codegeek.aop.day2.methodbefore.EmployeeServiceImpl@31e5415e
開始執行getEmployeeName方法.......
經理

我們讓前置通知拋出異常如下所示:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-0DB3REtg-1590596132188)(spring-aop.assets/image-20200526164617539.png)]
在看一下測試程序執行結果我們看到目標方法並沒有執行到getEmployeeName 如下所示:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-vMsMWnrm-1590596132190)(spring-aop.assets/image-20200526164731520.png)]
前置通知使用場景常常用於目標方法執行之前處理一些檢查、查詢等預處理,若不滿足指定條件則返回異常以終止目標執行,例如:當用戶開始調用目標方法時,我們可以檢查當前執行的用戶是否有權限,若不滿足操作權限則拋出無權限的異常以阻止當前用戶執行目標方法。

3.3.2 創建後置返回通知

通知名稱 接口實現 功能描述
後置返回通知 org.springframework.aop.AfterReturningAdvice 在連接點方法(目標方法)執行返回結果後執行,後置返回通知可以訪問目標對象、以及目標方法參數、返回值。如果目標方法拋出異常則該通知將不會執行。

後置返回通知方法執行發生在目標方法執行返回結果之後,所以其不能更改目標方法的執行參數。我們複用EmployeeService以及其實現下面簡單演示其使用如下:

新增後置返回通知如下所示:

public class SimpleAfterReturningAdvice implements AfterReturningAdvice {
    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        System.out.println("\n"+"當前執行對象:"+target);
        System.out.println("執行方法名稱:"+method.getName());
        System.out.println("執行方法參數:"+ Arrays.asList(args));
        System.out.println("執行方法的返回值:"+returnValue);
    }
}

測試類代碼:

    @Test
    public void testAfterReturning() {
        ProxyFactory proxyFactory = new ProxyFactory();
        SimpleAfterReturningAdvice simpleAfterReturningAdvice = new SimpleAfterReturningAdvice();
        proxyFactory.addAdvice(simpleAfterReturningAdvice);
        proxyFactory.setTarget(applicationContext.getBean(EmployeeService.class));
        EmployeeService proxy = (EmployeeService) proxyFactory.getProxy();
        System.out.println(proxy.getEmployeeName(0));
    }

測試程序執行結果:

開始執行getEmployeeName方法.......
當前執行對象:com.codegeek.aop.day2.methodbefore.EmployeeServiceImpl@31e5415e
執行方法名稱:getEmployeeName
執行方法參數:[0]
執行方法的返回值:普通員工
普通員工

後置返回通知使用場景:對於目標方法的返回值可以做後置預處理。比如目標返回值符合預期目標,我們可以在後置返回通知進行特殊處理,例如:電商下單場景中若用戶進行了下單處理並且支付成功,我們可以在後置返回通知進行添加會員積分、對接第三方物流公司發貨處理等等,如果客戶下單失敗或者下單接口不可用後置通知中業務邏輯也不會進行執行。

3.3.3 創建環繞通知

接口名稱 接口實現 功能描述
環繞通知 org.aopalliance.intercept.MethodInterceptor 環繞通知是一個強大的通知,使用其可以完成前置通知、後置通知、後置返回通知、異常通知。如有必要完全可以使用其改變方法的邏輯。

環繞通知是一個強大的通知使用它可以完成前置通知、後置返回通知、後置通知、異常通常等功能。通過環繞通知可以更改更改目標方法執行邏輯(可以更改目標方法入參、以及目標方法執行返回值)。通過環繞通知可以將其作爲方法的攔截器,Spring中有很多都是基於此接口((MethodInterceptor))創建如:遠程RMI方法調用、事務管理等功能如下所示:在這裏插入圖片描述
下面以一個簡單的例子演示其使用,首先創建環繞實現如下所示:

public class SimpleAroundAdvice implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
      // StopWatch 是Spring提供計時的類
        StopWatch stopWatch = new StopWatch();
        System.out.println("\n" + "切點表達式:" + invocation.getStaticPart());
        System.out.println("開始計時類:" + invocation.getThis() + "的" + invocation.getMethod().getName() + "方法");
        stopWatch.start(invocation.getMethod().getName());
        // 執行方法
        Object proceed = invocation.proceed();
        System.out.println("\n" + "共打印了" + Arrays.stream(invocation.getArguments()).findFirst().get() + "次");
        stopWatch.stop();
        System.out.println("執行任務共耗時:" + stopWatch.getTotalTimeSeconds());
        return proceed;
    }
}

MethodInterceptor這個接口只有一個invoke方法,而這個方法入參數MethodInvocation對象,通過這個對象我們可以拿到執行的方法對象、執行方法的執行參數、以及方法的返回結果,除此之外我們甚至可以改變執行方法邏輯根據不同的情況返回不同的值。在這裏插入圖片描述
目標類方法代碼如下:

public class MessagePrinter {

    public void print(int times) {
        for (int i = 0; i < times; i++) {
            System.out.print("*");
        }
    }
}

測試類代碼如下所示:

    @Test
    public void testAround() {
        ProxyFactory proxyFactory = new ProxyFactory();
        proxyFactory.addAdvice(new SimpleAroundAdvice());
        proxyFactory.setTarget(new MessagePrinter());
        MessagePrinter proxy = (MessagePrinter) proxyFactory.getProxy();
        proxy.print(1000000);
    }
}

運行結果如下:

切點表達式:public void com.codegeek.aop.day2.around.MessagePrinter.print(int)
開始計時類:com.codegeek.aop.day2.around.MessagePrinter@5b7a7f33的print方法
**************************
共打印了1000000次
執行任務共耗時:1.258593654

3.3.4 創建異常通知

接口名稱 接口實現 功能描述
異常通知 org.springframework.aop.ThrowsAdvice 該方法在目標方法執行拋出異常之後運行,與後置返回通知相反,只有目標方法執行拋異常才能運行此通知。

新增計算接口及其實現:

public interface CalculateService {

    int divide(int a,int b);
}

@Service
public class CalculateImpl implements CalculateService {
    @Override
    public int divide(int a, int b) {
        return a / b;
    }
}

創建異常通知如下:

public class SimpleThrowing implements ThrowsAdvice {

    public void afterThrowing(Exception e) {
        System.out.println("\n"+"拋出的異常是:" + e.getClass().getName());
        System.out.println("錯誤消息:" + e.getMessage());
        System.out.println("導致的原因是:" + e.getCause());
    }
		// 需要注意此方法的參數順序必須是如下順序,否則會報java.lang.IllegalArgumentException: argument type mismatch
    public void afterThrowing(Method method, Object[] args,Object target,  Exception e) {
        System.out.println("\n" + "執行的方法爲:" + method.getName());
        System.out.println("拋出的異常是:" + e.getClass().getName());
        System.out.println("錯誤消息:" + e.getMessage());
        System.out.println("導致的原因是:" + e.getCause());
    }
}

測試類代碼:

    @Test
    public void testThrowing() {
        ProxyFactory proxyFactory = new ProxyFactory();
        proxyFactory.addAdvice(new SimpleThrowing());
        proxyFactory.setTarget(applicationContext.getBean(CalculateService.class));
        CalculateService proxy = (CalculateService) proxyFactory.getProxy();
        proxy.divide(5,0);
    }

運行結果如下:

執行的方法爲:divide
拋出的異常是:java.lang.ArithmeticException
錯誤消息:/ by zero
導致的原因是:null
java.lang.ArithmeticException: / by zero

異常通知使用場景:可以監控特定的類如果目標方法執行出現異常可以進行特殊處理的邏輯,例如:電商下單調用第三方支付接口出現失敗,可以先生成訂單然後在緊急排查原因然後在對客戶賬戶上的金額進行扣款。

3.3.5 創建後置通知

通知名稱 接口實現 功能描述
後置通知 org.springframework.aop.AfterAdvice 無論目標方法執行成功或失敗,該通知都會進行執行

新增後置通知實現:

public class SimpleAfterService implements AfterAdvice {

    public void after(Method method,Object [] args,Object target) {
        System.out.println("執行的方法名:"+method.getName());
        System.out.println("執行參數:"+ Arrays.asList(args));
        System.out.println("執行的目標類:"+target.getClass().getName());
    }
}

測試方法如下:

    @Test
    public void testAfter() {
        ProxyFactory proxyFactory = new ProxyFactory();
        proxyFactory.addAdvice(new SimpleAfterService());
        proxyFactory.setTarget(applicationContext.getBean(CalculateService.class));
        CalculateService proxy = (CalculateService) proxyFactory.getProxy();
        proxy.divide(5, 6);
    }

當我們運行時候該測試方法拋出異常信息如下:在這裏插入圖片描述
我們debug上面報錯的第一行代碼處:
在這裏插入圖片描述
上面判斷通知是否屬於環繞通知,因爲我們創建的是後置通知故程序不會進入if中,我們繼續debug如下:
在這裏插入圖片描述
但是這個this.adapters竟沒有後置通知適配器,所以下面for循環後interceptors的size依然爲0,然後程序判斷interceptors的size爲0後就拋出異常。在這裏插入圖片描述
爲什麼這裏Spring AOP沒有提供對後置通知的支持呢?很奇怪待有時間再來研究Spring AOP後置通知處理這塊相關知識。

4. AOP 的綜合案例

日常開發中經常需要對重要功能方法進行日誌輸出,可以使用代碼在每個方法體裏輸出方法參數、返回值等信息,但這無疑與代碼進行了耦合。此時可以藉助於Spring aop完成日誌輸入與輸出並且可以做到日誌功能與核心功能分離實現零浸入。

  1. 定義業務模型計算器接口以及其實現以演示aop功能的使用
public interface Calculator {

    int add(int a, int b);

    int sub(int a, int b);

    double divide(int a, int b);
}

@Service
public class CalculatorImpl implements Calculator {
    @Override
    public int add(int a, int b) {
        return a + b;
    }

    @Override
    public int sub(int a, int b) {
        return a - b;
    }

    @Override
    public double divide(int a, int b) {
        return a / b;
    }
}

我們在測試將之前的前置通知、後置返回通知、環繞通知、異常通知一起綜合使用如下所示:

    @Test
    public void testAll() {
        ProxyFactory proxyFactory = new ProxyFactory();
        // 先添加環繞通知
        proxyFactory.addAdvice(new SimpleAroundAdvice());
        // 添加後置返回通知
        proxyFactory.addAdvice(new SimpleAfterReturningAdvice());
        // 添加前置通知
        proxyFactory.addAdvice(new SimpleBeforeAdvice());
        // 添加異常通知
        proxyFactory.addAdvice(new SimpleThrowing());
        proxyFactory.setTarget(applicationContext.getBean(CalculateService.class));
        CalculateService proxy = (CalculateService) proxyFactory.getProxy();
        int add = proxy.add(1, 5);
        System.out.println("計算的值:---------" + add);
    }
}

測試類輸出結果如下所示:

around...before...advice...start
切點表達式:public int com.codegeek.aop.day2.throwexception.CalculateImpl.add(int,int)
開始計時類:com.codegeek.aop.day2.throwexception.CalculateImpl@306f16f3的add方法

before......advice....start
執行的方法是:add
執行的參數是:[1, 5]
執行的對象是:com.codegeek.aop.day2.throwexception.CalculateImpl@306f16f3
before......advice...end

afterReturning......advice...start
當前執行對象:com.codegeek.aop.day2.throwexception.CalculateImpl@306f16f3
執行方法名稱:add
執行方法參數:[1, 5]
執行方法的返回值:6
afterReturning......advice...end
共打印了1次
執行任務共耗時:0.011571445
around...before...advice...end
計算的值:---------6

我們可以發現執行順序是按照如下步驟:在這裏插入圖片描述
我們將調整一下添加前置通知與環繞通知的順序如下所示:

輸出結果如下所示:

before......advice....start
執行的方法是:add
執行的參數是:[1, 5]
執行的對象是:com.codegeek.aop.day2.throwexception.CalculateImpl@49912c99
before......advice...end

around...before...advice...start
切點表達式:public int com.codegeek.aop.day2.throwexception.CalculateImpl.add(int,int)
開始計時類:com.codegeek.aop.day2.throwexception.CalculateImpl@49912c99的add方法

afterReturning......advice...start
當前執行對象:com.codegeek.aop.day2.throwexception.CalculateImpl@49912c99
執行方法名稱:add
執行方法參數:[1, 5]
執行方法的返回值:6
afterReturning......advice...end
共打印了1次
執行任務共耗時:0.016663616
around...before...advice...end
計算的值:---------6

我們發現通知執行流程如下所示;
在這裏插入圖片描述

源碼

以上代碼均可在 codegeekgao.git 下載查看。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章