Spring 采用純注解實現 AOP 切面增強


Spring 的 Aop 切面編程的主要用途是:在不改變相關方法原有代碼的情況下,實現對相關方法的功能增強,其本質就是采用動態代理技術來實現的。有關 Spring 的 Aop 底層原理所采用的動態代理技術,我將在下篇博客進行介紹。

本篇博客主要介紹 Spring 如何采用純注解的方式,對相關方法進行 Aop 擴展增強。有關 Spring 的 Aop 的相關術語,這里不進行詳細介紹,網上資料很多,限於篇幅有限,這里僅僅介紹如果進行快速搭建和使用,在本篇博客的最后會提供 Demo 的源代碼。


一、搭建工程

新建一個 maven 項目,導入相關 jar 包,我導入的都是最新版本的 jar 包,內容如下:

有關具體的 jar 包地址,可以在 https://mvnrepository.com 上進行查詢。

<dependencies>
    <!--Spring 的基礎核心 jar 包-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.17</version>
    </dependency>
    <!--第三方 Aop 切面編程 jar 包-->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.8</version>
    </dependency>

    <!--
    由於本 Demo 需要使用 junit 進行單元測試,
    因此需要導入 junit 和 spring-test 這兩個 jar 包,
    用於 spring 整合 junit 單元測試
    -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.3.17</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13.2</version>
        <scope>test</scope>
    </dependency>
</dependencies>

打開右側的 Maven 窗口,刷新一下,這樣 Maven 會自動下載所需的 jar 包文件。

搭建好的項目工程整體目錄比較簡單,具體如下圖所示:

image

項目工程結構簡單介紹:

aop 包下存放 Aop 的方法類,這些方法用於對相關 Service 類中的方法增強

service 包下存放的是業務處理實現類

AopApp 這個是該 demo 的 main 入口所在類,使用 Spring 訪問數據庫,驗證搭建成果

test 目錄下 EmployeeAopTest 這個是測試方法類,里面編寫測試 Service 接口的方法,驗證 Aop 的執行情況


二、Service 業務方法的細節

本 Demo 中只有一個接口 EmployeeService 和一個實現類 EmployeeServiceImpl ,里面的內容非常簡單。

需要注意的是:
Service 的業務方法類最好要實現接口,因為在實際開發場景中,Aop 切面配置的位置,最佳方案是配置在接口上,而不是配置到具體的實現類上。這樣的好處在於后續如果接口有其它的實現類,Aop 切面增強功能也在接口的其它實現類上自動生效。

package com.jobs.service;

public interface EmployeeService {

    //不會報錯的方法
    Integer normalTest(Integer xxx, Integer yyy);

    //會出現異常的方法
    void errorTest(Integer zzz);

    //測試方法
    String aopTest(String ppp, String qqq);
}
package com.jobs.service.impl;

import com.jobs.service.EmployeeService;
import org.springframework.stereotype.Service;

//通過 @Service 注解將該實現類的實例化對象,裝載到 Spring 容器中,方便后續注入使用
@Service("empService")
public class EmployeeServiceImpl implements EmployeeService {

    //不會報錯的方法,測試 Aop 方法執行
    @Override
    public Integer normalTest(Integer xxx, Integer yyy) {
        Integer result = xxx + yyy;
        System.out.println("normalTest 方法執行了,計算結果為:" + result);
        return result;
    }

    //會出現異常的方法,測試 Aop 方法執行
    @Override
    public void errorTest(Integer zzz) {
        System.out.println("errorTest 方法執行了,必然會拋出異常...");
        //這里必然會拋出異常
        Integer result = zzz / 0;
    }

    //測試方法,測試 Aop 方法執行
    @Override
    public String aopTest(String ppp, String qqq) {
        String result = ppp + "-------" + qqq;
        System.out.println("aopTest 方法執行了,結果為:" + result);
        return result;
    }
}

本 Demo 的業務方法,只有 3 個,實現內容也非常簡單。

normalTest 方法用來展示 Aop 在【被增強的方法】正常不報錯情況下的執行順序。

errorTest 方法用於展示 Aop 在【被增強的方法】出錯的情況下的執行順序。

aopTest 方法用於在本 Demo 的啟動類 AopApp 中,驗證本 Demo 的 Aop 功能是否搭建成功。


三、Spring 純注解配置 Aop 細節

編寫 Aop 功能的方法類 AopAdvice ,該類中的方法用於在不改變 Service 中方法的情況下,強行介入到具體方法的執行過程中,實現對 Service 方法的增強。AopAdvice 類的具體內容如下:

package com.jobs.aop;

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

//使用 @Component 注解將 AopAdvice 的實例化對象,裝載到 Spring 容器中
//使用 @Aspect 注解表示該類是切面類,里面提供了切面增強的方法
@Component
@Aspect
public class AopAdvice {

    //使用 @Pointcut 注解,設置要為哪些類的哪些方法進行切面方法增強
    //這里設置的是,針對 com.jobs.service 包下的所有 Service 類中的方法進行增強
    @Pointcut("execution(* com.jobs.service.*Service.*(..))")
    public void pt() {
    }

    //使用 @Before 注解,表示在【被增強的方法】之前執行下面的代碼
    //該注解修飾的方法,可以有一個 JoinPoint 參數,用於獲取【被增強的方法】的相關信息
    @Before("pt()")
    public void before(JoinPoint jp) {
        System.out.println("前置 before 方法執行了...");

        //獲取所執行方法的簽名信息
        Signature signature = jp.getSignature();
        System.out.println("\t 方法簽名為:" + signature);
        //通過簽名獲取執行接口名稱或類名
        String interfaceName = signature.getDeclaringTypeName();
        System.out.println("\t 方法所屬接口或所屬類名為:" + interfaceName);
        //通過簽名獲取執行方法名稱
        String methodName = signature.getName();
        System.out.println("\t 方法名為:" + methodName);
        //獲取方法的所傳入的參數值
        Object[] args = jp.getArgs();
        for (Object obj : args) {
            System.out.println("\t 方法傳入的參數值:" + obj);
        }
    }

    //使用 @After 注解,表示在【被增強的方法】之后執行下面的代碼
    //無論【被增強的方法】在執行過程中,是否出錯拋出異常,都會執行下面的代碼
    //該注解修飾的方法,可以有一個 JoinPoint 參數,用於獲取【被增強的方法】的相關信息
    @After("pt()")
    public void after(JoinPoint jp) {
        //后置方法,通過 jp 參數,也能夠獲取到所執行方法的相關信息
        System.out.println("后置 after 方法執行了...");
    }

    //使用 @AfterReturning 注解,表示在【被增強的方法】返回結果后,執行下面的代碼
    //如果【被增強的方法】在執行過程中,是否出錯拋出異常,將不會執行下面的代碼
    //該注解修飾的方法,可以有一個 Object 參數,用於獲取【被增強的方法】執行后的返回值
    @AfterReturning(pointcut = "pt()", returning = "ret")
    public void afterReturing(Object ret) {
        //通過參數,可以獲取到方法執行的結果
        System.out.println("返回結果后 afterReturing 執行了,返回結果為:" + ret);
    }

    //使用 @AfterThrowing 注解,表示【被增強的方法】出錯拋異常后,執行下面的代碼
    //如果【被增強的方法】沒有出錯拋異常,將不會執行下面的代碼
    //該注解修飾的方法,可以有一個 Throwable 參數,用於獲取【被增強的方法】出錯時的異常對象
    @AfterThrowing(pointcut = "pt()", throwing = "t")
    public void afterThrowing(Throwable t) {
        //通過參數,可以獲取到異常對象,從而獲取異常信息
        System.out.println("拋出異常后 afterThrowing 執行了,異常信息為:" + t.getMessage());
    }

    //使用 @Around 注解,可以在【被增強的方法】執行前后,增加相關代碼
    // @Around 注解是我們以后用的最多的注解,因為其功能強大靈活。
    //該注解修飾的方法,可以有一個 ProceedingJoinPoint 參數,用於獲取【被增強的方法】的相關信息
    //可以通過 ProceedingJoinPoint 參數的 proceed() 來執行【被增強的方法】
    //需要注意的是:如果這里沒有對【被增強的方法】進行 try catch 包裹處理異常,
    //那么【被增強的方法】如果出現異常,將不會執行后面的代碼,這樣后置增強代碼就不會執行了
    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("環繞前 around before 執行了...");
        //在環繞方法中,可以獲取到方法執行的結果
        Object ret = pjp.proceed();
        System.out.println("環繞后 around after 執行了...");
        return ret;
    }
}

下面我們創建一個 Spring 配置類 SpringConfig 並啟用 Aop 功能,具體內容如下:

package com.jobs.config;

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

// @Configuration 注解,表示該類是 Spring 的配置類
// @ComponentScan 注解,表示掃描具體包及其子包下的所有注解,將相關對象實例化並裝載到 Spring 容器中
// @EnableAspectJAutoProxy 這個就是啟用 Aop 功能的注解
@Configuration
@ComponentScan("com.jobs")
@EnableAspectJAutoProxy
public class SpringConfig {
}

下面我們創建一個擁有 main 方法的入口類,調用 EmployeeService 接口中的 aopTest 方法,驗證搭建成果。

package com.jobs;

import com.jobs.config.SpringConfig;
import com.jobs.service.EmployeeService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class AopApp {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        EmployeeService empService = (EmployeeService) ctx.getBean("empService");
        empService.aopTest("侯胖胖","任肥肥");
    }
}

因為 aopTest 方法內部不會出異常,所以不會執行 Aop 的切面類 AopAdvice 下 @AfterThrowing 注解修飾的方法,其它注解的方法都會正常執行,從而實現對 aopTest 的增強。具體如下圖所示:

image

本 Demo 把 Aop 的所有注解方法都用上了,目的在於演示 Aop 各種注解增強后的方法,具體的執行順序。

從上圖的執行結果可以發現:

@Around 注解修飾的方法(環繞通知),
其內部【被增強方法之前的代碼】在【最前面】執行,【被增強方法之后的代碼】在【最后面】執行。
(上圖中的紅色框輸出的內容)

@Before 注解修飾的方法(前置通知),
其實際執行順序僅次於【環繞通知】的【被增強方法之前的代碼】。
(上圖中的藍色框輸出的內容)

綠色框內輸出的內容,是【被增強方法】執行時輸出的,其實際執行順序僅次於【前置通知】。
(上圖中的綠色框輸出的內容)

@AfterReturning 注解修飾的方法(返回結果通知),其執行順序僅次於【被增強的方法】。
(上圖中的紫色框輸出的內容)

@After 注解修飾的方法(后置通知),在【返回結果通知】之后執行。
(上圖中的黑色框輸出的內容)


四、測試異常方法 Aop 執行順序

前面的博客已經介紹過 Spring 如何整合 junit ,這里就不再詳細介紹了,下面列出編寫的測試類內容:

package com.jobs;

import com.jobs.config.SpringConfig;
import com.jobs.service.EmployeeService;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class EmployeeAopTest {

    @Autowired
    private EmployeeService employeeService;

    @Test
    public void normalTest() {
        int result = employeeService.normalTest(120, 130);
        //Assert.assertEquals 主要用來驗證【執行結果】與【所期望的結果】是否一致
        Assert.assertEquals(250, result);
    }

    @Test
    public void errorTest() {
        try {
            //此方法執行會拋出異常,從而導致 AfterThrowing 執行
            employeeService.errorTest(38);
        } catch (Exception ex) {
            System.out.println("單元測試方法獲得的異常信息:" + ex.getMessage());
        }
    }

    @Test
    public void aopTest() {
        String result = employeeService.aopTest("候肥肥", "任胖胖");
        //Assert.assertEquals 主要用來驗證【執行結果】與【所期望的結果】是否一致
        Assert.assertEquals("候肥肥-------任胖胖", result);
    }
}

有關 normalTest 方法測試的 Aop 執行順序,跟 aopTest 方法的測試結果一樣,因為都是沒有報錯的正常方法。

下面列出 errorTest 方法測試的 Aop 執行順序,employeeService.errorTest 方法會拋異常,其執行結果如下:

image

從上圖中可以發現,當【被增強的方法】在執行過程中出錯拋出異常,Aop 的執行順序為:

@Around 注解修飾的方法(環繞通知),
其內部【被增強方法之前的代碼】在【最前面】執行,【被增強方法之后的代碼】不會被執行。
(上圖中的紅色框輸出的內容)

@Before 注解修飾的方法(前置通知),
其實際執行順序僅次於【環繞通知】的【被增強方法之前的代碼】。
(上圖中的藍色框輸出的內容)

綠色框內輸出的內容,是【被增強方法】執行時輸出的,其實際執行順序僅次於【前置通知】。
(上圖中的綠色框輸出的內容)

@AfterReturning 注解修飾的方法(返回結果通知),
當【被增強的方法】在執行過程中出錯拋出異常時,不會被執行。

@AfterThrowing 注解修飾的方法(異常通知),
當【被增強的方法】在執行過程中出錯拋出異常時,會被執行。
(上圖中的紫色框輸出的內容)

@After 注解修飾的方法(后置通知),
無論【被增強的方法】在執行過程中是否出錯拋出異常時,都會執行。
(上圖中的黑色框輸出的內容)

上圖中黃色框中的內容,是單元測試方法打印出來的。



到此為止,已經完成了 Spring 有關 Aop 切面編程快速搭建和使用的介紹,大家可以根據實際情況在工作中進行參考使用。限於篇幅有限,這里沒有介紹具體的 Aop 概念和細節。在實際開發場景中,使用最多的是 Aop 的環繞通知,其它 Aop 切面通知很少使用。

最后提供本 Demo 的源代碼:https://files.cnblogs.com/files/blogs/699532/spring_aop_junit.zip




免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM