1. 簡介
本文將使用 Spring 中的 AOP 支持,實現自定義 AOP 註解。
首先,我們將對 AOP 進行高層次概述,解釋其是什麼以及它的優勢。隨後,我們將逐步實現我們的註解,隨着步驟的推進,逐步深入理解 AOP 概念。
最終,您將對 AOP 有更深入的理解,並具備在未來創建自定義 Spring 註解的能力。
2. 什麼是 AOP 註解?
簡而言之,AOP 代表面向方面編程。本質上,它是一種在不修改現有代碼的情況下添加行為的方式
對於 AOP 的詳細介紹,請參考關於 AOP 切面和建議的文章。本文假設您已經具備一定的基礎知識。
在本篇文章中,我們將實現的是基於註解的 AOP。如果您已經熟悉 Spring 中的 @Transactional 註解,那麼您可能已經對此有所瞭解。
@Transactional
public void orderGoods(Order order) {
// A series of database calls to be performed in a transaction
}關鍵在於非侵入性。通過使用標註元數據,我們的核心業務邏輯不會被交易代碼污染。這使得我們更容易理解、重構和隔離測試。
有時,開發 Spring 應用的人可能會將這視為“Spring 魔法”,而沒有深入思考其工作原理。實際上,發生的事情並不複雜。但是,一旦我們完成了本文中的步驟,我們就能創建自己的自定義註解,以便理解和利用 AOP。
3. Maven 依賴
首先,我們添加我們的 Maven 依賴。
對於這個例子,我們將使用 Spring Boot,其約定優於配置的方法讓我們能夠儘快上手:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>請注意,我們包含了AOP啓動器,它引入了我們實現方面所需的庫。
4. 創建我們的自定義標註
我們將要創建的標註將用於記錄方法執行所需的時間。 讓我們創建我們的標註:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {
}
雖然這是一個相對簡單的實現,但值得注意的是這兩個元標註的用途。
<a href="https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/annotation/Target.html">@Target</a> 標註告訴我們標註將適用的位置。在這裏,我們使用了 ElementType.Method,這意味着它僅適用於方法。如果我們嘗試在其他地方使用該標註,則代碼將無法編譯。這種行為是合理的,因為我們的標註將用於記錄方法的執行時間。
<a href="https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/annotation/Retention.html">@Retention</a> 僅聲明標註在運行時是否可用。默認情況下它不可見,因此 Spring AOP 將無法看到該標註。這就是為什麼它被重新配置的原因。
5. 創建我們的方面
現在我們已經有了標註,接下來我們將創建我們的方面。這只是一個模塊,它將封裝我們的橫切關注點,在本例中是方法執行時間日誌記錄。它本質上是一個類,該類已使用 @Aspect 標註。
@Aspect
@Component
public class ExampleAspect {
}我們還包含了@Component註解,因為我們的類也需要成為 Spring Bean 才能被檢測到。本質上,這是我們將要實現我們自定義註解注入的邏輯所在。
6. 創建我們的 Pointcut 和 Advice
現在,讓我們創建我們的 pointcut 和 advice。這將是一個帶有註釋的方法,該方法位於我們的方面中。
@Around("@annotation(LogExecutionTime)")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
return joinPoint.proceed();
}技術上講,這尚未改變任何東西的行為,但仍有大量內容需要分析。
首先,我們已使用 @Around 對方法進行了註釋。這是一種建議,而環繞建議意味着我們在方法執行前後添加了額外的代碼。還有其他類型的建議,例如 before 和 after,但它們將超出本文的範圍。
接下來,我們的 @Around 註解具有一個切點參數。我們的切點只是説,“應用此建議的任何已使用 @LogExecutionTime 註解的方法。” 還有其他類型的切點,但它們也將超出範圍。
方法 logExecutionTime() 本身就是我們的建議。它有一個參數,ProceedingJoinPoint。 在我們的例子中,這將是一個正在執行且已使用 @LogExecutionTime 註解的方法。
最後,當我們的註釋方法被調用時,會首先調用我們的建議。然後,建議決定下一步該怎麼做。 在我們的例子中,我們的建議只是調用 proceed(),即調用原始註釋的方法。
7. 記錄執行時間
現在我們已經搭建了基本框架,只需要在建議中添加一些額外的邏輯,即可記錄執行時間,同時調用原始方法。 讓我們為建議添加這種行為:
@Around("@annotation(LogExecutionTime)")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object proceed = joinPoint.proceed();
long executionTime = System.currentTimeMillis() - start;
System.out.println(joinPoint.getSignature() + " executed in " + executionTime + "ms");
return proceed;
}再次強調,這裏並沒有進行任何特別複雜的操作。我們只是記錄了當前時間,執行了方法,然後將耗時打印到控制枱。我們還記錄了方法的簽名,這對於使用 joinpoint 實例至關重要。我們還可以訪問其他信息,例如方法參數。
現在,讓我們嘗試使用 @LogExecutionTime 註解一個方法,然後執行它來查看結果。請注意,這必須是一個 Spring Bean 才能正確工作:
@LogExecutionTime
public void serve() throws InterruptedException {
Thread.sleep(2000);
}執行完成後,我們應該在控制枱中看到以下內容:
void org.baeldung.Service.serve() executed in 2030ms