Spring中使用AOP進行日誌記錄
1. 概述
我們經常使用日誌記錄來記錄程式執行期間有意義的步驟和有價值的資訊。它允許我們記錄數據,以便稍後用於調試和分析程式碼。
此外,面向方面的程式設計(簡稱 AOP)是一種範式,它允許我們在整個應用程式中隔離橫切關注點,例如交易管理或日誌記錄,而不會擾亂業務邏輯。
在本教程中,我們將學習如何使用 AOP 和 Spring 框架實現日誌記錄。
2. 不使用AOP進行日誌記錄
當涉及日誌記錄時,我們通常會將日誌放在方法的開頭和結尾。這樣,我們就可以輕鬆追蹤應用程式的執行流程。此外,我們還可以捕獲傳遞給特定方法的值以及它們傳回的值。
為了進行演示,讓我們使用greet()
方法來建立GreetingService
類別:
public String greet(String name) {
logger.info(">> greet() - {}", name);
String result = String.format("Hello %s", name);
logger.info("<< greet() - {}", result);
return result;
}
儘管上面的實作看起來像是標準解決方案,但日誌語句在我們的程式碼中可能會造成不必要的混亂。
此外,我們也為程式碼引入了額外的複雜性。如果沒有日誌記錄,我們可以將此方法重寫為一行:
public String greet(String name) {
return String.format("Hello %s", name);
}
3. 面向方面的編程
顧名思義,面向方面的程式設計關注的是面向而不是物件和類別。我們使用 AOP 為特定應用程式部分實作附加功能,而無需修改其目前實作。
3.1. AOP 概念
在我們深入研究之前,讓我們先在一個非常高的層次上研究一下基本的 AOP 概念。
- 方面:我們希望在整個應用程式中應用的橫切關注點或功能。
- 連接點:應用程式流程中我們想要應用方面的點。
- 建議:應在特定連接點執行的操作。
- 切入點:應套用方面的連接點的集合。
此外,值得注意的是 Spring AOP 僅支援方法執行的連接點。我們應該考慮使用 AspectJ 等編譯時函式庫來為欄位、建構函式、靜態初始化器等建立切面。
3.2. Maven依賴
要使用 Spring AOP,讓我們在pom.xml
中加入spring-boot-starter-aop
依賴項:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
4. 使用 AOP 進行日誌記錄
在 Spring 中實作 AOP 的一種方法是使用帶有@Aspect
註解的 Spring bean:
@Aspect
@Component
public class LoggingAspect {
}
@Aspect
註解充當標記註解,因此 Spring 不會自動將其視為元件。為了表明它應該是一個由 Spring 管理並透過元件掃描檢測到的 bean,我們也使用@Component
註解對該類別進行註解。
接下來,讓我們定義一個切入點。簡而言之,切入點允許我們指定要使用切面攔截哪個連接點執行:
@Pointcut("execution(public * com.baeldung.logging.*.*(..))")
private void publicMethodsFromLoggingPackage() {
}
在這裡,我們定義了一個切入點表達式,其中僅包含 com.baeldung 中的public
方法com.baeldung.
logging
包。
接下來,讓我們看看如何定義建議來記錄方法執行的開始和結束。
4.1.使用Around
建議
我們將從更通用的建議類型開始—— Around
建議。它允許我們在方法呼叫之前和之後實現自訂行為。此外,透過這個建議,我們可以決定是否繼續處理特定的連接點、傳回自訂結果或拋出例外。
讓我們使用@Around
註釋來定義建議:
@Around(value = "publicMethodsFromLoggingPackage()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
String methodName = joinPoint.getSignature().getName();
logger.info(">> {}() - {}", methodName, Arrays.toString(args));
Object result = joinPoint.proceed();
logger.info("<< {}() - {}", methodName, result);
return result;
}
value
屬性將此Around
建議與先前定義的切入點相關聯。該建議圍繞與publicMethodsFromLoggingPackage()
切入點簽章匹配的方法執行運行。
此方法接受ProceedingJoinPoint
參數。它是JoinPoint
類別的子類,允許我們呼叫proceed()
方法來執行下一個通知(如果存在)或目標方法。
我們在joinPoint
上呼叫getArgs()
方法來檢索方法參數陣列。此外,我們使用getSignature().getName()
方法來取得我們正在攔截的方法的名稱。
接下來,我們呼叫proceed()
方法來執行目標方法並檢索結果。
最後,我們呼叫前面提到的greet()
方法:
@Test
void givenName_whenGreet_thenReturnCorrectResult() {
String result = greetingService.greet("Baeldung");
assertNotNull(result);
assertEquals("Hello Baeldung", result);
}
運行測試後,我們可以在控制台中看到以下結果:
>> greet() - [Baeldung]
<< greet() - Hello Baeldung
5. 使用侵入性最小的建議
在決定使用哪種類型的建議時,建議我們使用能滿足我們需求的最弱的建議。如果我們選擇一般建議,例如Around
建議,我們更容易出現潛在的錯誤和效能問題。
也就是說,讓我們研究一下如何完成相同的功能,但這次使用Before
和After
建議。與Around
建議不同,它們不會包裝方法執行,因此無需明確呼叫proceed()
方法來繼續連接點執行。具體來說,我們使用這些類型的建議在執行之前或之後攔截方法。
5.1.使用Before
建議
為了在執行之前攔截該方法,我們將使用@Before
註解建立建議:
@Before(value = "publicMethodsFromLoggingPackage()")
public void logBefore(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
String methodName = joinPoint.getSignature().getName();
logger.info(">> {}() - {}", methodName, Arrays.toString(args));
}
與前面的範例類似,我們使用getArgs()
方法來取得方法參數,並getSignature().getName()
方法來取得方法名稱。
5.2.使用AfterReturning
建議
更進一步,為了在方法執行後新增日誌,我們將建立@AfterReturning
建議,該建議在方法執行完成且不引發任何異常時執行:
@AfterReturning(value = "publicMethodsFromLoggingPackage()", returning = "result")
public void logAfter(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
logger.info("<< {}() - {}", methodName, result);
}
在這裡,我們定義了returning
屬性來取得方法傳回的值。此外,我們在屬性中提供的值應與參數的名稱相符。傳回值將作為參數傳遞給通知方法。
5.3.使用AfterThrowing
建議
另一方面,要記錄方法呼叫完成但出現異常時的情況,我們可以使用@AfterThrowing
建議:
@AfterThrowing(pointcut = "publicMethodsFromLoggingPackage()", throwing = "exception")
public void logException(JoinPoint joinPoint, Throwable exception) {
String methodName = joinPoint.getSignature().getName();
logger.error("<< {}() - {}", methodName, exception.getMessage());
}
這次,我們將在通知方法中取得拋出的異常,而不是回傳值。
6. Spring AOP 陷阱
最後,讓我們討論一下使用 Spring AOP 時應該考慮的一些問題。
Spring AOP 是一個基於代理的框架。它會建立代理物件來攔截方法呼叫並應用通知中定義的邏輯。這會對我們的應用程式的效能產生負面影響。
為了減少AOP對效能的影響,我們應該只在必要的時候才考慮使用AOP。我們應該避免為孤立的和不頻繁的操作創建切面。
最後,如果我們僅將 AOP 用於開發目的,我們可以有條件地建立它,例如,僅當特定的 Spring 設定檔處於活動狀態時。
七、結論
在本文中,我們學習如何使用 Spring AOP 執行日誌記錄。
總而言之,我們研究瞭如何使用Around
建議以及Before
和After
建議來實現日誌記錄。我們也探討了為什麼使用最不有力的建議來滿足我們的需求很重要。最後,我們解決了 Spring AOP 帶來的一些潛在問題。
與往常一樣,整個程式碼範例可以在 GitHub 上找到。