1. 概述
當使用 Spring AOP 時,存在許多複雜之處。一個常見的問題是,在同一類中處理方法調用會繞過 AOP 功能。
在本教程中,我們將學習一些 Spring AOP 的工作原理,以及可以應用哪些解決方法。
2. 代理與 Spring AOP
為了開始,我們將快速回顧一下代理對象是什麼以及它們在 Spring 框架中的使用。
代理對象可以被認為是包裹對象,它可以為目標對象的調用添加額外的功能。
在 Spring 中,當一個 Bean 需要額外的功能時,會創建一個代理對象,並將該代理對象注入到其他 Bean 中。例如,如果一個方法帶有 Transactional 註解或任何緩存註解,則使用該代理對象來為目標對象的調用添加所需的額外功能。
3. AOP:內部方法調用與外部方法調用
正如前面所述,Spring 創建的代理添加了所需的 AOP 功能。為了説明內部方法調用和外部方法調用之間的差異,我們來看一個緩存示例:
@Component
@CacheConfig(cacheNames = "addOne")
public class AddComponent {
private int counter = 0;
@Cacheable
public int addOne(int n) {
counter++;
return n + 1;
}
@CacheEvict
public void resetCache() {
counter = 0;
}
}當 Spring 創建 AddComponent Bean 時,還會創建一個代理對象,用於為緩存添加 AOP 功能。當另一個對象需要 AddComponent Bean 時,Spring 會提供該代理對象用於注入。
進行測試,我們調用 addOne() 方法,從一個單獨的組件中多次使用相同的輸入,並驗證計數器只增加一次。
@SpringBootTest(classes = Application.class)
class AddComponentUnitTest {
@Resource
private AddComponent addComponent;
@Test
void whenExternalCall_thenCacheHit() {
addComponent.resetCache();
addComponent.addOne(0);
addComponent.addOne(0);
assertThat(addComponent.getCounter()).isEqualTo(1);
}
}現在,我們為 AddComponent 添加另一個方法,該方法內部調用 addOne():
public int addOneAndDouble(int n) {
return this.addOne(n) + this.addOne(n);
}當這種新方法調用 addOne() 時,調用不會經過代理,計數器會被遞增兩次:
@Test
void whenInternalCall_thenCacheNotHit() {
addComponent.resetCache();
addComponent.addOneAndDouble(0);
assertThat(addComponent.getCounter()).isEqualTo(2);
}4. 規避方案
雖然同一類中方法調用不會通過預期的 AOP 功能,但仍有幾種規避方案。
其中一種最佳方法是重構。 在我們的 AddComponent 示例中,與其直接將 addOneAndDouble() 添加到類中,不如創建一個新的類包含該方法。 新類可以注入 AddComponent,或者更準確地説,AddComponent 的代理將注入到該類中:
@Component
public class AddOneAndDoubleComponent {
@Resource
private AddComponent addComponent;
public int addOneAndDouble(int n) {
return addComponent.addOne(n) + addComponent.addOne(n);
}
}正如我們之前的測試結果所表明的,這隻會增加計數器一次。
如果無法進行重構,則可以嘗試直接將代理注入到類中。這需要謹慎處理,因為這會創建一個直接的循環依賴關係,Spring 默認不再允許。但是,仍然有許多解決方案可以允許在 Spring 中存在循環依賴關係,我們將會使用 @Lazy 註解來標記自依賴關係:
@Component
@CacheConfig(cacheNames = "selfInjectionAddOne")
public class SelfInjection {
@Lazy
@Resource
private SelfInjection selfInjection;
private int counter = 0;
@Cacheable
public int addOne(int n) {
counter++;
return n + 1;
}
public int addOneAndDouble(int n) {
return selfInjection.addOne(n) + selfInjection.addOne(n);
}
@CacheEvict(allEntries = true)
public void resetCache() {
counter = 0;
}
}通過注入代理後,addOneAndDouble() 將會使用緩存功能,計數器只會遞增一次:
@Test
void whenCallingFromExternalClass_thenAopProxyIsUsed() {
selfInjection.resetCache();
selfInjection.addOneAndDouble(0);
assertThat(selfInjection.getCounter()).isEqualTo(1);
}Spring AOP 使用動態代理創建代理實例,這是一種動態織入的例子。另一方面,AspectJ 使用了其他幾種類型的織入方式,從而避免了重構和自注入的需求。
5. 結論
在本文中,我們探討了在同一類中處理 Spring AOP 調用的一些方法。重構通常是首選解決方案,但如果無法實現,可以使用 AspectJ 或自注入來實施所需的功能。