spring boot AOP筆記

dust1發表於2019-04-04

面向切面程式設計(AOP)是通過另一種思考方式來對物件導向程式設計(OOP)的補充。在抽象的結構中,OOP模組的基本單元是類,而AOP的基本單元是面。AOP的面能夠跨越多個型別和物件來達成模組化。

下面是根據我的理解畫的圖:

spring boot AOP筆記
這是一個簡單的MVC結構,不同的模組之間根據類來分離。但是AOP的切面卻可以跨越多個模組。圖中的示例表示UserAOP跨越了整個Controller模組。當然他也可以同時跨越Model模組。這取決於AOP的實際業務需求。

AOP提供了一個不同的程式設計思路,不過springIoc並沒有依賴AOP,對於Ioc來說,AOP可以提供支援但不是必須。

AOP結構

  • 切面(Aspect):跨越多個類別的關注點的模組化。事務管理是企業Java應用程式中對切面應用最多的例子。在SpringAOP中,切面是使用常規類(基於模式的方法)或使用@Aspect註釋(@AspectJ樣式)註釋的常規類來實現的 。
  • 連線點(JoinPoint):程式執行期間的一個點。在springAOP中表示AOP程式執行的點。
  • 通知(Advice):連線點在特定狀態下執行的操作。其中包括了“around”、“before”和“after”等不同型別的通知。許多AOP框架(包括Spring)都是以攔截器做通知模型,並維護一個以連線點為中心的攔截器鏈。
  • 切入點(Pointcut):和連線點匹配的斷言。切入點確定了AOP程式的入口,並在這之後和連線點相連並執行對應的連線點方法。切入點表示式如何和連線點匹配是AOP的核心:Spring預設使用AspectJ切入點表示式語言。
  • 引入(Introduction):用來給一個型別宣告額外的方法或屬性(也被稱為連線型別宣告(inter-type declaration))。Spring允許引入新的介面(以及一個對應的實現)到任何被代理的物件。例如,你可以使用引入來使一個bean實現IsModified介面,以便簡化快取機制。
  • 目標物件(Target Object):被一個或者多個切面所通知的物件。也被稱做被通知(advised)物件。既然Spring AOP是通過執行時代理實現的,這個物件永遠是一個被代理(proxied)物件。
  • AOP代理(AOP Proxy):AOP框架建立的物件,用來實現切面契約(例如通知方法執行等等)。在Spring中,AOP代理可以是JDK動態代理或者CGLIB代理。
  • 織入(Weaving):把切面連線到其它的應用程式型別或者物件上,並建立一個被通知的物件。這些可以在編譯時(例如使用AspectJ編譯器),類載入時和執行時完成。Spring和其他純Java AOP框架一樣,在執行時完成織入。

其中切入點可以和連線點合併,直接在通知中表示:
@Before("execution(com.dust.controller..(..))")
public void before() {/* 方法體 /}
該寫法等價於:
@Pointcut("execution(com.dust.controller.
.(..))")
public void point(){}
@Befor("point()")
public void before(){/
方法體 */}

通知

在AOP中,連線點與切入點的關聯關係以及相應的判斷規則是AOP的核心。切入點確定了AOP入口,但是具體要執行哪部分則由連線點決定。連線點是方法執行的入口。

  • @Before:前置通知,在連線點前執行。這個通知不能阻礙通知之前的程式執行。
    @Before("execution(* com.dust.controller.*.*(..))")
    public void beforController() {
        //在Controller被呼叫前
    }
複製程式碼
  • @AfterReturning:後置通知,在連線點之後執行,但是可以獲取到連線點的返回值。

這裡可以知道,對AOP來說,最小的單元是方法。AOP只能在方法和方法之間切入,而不能切入方法本身

    @AfterReturning("execution(* com.dust.controller.*.*(..))")
    public void afterController() {
        //在Controller執行完之後
    }
複製程式碼

對方法返回值的獲取:

    @AfterReturning(
        pointcut = "execution(* com.dust.controller.*.*(..))",
        returning = "retVal")
    public void afterController(Object retVal) {
        //對方法返回值的獲取
        System.out.println(retVal.toString());
    }
複製程式碼

其中returning中的引數名稱必須要和advice方法的引數名稱相同,當方法執行返回時,返回值將作為相應的引數值傳遞給advice方法。如果方法沒有返回值,則該引數為null。

  • @AfterThrowing:異常通知,當方法丟擲異常的時候執行。
    @AfterThrowing("execution(* com.dust.controller.*.*(..))")
    public void afterController() {
        //在Controller丟擲異常後執行
    }
複製程式碼

也可以設定到丟擲給定異常時才執行advice。

    @AfterThrowing(
        pointcut = "execution(* com.dust.controller.*.*(..))",
        throwing = "ex")
    public void afterController(NullPointerException ex) {
        //丟擲空指標異常時執行advice
    }
複製程式碼
  • @After:最終通知,在連線點之後執行。不論返回點是正常還是異常。通常用來釋放資源。通常在這裡需要進行正常和異常的返回條件。
    @After("execution(* com.dust.controller.*.*(..))")
    public void afterController() {
        //最終執行通知
    }
複製程式碼
  • @Around:環繞通知。包圍一個連線點的通知,如方法呼叫。這是最強大的一種通知型別。環繞通知可以在方法呼叫前後完成自定義的行為。他可以確定方法何時、如何甚至是否執行。環繞通知使用一個代理ProceedingJoinPoint型別的物件來管理目標物件,所以此通知的第一個引數必須是ProceedingJoinPoint型別,在通知體內,呼叫ProceedingJoinPoint的proceed()方法會導致後臺的連線點方法執行。proceed 方法也可能會被呼叫並且傳入一個Object[]物件-該陣列中的值將被作為方法執行時的引數。

通知引數

所有通知方法都可以宣告一個類行為org.aspectj.lang.JoinPoint的引數。

環繞通知宣告的引數為ProceedingJoinPoint,因為需要執行ProceedingJoinPoint的proceed()方法。而其他的通知則不需要。

JoinPoint提供了很多有用的方法:

  • getArgs()(返回方法引數):返回該方法的引數集合,是一組Object[]
  • getThis()(返回代理物件):獲取代理物件本身
  • getTarget()(返回目標):獲取連線點所在的目標物件
  • getSignature()(返回正在被通知的方法相關資訊):獲取連線點的方法簽名物件
  • toString()(列印出正在被通知的方法的有用資訊)

傳入引數

通常通知方法獲取方法引數除了上述通過JoinPoint獲取外還可以通過pointcut獲取

    @Pointcut("execution(* com.example.springdemo.controller.*.*(..)) && args(address, text, ..)")
    public void inController(String address, String text) {}

    @Before("inController(address, text)")
    public void beforController(String address, String text) {
        System.out.println("在執行控制器之前,獲取引數{address:" + address + ",text:" + text + "}");
    }
複製程式碼

args(address, text, ..)切入點表示式匹配切入點方法的引數,而後傳入給advice方法。 這是要求所有和該切入點匹配的連線點都需要接收引數,還可以單單在連線點接收引數:

    @Pointcut("execution(* com.example.springdemo.controller.*.*(..))")
    public void inController() {}

    @Before("inController() && args(address, text, ..)")
    public void beforController(String address, String text) {
        System.out.println("在執行控制器之前,獲取引數{address:" + address + ",text:" + text + "}");
    }
複製程式碼

其他引數:代理物件(this),目標物件(target)和註釋(@within, @target, @annotation, @args)都可以以類似的方式繫結。

例子:使用AOP來重試事務

由於併發問題:死鎖。可能導致業務執行失敗。下次執行又有可能執行成功,因此對於這種操作不希望將重試交給使用者來執行,這個可以交由系統來執行,這樣對於使用者來說他還是一次就執行成功了。

由於要嘗試執行多次process(),因此使用@Around環繞通知

@Aspect
@Configuration
public class AOPRedo {

    //預設最大重試次數
    private static final int DEFAULT_MAX_RETRIES = 2;

    private int maxRetries = DEFAULT_MAX_RETRIES;

    public void setMaxRetries(int maxRetries) {
        this.maxRetries = maxRetries;
    }

    @Around("execution(* com.example.springdemo.controller.RestAPIController.*(..))")
    public Object apiAroundController(ProceedingJoinPoint pjp) {
        int num = 0;
        Throwable throwable;
        do {
            num++;
            try {
                return pjp.proceed();
            } catch (Throwable th) {
                System.out.println("嘗試捕獲");
                throwable = th;
            }
        } while (num <= maxRetries);
        return null;
    }

}
複製程式碼

其中重試次數可以交給配置檔案,通過@PropertySource來匯入配置資訊。

@RestController
@RequestMapping("api")
public class RestAPIController {

    @Autowired
    EmailService emailService;

    private int count = 0;

    @RequestMapping("email")
    public String email(String address, String info) throws NullPointerException {
        if (count++ < 1) {
            throw new NullPointerException();
        }
        return emailService.setEmail(address,info) + "執行次數:" + count;
    }

}
複製程式碼

執行結果

spring boot AOP筆記

相關文章