JBoss EAPSpring Framework交易企業系統升級

JBoss EAP 09:看懂 Spring XML,驗證 Java 設定沒有改掉交易

··閱讀約 7 分鐘
回到 JBoss EAP 系列導讀

舊專案的 applicationContext.xml 很長,AI 把它改成一排 @Bean,看起來短了,卻不代表交易範圍還一樣。先讀懂哪個物件由誰建立、呼叫有沒有穿過代理,再決定要搬哪些設定。這一課用同一個 Service,讓 XML、Java 設定與 Java 匯入 XML 共用一組行為測試。

下載第 09 課:XML/Java 對照專案與測試

範例固定 Java 21、Spring Framework 6.2.19 與 H2。Maven 借用 Boot 3.5.16 parent 管理建置,但沒有啟動 SpringApplication 或 Web server。三種設定都跑在同一個新版 Spring;這是認識設定與代理的實驗,沒有在舊 Spring 3/4 上執行,也不是企業升級已相容的證明。

bean、ref 與 @Bean,先追物件關係

xml
<bean id="jdbc" class="org.springframework.jdbc.core.JdbcTemplate">
  <constructor-arg ref="dataSource"/>
</bean>
<bean id="ticketService" class="com.keeponfirst.xml.TicketService">
  <constructor-arg ref="jdbc"/>
</bean>

這是 context.xml 的片段。id 是容器裡的識別名稱,ref 指向另一個 Bean;constructor-arg 把它交給建構子。沿著 ticketService → jdbc → dataSource 看,就能找到這個 Service 最後連到哪份資源。這裡的 dataSource 由 XML 建立 H2,不是 EAP 的 JNDI 資源。

java
@Bean
public TicketService ticketService(JdbcTemplate jdbc) {
    return new TicketService(jdbc);
}

Java 版本用方法參數取得容器中的 JdbcTemplate,再建立同一種 Service。JavaConfig 設成 proxyBeanMethods=false,因此範例刻意用參數注入,沒有靠呼叫另一個 @Bean 方法取物件。讀舊碼時要分清「請容器提供 Bean」和「自己 new 一個」,後者未必有代理或生命週期處理。

component-scan 和 XML 可以一起用

兩種設定都只掃 com.keeponfirst.xml.entry,找到 @Component 的 TicketEntry,其他 Bean 明寫。TicketEntry 用建構子取得 TicketService。掃描範圍若一下拉到整個根套件,可能把另一份 Configuration 或第二份 Service 也載進來;不能為了解決找不到 Bean 就一路擴大掃描。

java
@Configuration(proxyBeanMethods = false)
@ImportResource("classpath:context.xml")
public class ImportedXmlConfig { }

第三條路直接由 Java 匯入同一份 XML,沒有再次匯入 JavaConfig,也沒有疊加另一套 annotation 交易設定。這讓你可以先保留已讀懂的 XML,逐步遷移。ImportResource 只是載入入口;XML 裡若參照被移除的類別、自訂 namespace handler 或不相容套件,仍要逐項處理。

tx:advice 與 @Transactional,要對到同一組方法

xml
<tx:advice id="ticketAdvice" transaction-manager="transactionManager">
  <tx:attributes>
    <tx:method name="createThenCheckedRollback" rollback-for="java.lang.Exception"/>
    <tx:method name="create*"/>
  </tx:attributes>
</tx:advice>

XML 版另外用 aop:advisor 把這份 advice 套到公開的 create* 方法;只有 advice 名字,不會自動套用到每個物件。Java 版則由 EnableTransactionManagement 啟用 Service 上的 annotation。XML 版沒有 annotation-driven,故同一份 Service 雖有 annotation,實際採用的是 XML 規則。這個安排避免兩套設定一起作用,把問題藏起來。

本例未覆寫全域回滾政策:runtime exception 會回滾,checked exception 預設不會。因此 createThenCheckedRollback 明寫 rollback-for,Java 對照方法也明寫 rollbackFor=Exception.class。移植時除了方法名,還要核對 propagation、isolation、timeout、readOnly、交易管理器及例外規則;新版 API 能載入,不代表那些語意自動對齊。

先猜會留下幾筆,再執行測試

bash
mvn test
mvn package

每個案例都從空表開始,測試本身沒有 @Transactional。三種載入方式執行相同的測試 body,另有自己 new Service 的反例,共四個 JUnit invocation。表裡是每種設定都會檢查的六個情境,不要把它誤報成 18 個獨立 JUnit 測試。

呼叫方式剩餘筆數方法內觀察到交易
正常 create1有
createThenFail:runtime exception0有
createThenChecked:預設規則1有
明寫 checked exception 回滾0有
同類別內呼叫 createThenFail1沒有
TicketEntry 呼叫代理 Service0有
本機觀察:方法結束或拋錯後,再從資料庫查筆數。

同類別內呼叫,為什麼是不同路徑

text
Caller → Spring proxy → createThenFail → rollback

Caller → selfInvokeFailure → this.createThenFail
                           (no new proxy crossing)

TicketEntry → injected Service proxy → createThenFail → rollback

selfInvokeFailure 本身沒有交易,內部直接呼叫自己的方法,沒有再穿過代理。這次方法內觀察不到交易,H2 留下一筆;換成 TicketEntry 呼叫注入的代理,則觀察到交易並回滾。若外層原本就有交易,或採 AspectJ weaving,條件就不同,不能把「self-invocation 一律沒有任何交易」當口訣。

練習答案是先保留交易契約,再選設定寫法。若為了讓畫面上全是 annotation,順手改了 checked exception 的回滾規則,就已經改變行為。讀完一段舊 XML,能畫出物件與代理路徑,再指出驗證案例,比把 XML 全部清掉更接近這次升級的需求。

下一課:MDB、DB 與重送的失敗邊界

參考資料