最近,我必須使用六邊形架構模式 在 Java 中實現一個新的 CRUD 服務。六邊形架構模式是一種強調系統中關注點分離和元件獨立性的軟體模式。遵循此模式的服務由以下部分組成:
- 核心模組:這是應用程式的業務邏輯所在的位置。它包含系統的基本功能。
- 埠:這些介面定義了核心模組如何與外部世界互動。它們代表應用程式的輸入和輸出點並隱藏其實現細節。
- 介面卡:這些是埠的實現。它們充當外部世界和核心模組之間的橋樑。
在這種架構模式中,系統的所有智慧都在核心模組中定義,並且與外部系統的所有互動都是使用介面進行的。這些介面向核心模組隱藏了外部系統的詳細資訊。預計在埠級別定義的行為是非常基本的,而所有協調都是在核心模組級別完成的。在 CRUD 服務中,這意味著如果某個操作需要在資料庫中進行多次更改,則所有這些協調都應在核心模組中完成。
通常,當系統中的單個操作需要在資料庫中進行多次更改時,我們使用資料庫事務 來確保資料一致性。資料庫事務是關聯式資料庫系統最重要的功能之一。它們提供了一種將一個或多個資料庫語句分組為單個不可分割的工作單元的方法。透過這樣做,他們可以保證所有語句要麼成功執行,要麼沒有執行,從而防止部分更新並保持資料完整性。
一個問題浮出水面
將六邊形架構和資料庫事務的概念結合起來,我心中產生了一個問題:如果六邊形架構模式規定所有的業務邏輯都應該在核心模組完成,而核心模組不知道與外部系統互動的實現細節如何在不影響關注點分離相關六邊形架構主要思想的情況下保證資料一致性?
Spring 框架來救援
Spring 框架是一種廣泛使用的開源框架,用於構建基於 Java 的應用程式,它提供了強大且靈活的事務管理系統。該框架提供了一個註釋@Transactional,確保在用它註釋的方法中執行的所有資料庫操作都在單個資料庫事務的上下文中執行。Spring 在方法開始之前自動啟動事務,並在方法完成後提交或回滾事務,具體取決於方法的結果(成功或失敗)。
在六邊形架構的上下文中,在核心模組定義資料庫事務是有意義的,因為該模組將負責聚合單個服務操作所需的所有資料庫更改。但是,如果我們在核心模組定義此註釋並且核心模組不知道如何在較低階別完成操作,那麼這將如何工作?
這個揮之不去的疑問一直困擾著我,所以我決定深入研究這個 Spring 註解的實現細節,以充分理解它的行為。
Spring 註解@Transactional的幕後花絮
為了探索上面討論的概念,我們將使用我建立的演示專案,該專案可以在GitHub上找到。在這個演示專案中,我們有一個 API 模組,允許我們建立帳戶並將其與城市關聯。
- API 模組 -AccountsApi呼叫核心模組 -SpringAccountService來執行此操作,遵循六邊形架構的原則,
- 而六邊形架構又呼叫適當的埠 - AccountRepositoryPort- 使用 JPA 儲存庫實現。
- 此服務操作在資料庫級別執行兩項更改:一是建立帳戶,二是將其與指定城市關聯。
註釋@Transactional用於保證帳戶的建立及其與城市的關聯之間的資料一致性。這是一個非常簡單和虛擬的專案,只是為了讓我們探索所需的概念。
當我們呼叫帶有 @Transactional 註解的 SpringAccountService.createAccount() 方法時,奇蹟就開始了:
- 此時,Spring 不會直接呼叫 SpringAccountService 物件,而是會呼叫 Spring 建立的代理物件。
- Spring 之所以能做到這一點,是因為它在建立 Bean 的過程中會檢查是否有任何方面與給定的類相關聯,如果有,它就會將真實物件封裝在代理物件中。
- 然後,它就可以為代理物件新增額外的行為,這些行為可以在真實物件方法呼叫之前或之後執行。
- 在 @Transactional 的例子中,Spring 新增了額外的行為來處理事務管理。
Spring 有兩種建立代理物件的策略:
- JDK 動態代理:如果目標物件至少實現了一個介面,Spring 就會使用 JDK 動態代理。這些代理實現了與目標物件相同的介面,並攔截方法呼叫以應用相關方面。
- CGLIB:如果目標物件沒有實現任何介面,Spring 就會使用 CGLIB。CGLIB 會在執行時生成目標類的子類,該子類會覆蓋方法以應用各方面。
步驟:
- SpringAccountService.createAccount()方法處, Spring 建立的一個代理物件
- 代理物件中呼叫TransactionInterceptor AOP攔截器類。該類負責建立一個資料庫事務,所有資料庫操作都將在該事務中執行。
- 這是在TransactionInterceptor 的invokeWithinTransaction()方法中完成的。在此方法中,我們接收回撥作為引數,其中包含應在資料庫事務中執行的程式碼,該程式碼指向真實SpringAccountService物件
- 在此方法中,Spring 建立一個資料庫事務(如果尚未建立)並將事務資訊新增到執行操作的執行緒的ThreadLocal例項中。
- 這將允許在此執行緒內執行的後續方法使用先前開啟的事務。儲存了有關交易的大量資訊,但與我們的討論更相關的是EntityManager。該事務將具有關聯的 EntityManager,並且只要使用相同的 EntityManager 完成資料庫操作,它們就會位於同一事務內
- 在此之後,真正的 SpringAccountService.createAccount() 會被呼叫,這反過來又會呼叫 AccountRepositoryPort,而 AccountRepositoryPort 是利用 Spring Data JPA 資源庫實現的。
- 預設情況下,Spring Data JPA 資源庫中的方法具有與之相關的事務性。這意味著資料庫操作將在之前啟動的事務中執行。
- JPA 資源庫將訪問 ThreadLocal 例項以獲取事務資訊,然後訪問相應的 EntityManager。
- JPA 資源庫所做的所有操作都將透過該實體管理器完成,以確保它們是在先前啟動的事務上下文中完成的。這樣JPA 資源庫久能使用現有當前的事務。
- 在 SpringAccountService.createAccount() 方法結束時,當所有資料庫更改都成功完成後,將使用相同的 EntityManager 提交事務,並清理 ThreadLocal 例項中的資源。
問題終於有了答案!
當使用 @Transactional 註解宣告核心模組的方法時,一旦該方法被呼叫,資料庫事務就會啟動,並透過 ThreadLocal 例項將事務詳細資訊新增到執行該方法的執行緒中。
然後,Spring Data JPA 資源庫將訪問 ThreadLocal 例項中的事務資訊,並在該事務範圍內進行資料庫更改。
這意味著核心模組不需要知道哪些介面卡將被執行,只要它們是在啟動事務的同一執行緒中執行即可。
當然,如果介面卡的實現不是基於 Spring,它們就不會知道正在進行的事務,資料完整性也就得不到保證。
不過,只要我們使用 Spring 與外部資料庫系統互動,這種保證就會一直有效。
我們可以在不影響服務層或其事務行為的情況下,輕鬆地將 JPA 儲存庫更換為不同的實現。
利用 AOP 方法,我們可以在不汙染業務邏輯的情況下獲得事務保證。Spring 真是一個充滿魔力的世界:D