Spring 5框架之使用AspectJ 實現AOP功能 (九)

1.前言

開發中最常見就是使用Aspect完成AOP功能的實現,下面將簡單使用@Aspect實現AOP的面向切面編程。關於Spring原生AOP的用法可參考之前的一篇博客文章 Spring5框架之AOP-ProxyFactory底層實現(五)

在使用AspectJ之前讓我先溫習一下幾個比較重要的概念如下所示:

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

2.使用Aspect實現AOP功能

如果使用@Aspect註解需要在項目中支持引入下述Aspect的依賴:

        <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>1.8.10</version>
        </dependency>

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.10</version>
        </dependency>

下面我們依然使用Calculator這個計算器接口演示AspectJ方法的使用如下所示:

  • 新增接口及其實現

首先AspectJ爲我們提供瞭如下幾個通知註解:
@Before@AfterReturning@After@Around@AfterThrowing 接下來將使用一個簡單的案例演示這幾個註解的使用。

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
    @AdviceRequired
    public int sub(int a, int b) {
        return a - b;
    }

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

  • 定義切面類
@Aspect
@Component
public class LogAspect {

    /**
     * 定義切點,其中execution定義切入點表達式
     */
    @Pointcut(value = "execution(* *..aop..*(..))")
    public void logPoint() {

    }
    /**
     * 前置通知在方法執行前執行
     */
    @Before(value = "execution(public * com.codegeek.aop.day1.CalculatorImpl.*(..))")
    public static void logStart(JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        // 拿到執行方法的源對象-----即切入表達式實際切入的地方並運行的地方
        System.out.println(joinPoint.getTarget().getClass());
        //  System.out.println(joinPoint.getStaticPart()); 打印詳細切入點表達式

        System.out.println("LogAspect-普通通知方法@before:" + joinPoint.getSignature().getName() + "日誌開始了....方法參數:" + Arrays.asList(args));
    }

    /**
     * 方法執行後返回結果後執行該通知
     * @param result
     */
    @AfterReturning(value = "execution(public * com.codegeek.aop.day1.CalculatorImpl.*(..))", returning = "result")
    public static void logRun(Object result) {
        System.out.println("LogAspect-普通通知@AfterReturning" + "運行結果爲:" + result);
    }

    /**
     *
     * 如果方法執行出現異常將執行此通知
     */
    @AfterThrowing(value = "execution(public  * com.codegeek.aop.day1.Calculator.*(..))", throwing = "e")
    public static void logException(Exception e) {
        System.out.println("LogAspect-普通通知@AfterThrowing出異常啦:" + e);
    }

    /**
     * 後置通知切入點執行後
     */
    @After(value = "execution(public  * com.codegeek.aop.day1.Calculator.*(..))")
    public void logEnd() {
        System.out.println("LogAspect-普通通知@After日誌結束了");
    }

    @Around(value = "logPoint()")
    public Object logAround(ProceedingJoinPoint proceedingJoinPoint) {
        Object proceed = null;
        try {
            // @Before
            System.out.println("環繞前通知.....當前執行的方法:" + proceedingJoinPoint.getSignature().getName());
            proceed = proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());
            // @AfterReturn
            System.out.println("環繞後通知.....");
        } catch (Throwable throwable) {
            // @AfterThrowing
            System.out.println("環繞異常通知.......");
            // 如果不拋出異常----則就將異常catch掉了,普通通知異常將無法感知到異常對象,所以認爲是正常執行的
           throw new RuntimeException(throwable);

        } finally {
            // @After
            System.out.println("環繞結束通知.....");
        }
        return proceed;
    }
}

在我們定義完切面類後有幾個重要的點需要注意一下:

  • 使用了@Component和@Aspect註解聲明一個切面類對象,且可以被Spring IOC容器掃描到此對象。
  • 使用了 @Pointcut註解定義了方法的切點,所謂的切點的作用就是決定通知能夠在那些目標類的目標方法執行。
  • @Before、@Around、@After、@AfterReturning、@AfterThrowing 中可以引入切點的值,亦可自定義切點的值

接下來我們還需要定義配置類以使Spring可以掃描到切面類併成功可以執行通知。

@Configuration
@ComponentScan(basePackages = {"com.codegeek.aop.day1"})
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AOPConfig {

}
  • 測試方法:

需要注意的是在測試類上可以引入此配置類如下所示:
在這裏插入圖片描述

    @Test
    public void testAspect() {
        // 配置切面類IOC就生成接口的代理類否則就是基本類
        System.out.println();
        Calculator bean = applicationContext.getBean(Calculator.class);
        System.out.println(bean.add(1, 5));
        System.out.println("-------------------");
    }

這裏我們使用了正常的方法測試其運行結果如下:

環繞前通知.....當前執行的方法:add
class com.codegeek.aop.day1.CalculatorImpl
LogAspect-普通通知方法@before:add日誌開始了....方法參數:[1, 5]
環繞後通知.....
環繞結束通知.....
LogAspect-普通通知@After日誌結束了
LogAspect-普通通知@AfterReturning運行結果爲:6
6
-------------------

接下來我們測試一個異常的情況如下:

    @Test
    public void testAspect() {
        // 配置切面類IOC就生成接口的代理類否則就是基本類
        System.out.println();
        Calculator bean = applicationContext.getBean(Calculator.class);
        System.out.println(bean.add(1, 5));
        System.out.println("-------------------");
        System.out.println(bean.divide(5, 0));
        System.out.println(bean);
        System.out.println(bean.getClass());
    }

運行測試方法後輸出結果如下所示:

環繞前通知.....當前執行的方法:add
class com.codegeek.aop.day1.CalculatorImpl
LogAspect-普通通知方法@before:add日誌開始了....方法參數:[1, 5]
環繞後通知.....
環繞結束通知.....
LogAspect-普通通知@After日誌結束了
LogAspect-普通通知@AfterReturning運行結果爲:6
6
-------------------
環繞前通知.....當前執行的方法:divide
class com.codegeek.aop.day1.CalculatorImpl
LogAspect-普通通知方法@before:divide日誌開始了....方法參數:[5, 0]
環繞異常通知.......
環繞結束通知.....
LogAspect-普通通知@After日誌結束了
LogAspect-普通通知@AfterThrowing出異常啦:java.lang.RuntimeException: java.lang.ArithmeticException: / by zero

java.lang.RuntimeException: java.lang.ArithmeticException: / by zero

我們發現了異常通知執行了,但是程序運行正常情況下異常通知將不會執行,於此同時可以發現@AfterReturning註解通知並沒有執行,它只有在程序運行正常下才會執行通知。關於通知執行順序可以很形象表示如下:

[普通前置]
try {
  環繞通知前置
  環繞執行
  環繞返回
} catch(Exception e) {
  環繞異常通知
} finally {
  環繞後置通知
}
[普通後置]
[普通方法返回/普通異常通知]

3.使用xml實現AOP實現

我們首先將LogAspect類上的@Aspect註解進行註釋,然後新建立一個切面類OrderAspect如下:

@Component
//@Aspect
@Order(1)
public class OrderAspect {
    
   // @Before(value = "execution(* *..aop..*(..))")
    public static void logStart(JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        //  System.out.println(joinPoint.getStaticPart()); 打印詳細切入點表達式

        System.out.println("OrderAspect-普通通知方法@before:" + joinPoint.getSignature().getName() + "日誌開始了....方法參數:" + Arrays.asList(args));
    }
   // @Pointcut(value = "execution(* *..aop..*(..))")
    public void logPoint() {

    }

    // returning 告訴方法執行完畢後返回的值
   // @AfterReturning(value = "execution(public * com.codegeek.aop.day1.CalculatorImpl.*(..))", returning = "result")
    public static void logRun(Object result) {
        System.out.println("OrderAspect-普通通知@AfterReturning" + "運行結果爲:" + result);
    }

    //@AfterThrowing(value = "execution(public  * com.codegeek.aop.day1.Calculator.*(..))", throwing = "e")
    public static void logException(Exception e) {
        System.out.println("OrderAspect-普通通知@AfterThrowing出異常啦:" + e);
    }

  //  @After(value = "execution(public  * com.codegeek.aop.day1.Calculator.*(..))")
    public void logEnd() {
        System.out.println("OrderAspect-普通通知@After日誌結束了");
    }
}

然後在xml中配置如下內容:

<?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:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!--  false使用jdk代理否則使用CGlib-->
<aop:aspectj-autoproxy proxy-target-class="true"/>
<context:component-scan base-package="com.codegeek.aop.day1"/>

  <aop:config>
    <aop:pointcut id="logPoint" expression="execution(* *..aop..*(..))"/>
    <aop:aspect id="myAspect" ref="orderAspect">
      <aop:before method="logStart" pointcut-ref="logPoint" arg-names="joinPoint"/>
      <aop:after method="logEnd" pointcut-ref="logPoint" />
      <aop:after-returning method="logRun" pointcut-ref="logPoint" returning="result"/>
      <aop:after-throwing method="logException" pointcut-ref="logPoint" throwing="e"/>
    </aop:aspect>
  </aop:config>
  <bean id="orderAspect" class="com.codegeek.aop.day1.OrderAspect"/>
</beans>

我們再次運行測試方法如下所示:

    @Test
    public void testAspect() {
        // 配置切面類IOC就生成接口的代理類否則就是基本類
        System.out.println();
        Calculator bean = applicationContext.getBean(Calculator.class);
        System.out.println(bean.add(1, 5));
        System.out.println("-------------------");
        System.out.println(bean.divide(5, 0));
        System.out.println(bean);

運行結果如下所示:

OrderAspect-普通通知方法@before:add日誌開始了....方法參數:[1, 5]
OrderAspect-普通通知@After日誌結束了
OrderAspect-普通通知@AfterReturning運行結果爲:6
6
-------------------
OrderAspect-普通通知方法@before:divide日誌開始了....方法參數:[5, 0]
OrderAspect-普通通知@After日誌結束了
OrderAspect-普通通知@AfterThrowing出異常啦:java.lang.ArithmeticException: / by zero

java.lang.ArithmeticException: / by zero

4. 如何選擇AOP類型

在上面我們演示了一個基於Aspect風格註解的AOP實現,也看到了一個基於XML配置的AOP實現。這些受到多種因素的影響,例如程序需求、開發工具、開發團隊對AOP熟悉的程度等等。由於Spring AOP與AspectJ都使用了相同的Aspect風格,如果有額外的需要AspectJ的功能需求,可以將現有的Aspect風格註解的代碼遷移到Aspect上。

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