Spring @Transactional 註解是否適用於私有方法?
1.概述
在本教程中,我們將解決 Spring 的@Transactional
註解是否適用於private
方法的問題。作為 Spring 應用程式中事務管理的基石, @Transactional
簡化了跨資料庫操作的資料一致性維護。然而,開發人員在應用它時經常會遇到意外行為,這引發了對其與方法可見性的兼容性的質疑。
我們將深入研究 Spring 的事務管理機制,並提供清晰的解釋和實用的見解來解決這個問題。
2.理解Spring的@Transactional
註解
Spring 中的@Transactional
註解用於定義方法或類別的事務邊界。它確保註解範圍內的操作作為單一工作單元執行。當呼叫具有@Transactional
註解的方法時,Spring 的事務管理會建立事務、處理提交或回滾,並管理資料庫連線等資源。
2.1. 範例服務方法
讓我們看一個程式碼範例,我們將註解應用於服務方法:
@Service
public class OrderService {
@Autowired
private TestOrderRepository repository;
@Transactional
public void createOrder(TestOrder order) {
repository.save(order);
}
}
當我們將@Transactional
應用於createOrder
方法時,它本身並不執行任何邏輯,而是充當一個標記。 Spring的面向方面編程 (AOP) 框架使用代理來處理此標記,以攔截方法調用並編織事務行為。
讓我們深入了解這些代理程式是什麼以及它們如何與@Transactional
相關聯。
2.2. 方面和代理
代理充當中間對象,包裝目標對像以攔截和控制對其方法的存取。它可以啟用其他行為,例如事務管理。
為了說明自訂@Transactional
方面,讓我們考慮一個簡化的範例:
@Around("@annotation(org.springframework.transaction.annotation.Transactional)")
public Object manageTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
// Start transaction
try {
Object result = joinPoint.proceed(); // Execute the target method, like 'createOrder'
// Commit transaction
return result;
} catch (Throwable t) {
// Rollback transaction
throw t;
}
}
此程式碼範例演示了一個模擬@Transactional
行為的攔截函數manageTransaction
。它使用@Around
建議攔截帶有@Transactional
註解的方法,在執行之前啟動事務,在成功時提交事務,在失敗時回滾事務。
方法可見性—— public
、 protected
、包私有或private
——在偵測要攔截的方法方面發揮重要作用。
在這個基礎上,我們來解決核心問題: @Transactional
是否適用於私有方法?
3. @Transactional
適用於private
方法嗎?
簡短的回答是:不,預設不是。
為了理解原因,我們來看看 Spring 的 AOP 代理程式是如何運作的。這些代理包裝目標物件以攔截調用,並在方法周圍添加事務邏輯。
但是,代理只能攔截它們可以訪問的方法,這傳統上意味著public
方法。
從 Spring 6.0 開始, @Transactional
Transactional 也支援基於類別的代理程式中protected
且套件可見的方法。但請注意:對於基於介面的代理(JDK 動態代理),方法仍然必須是public
,並且在介面中定義。
為什麼會有這些限制?
3.1. 方法可見性的作用
Spring 使用兩種主要的代理類型來處理@Transactional
:JDK 動態代理(基於接口,需要接口實現)和 CGLIB 代理(基於類,透過子類化實現)。它們都無法攔截private
方法,或像final
或static
方法這樣實際上私有的方法。
要確定應用程式使用的代理模式,請注意:如果 Bean 實作了接口,Spring 預設使用 JDK 動態代理;否則,則使用 CGLIB。我們可以透過在@EnableTransactionManagement
或 AOP 配置中設定proxyTargetClass=true
來強制使用 CGLIB。
在 Java 中, private
方法不能被繼承,也不能被覆寫。因此,當我們使用@Transactional
註解private
方法時,代理無法查看或攔截該方法,從而導致事務行為被跳過。
3.2. 如果我們註解private
方法會發生什麼事?
為了演示,我們為OrderService
添加不同可見性的方法。每個方法都會保存一個訂單並拋出異常。如果@Transactional
成功,異常應該會觸發回滾,使儲存庫保持為空。如果失敗,訂單將保留。
首先,讓我們專注於private
方法,因為它是我們預期失敗的核心情況:
@Transactional
private void createOrderPrivate(TestOrder order) {
repository.save(order);
throw new RuntimeException("Rollback createOrderPrivate");
}
public void callPrivate(TestOrder order) {
createOrderPrivate(order);
}
現在,我們來測試一下。我們透過public
包裝器間接呼叫private
方法,因為private
方法不能從外部直接呼叫:
@Test
void givenPrivateTransactionalMethod_whenCallingIt_thenShouldNotRollbackOnException() {
assertThat(repository.findAll()).isEmpty();
assertThatThrownBy(() -> underTest.callPrivate(new TestOrder())).isNotNull();
assertThat(repository.findAll()).hasSize(1);
}
這裡,儲存庫在異常後以一個順序結束,顯示沒有回滾,這表明**@Transactional
被忽略了,因為代理無法攔截private
方法**。
接下來,讓我們將其與public
方法的行為進行比較:
@Transactional
public void createOrderPublic(TestOrder order) {
repository.save(order);
throw new RuntimeException("Rollback createOrderPublic");
}
@Test
void givenPublicTransactionalMethod_whenCallingIt_thenShouldRollbackOnException() {
assertThat(repository.findAll()).isEmpty();
assertThatThrownBy(() -> underTest.createOrderPublic(new TestOrder())).isNotNull();
assertThat(repository.findAll()).isEmpty();
}
這次,異常發生後儲存庫保持為空,確認回滾透過代理攔截起作用。
類似地,對於 package-private (預設可見性) :
@Transactional
void createOrderPackagePrivate(TestOrder order) {
repository.save(order);
throw new RuntimeException("Rollback createOrderPackagePrivate");
}
@Test
void givenPackagePrivateTransactionalMethod_whenCallingIt_thenShouldRollbackOnException() {
assertThat(repository.findAll()).isEmpty();
assertThatThrownBy(() -> underTest.createOrderPackagePrivate(new TestOrder())).isNotNull();
assertThat(testOrderRepository.findAll()).isEmpty();
}
假設我們使用基於類別的代理(CGLIB),這會在 Spring 6.0+ 中按預期回滾。
最後,讓我們測試一下@Transatcional
註解對protected
方法的行為:
@Transactional
protected void createOrderProtected(TestOrder order) {
repository.save(order);
throw new RuntimeException("Rollback createOrderProtected");
}
@Test
void givenProtectedTransactionalMethod_whenCallingIt_thenShouldRollbackOnException() {
assertThat(repository.findAll()).isEmpty();
assertThatThrownBy(() -> underTest.createOrderProtected(new TestOrder())).isNotNull();
assertThat(testOrderRepository.findAll()).isEmpty();
}
再次,使用基於類別的代理回滾成功。
歸根結底, @Transactional
只是元資料——Spring 運行時用來操作的標記。如果代理無法偵測到它(例如private
方法),則相當於該註解不存在:該方法執行時沒有事務。
4. 如何解決private
方法的問題
由於@Transactional
不適用於private
方法,我們需要採取變通方法來避免諸如錯過回溯之類的問題。
讓我們探討一些選擇。
4.1. 使用public
方法
最簡單的解決方法是將邏輯移至public
方法,代理可以可靠地攔截該方法。
話雖如此,將方法public
可能會破壞封裝性。為了平衡這一點,請仔細設計接口,或使用套件私有方法(Spring 6.0+ 及 CGLIB 支援)。
4.2. 切換到 AspectJ 織入
為了獲得更大的靈活性,我們可以使用 AspectJ,它在編譯或載入時將方面直接編織到字節碼中,從而繞過代理限制。
這種方法允許@Transactional
在私有方法上工作,但需要額外的配置,例如啟用 AspectJ 編織。
4.3. 提取到單獨的 Bean
另一種方法是將事務邏輯提取到一個單獨的 Spring bean 中,並新增一個public
方法。然後,將此 bean 注入到原始服務中並呼叫它。這樣可以保留主類別的封裝,同時讓代理人處理新的public
方法。
5. 結論
在本文中,我們探討了 Spring 的@Transactional
註解及其在private
方法上的局限性,這些局限性源於基於代理的 AOP。代理無法存取private
方法,因此該註解會被忽略。
我們可以透過public
方法、AspectJ 織入或分離 Bean 來解決這個問題。掌握這些機制並選擇正確的解決方案,就能確保 Spring 應用程式中的可靠交易。
與往常一樣,本文使用的完整程式碼可以在 GitHub 上找到。