理解SpingAOP

Acelin_H發表於2021-08-24



什麼是AOP?


AOP,即我們平時經常提到的面向切面程式設計。首先我們要理解一個叫橫切關注點(cross-cutting concern)的概念,它其實是描述我們應用中的功能,假如有一個功能,它在應用程式中很多個地方都用了,那麼我們把這樣的功能稱之為橫切關注點

​ 日常開發中,我們都會將不同的業務場景抽象出對應的模組進行開發,而不同的模組,除了那些針對特定領域的核心功能外,還有一些相同的輔助功能,比如日誌管理、安全管理、事務管理等等。橫切關注點這個概念其實就點明瞭:類似這樣的功能就是我們面向切面程式設計需要關注的地方。這也是面向切面程式設計的意義所在:它幫助我們實現橫切關注點和他們所影響的物件之間的解耦。

​ 面向切面程式設計的實質,就是講橫切關注點模組化成稱為切面的特殊的類。


AOP術語


以下討論都基於SpringAOP


通知(Advice)

切面的工作被稱為通知。也就是定義了切面的要做什麼,以及何時做的問題。下面是Spring切面有5種型別的通知,以及相對應的在SpringBoot中的五個註解

  • 前置通知(Before):方法呼叫之前,對應@Before
  • 後置通知(After):方法呼叫之後(不關心方法輸出是什麼)對應@After
  • 返回通知(After-returning):目標方法成功執行之後,對應@AfterReturning
  • 異常通知(After-throwing):目標方法丟擲異常之後,對應@AfterThrowing
  • 環繞通知(Around):方法呼叫之前和之後,對應@Around
  • 後置通知和返回通知的區別

    後置通知應用時機在返回通知之後,任何情況下都會應用,而返回通知只有方法正常執行

    正常返回後執行

  • 環繞通知與其它通知的區別

    不同於其它的通知,環繞通知有目標方法的執行權,能夠控制目標方法是否執行。而其它的通知更多是對目標方法的一個增強,無法影響目標方法的執行

以上兩點,我們下面會通過一個例子更好的體會其中的差異。


連線點(Join point)

程式中那些我們想要應用通知的地方,就是連線點。這個點可以是我們呼叫方法時、丟擲異常時或甚至是修改某一個欄位的時候。切面程式碼(通知)可以通過這些點插入到應用的正常流程中,是原本的功能增添新的行為。


切點(Pointcut)

我們的應用程式可能會有很多個連線點需要我們應用通知,所以我們有必要把連線點的分類彙總,抽象出相同的特點,好讓正確的切面切入到正確的地方去,各司其職,而不是切入所有的連線點。切點定義 了一個切面需要在哪裡進行切入。是一堆具有特定切面切入需求的連線點的共性抽象。

我們通常通過明確類和方法名、或者匹配正規表示式的方式來指定切點

連線點和切點的區別

切點是對的擁有相同特點的連線點集合的一個抽象。切點定義了連線點的特性,是一個描述性的歸納,由於在SpringAOP中切點的切入級別是方法,所以那些符合切點定義的方法,都是一個連線點,連線點是切點的具象表現。


切面(Aspect)

切面是通知和切點的結合。通知和切點共同定了切面的全部內容——它想要幹什麼,在何時何地完成功能。


引入(Introduction)

引入能夠讓我們在不修改原有類的程式碼的基礎上,新增生的方法或屬性。


織入(Weaving)

織入是把切面應用到目標物件並建立新的代理物件的過程。


SpringAOP



SpringAOP的特點

SpringAOP是通過動態代理實現的。不同於AspectJ等其他aop框架,SpringAOP只支援方法級別的切點,不支援類構造器或欄位級別的切點。因此只能攔截方法進行通知,而在物件建立或物件變數的修改時,都無法應用通知


SpringBoot整合SpringAOP

SpringBoot引入AOP依賴後,spring.aop.auto屬性預設是開啟的,也就是說只要引入了AOP依賴後,預設已經增加了@EnableAspectJAutoProxy

image

現在我們來設想一個場景:我有若干個業務場景,每個場景對應一個服務,有些服務需要我在調服務的時候,列印開始和結束資訊,並輸入服務處理的時長。

- 依賴引入

首先在專案中引入SpringAOP的依賴:

<!--引入AOP依賴-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
- 建立註解

建立一個自定義註解@ApiLog,用boolean型別的欄位來決定是否開啟日誌輸入功能,程式碼如下:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ApiLog {
    boolean isOn() default true;
}
- 定義切面

定義一個切面類MyAspect.java,程式碼如下

@Aspect
@Component
public class MyAspect {

    @Pointcut("execution(* com.acelin.hello.study.springTest.aop.AspectController.*(..))")
    private void onePointcut(){}

    @Around("onePointcut()")
    public Object around(ProceedingJoinPoint point)throws Throwable{

        Signature signature = point.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();

        /* 獲取方法上的註解 */
        ApiLog apiLog = method.getAnnotation(ApiLog.class);
        if (apiLog != null && apiLog.isOn()){
            System.out.println("ApiLog is on!");
        }

        Object[] objects = point.getArgs();
        String bussinessName = (String) objects[0];
        System.out.println("-- " + bussinessName + " start --");
        long begin = System.currentTimeMillis();
        Object object = point.proceed();
        long end = System.currentTimeMillis();
        System.out.println("-- " + bussinessName + " end  耗時" + (end - begin) + "ms --");
        return object;
    }
}
- 設定切點
@Pointcut("execution(* com.acelin.hello.study.springTest.aop.AspectController.*(..))")
private void onePointcut(){}

@Pointcut註解表示宣告一個切點,然後我們要配置execution指示器,它用來匹配所有符合條件的連線點。已上面的指示器配置* com.acelin.hello.study.springTest.aop.AspectController.*(..)為例,分析一下指示器的寫法

  • 第一個*號:表示的不關心方法的返回值型別
  • 中間的com.acelin.hello.study.springTest.aop.AspectController.*指定方法的特點
  • 第二個*號:表示AspectController類下的任意方法
  • (..):表示不關心引數個數和型別
- 業務介面編寫

我們寫一個簡單的介面,然後註釋我們自定義的註解,開啟相關日誌的輸出。

@RestController
@RequestMapping("/aop")
public class AspectController implements IAspect{

    @RequestMapping("/test/1/{businessName}")
    @ApiLog(isOn = true)
    public void testAop(@PathVariable("businessName") String businessName){
        System.out.println("This is a controller about aop test!");
    }
}
- 測試

測試結果如下:

ApiLog is on!
-- 業務1 start --
This is a controller about aop test!
-- 業務1 end  耗時5ms --

通知時機

以上我們通過一個簡單的例子,初步體驗了aop的魅力。接下來我們來討論以下各類通知的特點以及應用時機問題。我們對的上面的切面類進行了修改,加上所有型別的通知。

@Aspect
@Component
public class MyAspect {

    @Pointcut("execution(* com.acelin.hello.study.springTest.aop.AspectController.*(..))")
    private void onePointcut(){}

    @Before("onePointcut()")
    public void before(){
        System.out.println("-- before");
    }

    @After("onePointcut()")
    public void after(JoinPoint joinPoint) throws NoSuchMethodException{
        System.out.println("-- after");
    }
    
    @AfterThrowing("onePointcut()")
    public void afterThrowing(){
        System.out.println("-- afterThrowing");
    }

    @AfterReturning("onePointcut()")
    public void afterReturning(){
        System.out.println("-- afterReturning");
    }
    
    @Around("onePointcut()")
    public Object around(ProceedingJoinPoint point)throws Throwable{

        Signature signature = point.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();

        /* 獲取方法上的註解 */
        ApiLog apiLog = method.getAnnotation(ApiLog.class);
        if (apiLog != null && apiLog.isOn()){
            System.out.println("ApiLog is on!");
        }

        Object[] objects = point.getArgs();
        String bussinessName = (String) objects[0];
        System.out.println("-- " + bussinessName + " start");
        long begin = System.currentTimeMillis();
        Object object = point.proceed();
        long end = System.currentTimeMillis();
        System.out.println("-- " + bussinessName + " end  耗時" + (end - begin) + "ms");
        return object;
    }
}

然後修改介面,怎加業務名稱的一個長度判斷,如果名稱太長,丟擲異常,來模擬方法執行過程中法僧異常

@RequestMapping("/test/1/{businessName}")
@ApiLog
public void testAop(@PathVariable("businessName") String businessName){
    System.out.println("This is a controller about aop test!");
    if (businessName.length() > 4){
        throw new RuntimeException("業務名稱太長!");
    }
}
- 正常情況

來看一下當方法正常執行的情況下,前置通知、後置通知、返回通知和環繞通知的應用時機。

再次呼叫測試介面,可看到如下結果:

ApiLog is on!
-- 業務 start
-- before
This is a controller about aop test!
-- afterReturning
-- after
-- 業務 end  耗時6ms
- 異常情況

然後我們修改業務名稱為,使其超過規定長度導致觸發異常丟擲,結果如下:

ApiLog is on!
-- 超級複雜的業務 start
-- before
This is a controller about aop test!
-- afterThrowing
-- after

java.lang.RuntimeException: 業務名稱太長!

	at com.acelin.hello.study.springTest.aop.AspectController.testAop(AspectController.java:16)

從上面結果可以看出的,前置通知應用於目標方法執行前,然後當方法正常執行時,會在方法結束前執行返回通知,如果發生異常,執行異常通知,返回通知不執行(因為方法已經中斷,不正常返回),而後置通知的是不管方法執行情況,都會在方法結束後執行。環繞通知則包裹著以上所有流程。


總結


以上就是對AOP相關知識的入門,相關術語的解釋,以及SpringBoot整合SpringAOP的簡單應用,歡迎留言討論。