JBoss EAP 08:把 Spring Boot 打包成 WAR,順手抓出 context root 的陷阱
同一支新增工單 API,在根路徑測試通過,搬到 /training 後卻可能回傳一個走不通的 Location。這次把前面 Spring Boot 系列的工單程式做成 WAR,新增的測試就抓到了這個問題:回應少了部署前綴。打包方式的改動,會把原本沒測到的路徑假設帶出來。
本課提供完整可建置的 source,保留原例的 Service、JDBC、H2 與錯誤契約。已在本機完成 WAR 打包、九項測試與兩種 context 的實際 HTTP 檢查。這些 HTTP 請求進入的是內嵌 Tomcat;尚未部署到 EAP,也沒有把目標環境相容性當成已解決。
下載第 08 課:完整 WAR 專案、驗證程式與 EAP 工作表先分清楚:WAR 有兩條啟動路徑
Executable WAR (verified locally)
java -jar → WarLauncher → main → embedded Tomcat → Spring MVC
External container (not verified here)
EAP starts → loads WAR → servlet initialization
→ SpringBootServletInitializer → application contextWAR 描述包裝結構,不代表每次都由外部伺服器啟動。這個範例保留 Boot 的重新打包外掛,因此 WAR 也能用 java -jar 執行。外部容器則透過 Servlet 初始化機制載入應用。先問這次走哪條路,才知道成功啟動究竟驗證了什麼。
Maven 改包裝,也改依賴的責任
<packaging>war</packaging>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>這是完整 pom.xml 裡的兩段重點,不能把片段直接當整份 POM。packaging 決定 Maven 產出 WAR;provided 表達容器依賴的角色。Java 21 與 Boot 3.5.16 維持原版本,其他 starter 也保留,先避免把打包與框架升級混在同一次練習。
本次打開實際 WAR,Tomcat core/websocket 10.1.55 位在 WEB-INF/lib-provided,驗證功能需要的 tomcat-embed-el 則在 WEB-INF/lib。檢查 validation starter 的 POM,可以看到它另以 compile scope 依賴 EL。因此「provided 就代表所有 Tomcat jar 都消失」並不符合這個產物;進 EAP 前還要核對 EL 與容器模組的提供者。
讓外部容器知道要載入哪個應用
public class LabApplication extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(LabApplication.class);
}
public static void main(String[] args) {
SpringApplication.run(LabApplication.class, args);
}
}完整類別仍有 @SpringBootApplication 與原本的設定註冊,片段省略了 import 和 annotation。configure 指定外部初始化的應用來源;main 保留可執行模式。這兩個入口不能互相當成測試證據:目前 configure 已編譯入 WAR,但外部容器的生命週期還沒實際跑過。
先重現:Location 為什麼漏掉 /training
// Previous expression in the root-context JAR lab:
URI.create("/api/tickets/" + ticket.id())
// WAR lab: preserve the actual request context.
URI.create(httpRequest.getContextPath() + "/api/tickets/" + ticket.id())Controller 映射的是 /api/tickets,context root 是容器在它前面加上的路徑。根部署的 getContextPath() 是空字串;在 /training 下則會取得 /training。這次修正保留 origin-relative 的 Location,只把遺漏的 context 補回。原本範例的根路徑行為仍由原八項測試保護。
新增的第九個測試用 MockMvc 在 /training 建立工單,核對 Location,再跟隨它讀回同一個 id。修正前實際得到 expected /training/api/tickets/<id>、actual /api/tickets/<id>;修正後通過。這個反例比單看「九個測試全綠」更有用,因為它說明新測試確實能抓到這項缺陷。
這個修正涵蓋直接連線與預設 servlet mapping。公司代理若另外改寫 /gateway 前綴,或應用採用自訂 servlet path,還要另外處理及驗證;不能把 getContextPath() 當成所有外部網址的組裝公式。
動手:檢查產物,再真的呼叫它
mvn package
python3 verify_war.py在解壓縮後的 spring-boot-war 目錄執行。需要 Java 21、Maven 3.6.3 以上與 Python 3;首次 Maven 執行會下載依賴。腳本先確認 Java class major 65、manifest、應用類別與依賴位置,再以隨機本機 port 啟動 WAR,依序測根 context 與 /training,最後關閉自己啟動的程序。
根路徑有七項 HTTP 檢查:ping、新增、跟隨 Location、空白輸入、壞 JSON、過長標題與不存在的工單。/training 再加一項:直接呼叫沒有前綴的 /api/ping 應回 404。兩組合計 15 項通過,結果和 log 留在 target/verification。HTTP 證據補上真實連線,H2 回滾則由 Maven 的整合測試觀察。
到 EAP 前,還缺哪些證據
工作表會要求你記錄 EAP update、JDK、WAR hash、runtime-name、實際 context root,以及 logging、EL、資料來源與交易由誰提供。範例仍是應用自己管理 H2,沒有改成 JNDI;也沒有放入一份泛用的模組排除設定。先確定目標容器責任,才知道哪些依賴與設定需要調整。
server.address、server.port 與 server.servlet.context-path 約束的是本例的可執行模式,不能替 EAP 設定監聽位址或保證它只對本機開放。範例沒有正式認證、安全控制、DB2 或 MQ;外部部署只能在已確認的隔離練習環境進行。
自我檢查:哪個成功不能替代哪個成功
如果把 configure 拿掉,java -jar 仍可能正常啟動嗎?可能,因為這次走的是 main;這是依啟動路徑做的推論,本課未執行這個移除實驗。相反地,Location 缺前綴的失敗是已實際觀察的測試結果。交接時把兩者分清楚,讀者才知道哪些結論可以直接採用、哪些還要補做。
接回第 07 課:把觀察結果做成可比對的證據下一課:Spring XML 與交易對照