1. 概述
我們經常使用日誌記錄來記錄程序執行過程中的有意義步驟和有價值的信息。這使我們能夠記錄可用於後續調試和分析代碼的數據。
此外,面向切面編程(簡稱AOP)是一種允許我們將橫切關注點,例如事務管理或日誌記錄,在應用程序中分離的技術範式,而無需污染業務邏輯。
在本教程中,我們將學習如何使用AOP和Spring框架實現日誌記錄。
2. 不使用 AOP 進行日誌記錄
當涉及到日誌記錄時,我們通常會在方法的開頭和結尾添加日誌,這樣可以輕鬆地跟蹤應用程序的執行流程。此外,我們還可以捕獲傳遞到特定方法的值以及它們返回的值。
為了演示,讓我們創建一個 GreetingService 類,其中包含 greet() 方法:
public String greet(String name) {
logger.debug(">> greet() - {}", name);
String result = String.format("Hello %s", name);
logger.debug("<< greet() - {}", result);
return result;
}儘管上述實現看起來像一個標準解決方案,但日誌語句可能會在我們的代碼中顯得多餘且雜亂。
此外,我們還為我們的代碼引入了額外的複雜性。如果沒有日誌,我們可以在一行代碼中重寫此方法:
public String greet(String name) {
return String.format("Hello %s", name);
}3. 面向方面編程
正如其名稱所示,面向方面編程側重於方面而非對象和類。 我們使用 AOP 來為特定的應用程序部分實現額外的功能,而無需修改其當前實現。
3.1. AOP 概念
在深入探討之前,讓我們從高層次上審視基本的 AOP 概念。
- 方面 (Aspect):指我們希望在應用程序中應用穿插式功能的橫切關注點。
- 連接點 (Join Point):指我們希望在應用程序流程中應用方面的具體位置。
- 建議 (Advice):指在特定連接點應執行的操作。
- 切入點 (Pointcut):指一個連接點集合,用於指定方面應應用的範圍。
此外,值得注意的是,Spring AOP 僅支持方法執行的連接點。為了創建用於字段、構造函數、靜態初始化器等的方面,我們應該考慮使用編譯時庫,如 AspectJ。
3.2. Maven 依賴
要使用 Spring AOP,請在我們的 <em pom.xml</em> 中添加 <a href="https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop">spring-boot-starter-aop` 依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>4. 使用 AOP 進行日誌記錄
通過使用帶有 @Aspect 註解的 Spring Bean 來實現 AOP 的一種方法:
@Aspect
@Component
public class LoggingAspect {
}@Aspect 註解作為標記註解,Spring 不會自動將其視為組件。為了指示它應該由 Spring 管理並通過組件掃描進行檢測,我們還使用 @Component 註解對該類進行標註。
接下來,讓我們定義一個切入點。簡單來説,切入點允許我們指定我們希望使用方面攔截的連接點執行:
@Pointcut("execution(public * com.baeldung.logging.*.*(..))")
private void publicMethodsFromLoggingPackage() {
}在這裏,我們定義了一個只包含 公共方法,來自 com.baeldung.logging 包的切片表達式。
接下來,讓我們看看如何定義用於記錄方法開始和結束的建議。
4.1. 使用 <em>環繞</em> 建議
我們將從更通用的建議類型開始——<em>環繞</em> 建議。它允許我們實現在方法調用前後自定義行為。 此外,使用這種建議,我們可以決定是否繼續處理特定的切入點,返回自定義結果,或拋出異常。
讓我們使用 <em>@Around</em> 註解定義建議:
@Around(value = "publicMethodsFromLoggingPackage()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
String methodName = joinPoint.getSignature().getName();
logger.debug(">> {}() - {}", methodName, Arrays.toString(args));
Object result = joinPoint.proceed();
logger.debug("<< {}() - {}", methodName, result);
return result;
}value 屬性將此Around 建議與先前定義的 pointcut 關聯起來。該建議在與 publicMethodsFromLoggingPackage() pointcut 簽名匹配的方法執行期間運行。
該方法接受一個 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 Baeldung5. 使用最不侵入性的建議
在決定使用哪種類型的建議時,我們建議使用最能滿足我們需求的建議類型。如果選擇通用的建議,例如 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.debug(">> {}() - {}", 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.debug("<< {}() - {}", 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 配置文件處於活動狀態時。
7. 結論
在本文中,我們學習瞭如何使用 Spring AOP 進行日誌記錄。
總結一下,我們研究瞭如何使用 <em >Around</em> 建議以及 <em >Before</em> 和 <em >After</em> 建議來實現日誌記錄。我們還探討了為什麼在滿足需求時,應該使用最弱的建議。最後,我們還解決了 Spring AOP 帶來的潛在問題。