在上一篇閒談軟體設計:日誌框架提到,新的專案沒有用 Spring Boot (Spring framework),這樣說不太精確,在專案一開始,我曾考慮過 Spring Boot 及 Spark framework (不是 Apache Spark),而那一輪的思考後,一開始是決定用 Spring Boot 作為新專案的 web 框架。
從選擇 Spring Boot 到離開 Spring Boot
我在第一份工作開始學 Spring framework 並用在實際的專案裡,雖然後續的工作沒有繼續使用,但仍有持續在追蹤。搭配 annotation,開發其實蠻方便的,像是用 @PostMapping
將一個單純的 method 變成一個 endpoint,搭配 @Autowire
自動注入 dependency,用 @Transactional
管理資料庫的交易,加上其他 annotation,還能自動生成 Swagger 文件。
本文所謂的輕量是指相較於 Spring framework 或是 J2EE 這類框架,但說起來也挺有趣的,Spring framework 當初提出來時也是說自己是比 J2EE 輕量,如今,自己也變成反向指標了。
輕量當然會有些功能上的犧牲,同樣的 endpoint,寫起來大概像這樣,很簡潔,但要自己處理 dependency 注入 (第 5 行),request body 和 Java 物件之間的轉換 (第 12 行),以及 Java 物件輸出成 JSON response (封裝在第 7 行的 enclosed
函式中)。對我來說,這都是小事,沒有選擇 Spark framework 的主要原因,是我找不到簡單自動生成 API 文件的方法,以及它數年來更新的頻率很低,感覺作者自己都要棄坑了。
那 Spring Boot 用得好好的,為什麼要換掉?主要是自由度,當時為了使用 JPA 的 annotation,於是在 pom.xml
加入對應的套件,然後,原本可以動的程式就掛了,啟動時會抱怨找不到 data source,看錯誤訊息我大概知道怎麼處理,只要加上資料庫相關的設定,應該就可以了。
但是,我還沒決定要用什麼資料庫,我還在猶豫用傳統的關聯式資料庫,還是用 NoSQL 的解決方案,當下我只想先專注在商業邏輯層,資料庫的存取先用 mock 的方式處理。只是沒有設定 data source,就讓整個程式終止這件事讓我開始思考,我為了加速開發讓 JPA annotation 進到核心層已經違背我自己過去的原則,換來的卻是這個錯誤?
下個框架選擇
當時有數個選擇,照 Spring Boot 的慣例,把 data source 設定好,繼續開發;把 JPA annotation 從核心層移除,移到另一個 project,延後 Spring Boot 載入 JPA 的時間等等。
換掉 Spring Boot 也是其中一個選擇,但選擇新的框架,必須該框架能直接滿足或是間接滿足幾個條件:
- 輕量。程式最終會整個打包成可以單獨啟動的 JAR 檔並封裝成 docker image,希望這個 image 越小越好,加速 k8s 下載 image 的時間。
- API 文件。當初放棄 Spark framework 就是沒有找到簡單生成文件的方法,新的框架要能生成文件。
- 持續維護。不需要像 Spring framework 那樣背後有公司直接維護,但至少社群還是有人在維護,定期有修正錯誤。
- 自由度。有人喜歡照著 framework 的規定,該怎樣就這樣,省去一些爭執的麻煩,但有時候有些 framework 規定很多,變成綁手綁腳的,新的框架需保有一定的自由度。
- 資料庫交易與安全檢查。這兩個是 Spring Boot 有提供的功能,若新框架有提供很好,但只是加分項目。即便沒有,只要能輕鬆實現的話也行。
- 支援 Virtual Threads。這也是加分項,Spring Boot 有支援,之前自己實測效果很好。
- 支援 GraalVM。加分項,初期應該還是以 JVM 為主,但之後可能會選擇打包成原生映像檔來加速。
回去找當初介紹 Spark 的那一期雜誌,裡面還介紹 Micronaut、Helidon 以及 Javalin,但由於時程的關係,沒有太多的時間可以讓我每個框架都實際下去試試,主要是靠閱讀官網和其他參考文件。
沒有花時間做 POC 其實是有風險的,官網通常只講好的部分,而其他參考文件則是帶有作者主觀的想法,不一定符合自己的見解,所以,如果有時間,我還是會建議做 POC。
三個框架在輕量這一項,看起來都很不錯,Helidon 官網直接放出圖表。在 API 文件的產生,三個框架都能透過 plug-in 支援。持續維護上,Javalin 相較其他兩個稍弱,但有持續在修正錯誤。自由度上,個人覺得 Javalin 比較好,但這是基於其他兩個框架都有資料庫相關的配套,某種程度上規則會比 Javalin 多一點,Javalin 則是很單純的 web 框架。在安全檢查上三個框架都有對應的設計。剩下的加分項則是三個都有。
最後,我選擇了 Javalin。老實說,這個選擇稍微有一點點冒險,因為要自己補上資料庫交易的控制,還好,這部分並沒有花太多的時間,就有一個夠用的版本,後續再介紹。一個簡單的 endpoint 會像這樣:
基本上非常像 Spark framework,多出來的內容是用於產生 API 文件的,request body 和 Java 物件的轉換也有簡單的支援 (第 24 行),整體也十分簡潔。打包後的 flat JAR 檔挺小的,docker image 在使用傳統 JVM 的情況下 150 MB 左右。
核心層與 Web 框架的距離
目前系統已經上線,也用了好一陣子,沒遇到什麼問題,剩下要擔心的是 Javalin 會不會停止更新?如果真的要再次換掉 Javalin,其實問題也不大,看上面三個版本的程式碼,眼尖的應該會發現,裡面根本沒什麼邏輯,只做一件事:把 request body 轉成物件,呼叫核心層,將結果轉成 response。
真正的商業邏輯都在核心層,如果要再次換框架也只是根據新框架,換成對應的寫法。即便我不是選 Javalin,而是 Helidon 或 Micronaut,然後換成別的框架,也是一樣。這完全是建立在我個人偏好的原則:始終與框架保持距離。讓自己不被框架給綁住,有更多的自由度。
小節
最後,其實選擇哪個 web 框架並不重要,這次選擇從 Spring Boot 離開,換到 Javalin,不表示我之後就不會使用 Spring Boot,Spring Boot 仍是一個好用的框架,只是這次我在意的因素裡, Spring Boot 拿到的分數比較低,下個專案可能會有不同的考量,可能會推導出不同的結果。
重要的是「怎麼選擇框架」,寫程式時因為要快,很多的決定其實很直接 (靠直覺)。但這樣的決策品質不一定都是好的,有時候還是靜下心,把所有會影響決策的因素列下來,根據每一項因素去打分數,然後再做決定,即便分數會有主觀的成分在,但在多個因素通盤考量下,通常會有較好的決策品質。
上面列舉的因素,是我根據專案列下的因素,不表示符合你的專案,在思考時請根據你的專案、團隊和需求去列因素。
當然,想要讓自己有彈性選擇框架,或是換框架,重點就是如何讓核心與框架保持距離,我不認為我完全遵守 Clean Architecture,但保持距離讓我有足夠的彈性可以隨時做出改變。