
圖片來源:ChatGPT 生成
在決定使用 Javalin 取代 Spring framework 作為後端的框架後 [1],除了安全控管 [2][3]、日誌 [4] 和交易管理 [5],還有一個被忽略,但其實非常重要的基礎設施:配置 (Configuration)。今天就聊一下在新系統裡怎麼處理配置的問題。
對照組
在看新設計前,先看一下對照組。一般在使用 Spring framework 開發時,可以用@Value 自動將配置檔的內容直接綁定到物件中。事實上 @Value 只是 Spring Boot 眾多處理配置的其中一種方式,但這些方式原則上都有幾個特點:
- 型別安全 — 會自動將字串轉成目標的型別
- 來源優先序 — 讀取順序從配置檔 → 系統參數 → 環境變數 → 預設值
- 使用 annotation 注入 — 使用上非常方便,但 framework 在啟動時需要透過 reflection與 metadata 處理 configuration binding
這些特點確實有幫助到開發者,但有個缺點,只能搭配 Spring framework 使用,而這個缺點正是選擇 Javalin 後最大的問題,但好在上述的問題並不是無法解決的。
設計目標
由於 Javalin 的核心思維是簡單與輕量,因此原生沒有提供任何配置的相關框架,此時,就只好先回顧一下 Java 原生常見的幾種處理方式:
- 啟動參數 — 在啟動應用程式時,帶入參數,但需要寫參數的解析程式,這邊可以用 JCommander 幫忙處理,還算容易
- 系統參數 — 也是在啟動應用程式時,透過
-Dsecret=value的方式定義系統參數,然後在程式裡用System.getProperty("secret")取得參數 - 環境變數 — 在程式中用
System.getenv("secret"),使用方式很像系統參數,差別是變數的來源不同 - 配置檔 — 即便不使用 Spring framework 也是可以自己寫程式讀取配置檔
在雲原生的環境中,直接透過環境變數通常會比啟動參數或系統參數更直觀,剩下就是環境變數與配置檔,最後我選擇的是環境變數,不需要解析 JSON 或 YAML。
但如果單純使用環境變數,會遇到一個問題是 key 是四散在各個角落,難以管理,所以還是需要一個抽象層,而這個抽象層希望達成幾個目標:
- 集中管理 — 這裡的集中管理是指 key、預設值與使用位置在同一個模組中定義,而不是散落在不同地方。
- 型別安全
- 慣例優先 — 如果沒有特殊需求,默認使用慣例,但如果有特殊需求,能夠隨時替換
- 容易實作 — 雖然 annotation 使用上很方便,但背後的處理很麻煩,因此新的抽象層要在使用和實作上都要很方便
- 隔離來源 — 雖然已經確定使用環境變數,但不希望使用端直接呼叫
System.getenv()。
這裡,來源優先序不在這次的考量內,因為整個系統都會使用環境變數,不需要為不存在的需求讓設計變複雜,只有確保來源是可以被抽換的。
Interface-based Typed Configuration
首先,在這個設計中,configuration schema 直接以 Java interface 的形式表達。因此,設計中有一個非常重要的基礎,雖然只有一個檔案,卻提供了環境變數的讀取、型別轉換與預設值的處理:
接下來,每個需要配置的地方,定義自己的 Configuration,以用 SendGrid 發送郵件為例,可以定義一個介面 SendGridClientConfig 提供呼叫 API 時需要的 API Key:
這個檔案提供了三個功能:
- 定義慣例 — 環境變數的 key 就是
SENDGRID_API_KEY,如果沒有其他需求,不需要再提供 - 定義預設值 — 在這裡預設值是
null,因為這資訊是不能明文放在程式碼中的,但如果可以明文儲存,這裡可以放一些預設值,例如 Javalin 啟動時要使用哪個 port。 - 提供 API Key — 由
getSendGridApiKey用慣例的 key 呼叫getConfig來取得 API Key,如果沒有值則回傳預設值。
而這個檔案的位置和使用的檔案 SendGridClient 是放在一起的,這個類別提供兩個建構子:
- 預設建構子 — 由於
SendGridClient黏合 (SendGridClientConfig),在沒有提供 config 的情況下,能用自己作為 config。 - 能指定
SendGridClientConfig的建構子 — 有任何不符合慣例或額外設定的情況下,提供符合SendGridClientConfig介面的實作。
Java 沒有原生 mixin (黏合) 的語法,Java interface 的 default method 在某種程度上提供了類似 trait 的能力,因此可以被用來黏合。
如此一來,當初希望達成的四個目標都算是達到了:
- 集中管理 — key 的定義、預設值與使用方都集中在同一個地方,要使用時很容易較能找到 key
- 型別安全 — 定義 configuration 時,能直接提供正確型別的值
- 慣例優先 — 如果沒有特殊需求,用預設建構子,然後把 API Key 用預設的 key 設為環境變數就可以使用
- 容易實作 — 雖然不像 annotation 那樣簡單,但實際上也沒有寫多少程式碼,而且要抽換實作也非常容易
- 隔離來源 —
SendGridClient不需要知道 API Key 的來源,只依賴 configuration interface
進階議題
雖然來源優先序不再這次的設計目標中,但如果未來要加入相關的支援,有幾種方式可以實現:
- Template method — 若策略固定,可以在定義 configuration 的地方透過 template methods 決定讀取來源與讀取的順序
- Responsibility chain —若想要動態來源,可以透過串接數個相同介面的實作,動態地調整讀取的順序
- Builder — 想要彈性的組合側月,可以用 Builder 決定一個慣例的優先順序,在建立時可以替換掉順序中的某幾個步驟,例如忽略某個來源或是替換不同的實作
總而言之,目前的設計是非常輕量,易於使用,而且保留了未來的擴充性。
比較
最後簡單和對照組的 Spring framework 做一個比較。如圖所示,這個設計只依賴一個簡單的 Config 介面,如果需要在其他專案中使用,基本上只需要複製這個介面即可,不依賴任何特定框架。
雖然沒有方便的 annotation 注入,但也避免了使用 reflection 進 binding。所有 configuration 都是透過明確的方法呼叫取得,讓整個流程更直接,也減少了啟動時需要進行的額外處理。在雲環境中,應用程式通常會頻繁地啟動與部署,減少啟動時的負擔往往是有意義的。
在功能上,這個設計仍然保留了幾個重要特性,例如型別安全,以及未來擴充來源優先序的可能性。雖然不像 Spring framework 那樣完整,但在保持輕量的同時,也提供了一個簡單且實用的替代方案。

Spring framework Configuration vs Interface-based Typed Configuration
小結
當從 Spring framework 轉向 Javalin,原本依賴 annotation 注入配置的方式無法使用,因此需要一個和 Javalin 搭配的替代方案,透過 interface-based typed configuration,在保持輕量的前提下,達成了集中管理、型別安全、慣例優先、容易實作與隔離來源等目標,是一個務實且簡單的解決方案。




















