跳至主要内容

Feature 設計與使用

適用對象: 維護 appfuse-serverapp-tenant-server,或以 AppFuse 建立下游 server 的團隊 最後更新: 2026-08-18 相關決策: ADR-014ADR-024方法論 ADR-006方法論 ADR-012

本指南的 Feature 是 server reference implementation 中可 provision、可版本化升級的 feature slice;不是產品規格裡的 Feature List,也不是「所有功能程式碼」的泛稱。

先分清三層

用途典型落點誰決定
Capability可重用的機制、演算法與 SPIappfuse-server jar 的 io.leandev.appfuse.*框架
Feature把 capability 接進應用的中性、可同步 slice,並可附帶預設 reference implementationapp 的 {basePkg}.feature.{id}config/feature-{id}.yml 與 catalog referenceDomains框架維護 core;下游擁有 seam 與附屬 reference domain
Reference implementation可運行、預期依產品重塑的實作範例app 的 controller、service、entity、repository、security 與業務域應用

三層不是依檔案行數或是否使用 Spring 判斷,而是依所有權與生命週期判斷:

  • 固定技術機制若每個應用都不該重寫,放 capability。
  • 需要留在應用、但可以中性同步的組裝,放 Feature。
  • URL、HTTP method、wire DTO、entity schema、角色詞彙、資料治理與產品政策,放 reference implementation。

複雜度的優先順序

機制的首要目標是降低 reference implementation 的複雜度,其次才是降低 Feature 的複雜度。Capability 可以承擔必要且固定的技術複雜度,因為它只實作一次、由套件版本同步。

採用以下順序:

  1. 先消除每個應用都要重寫、且容易寫錯的機制。
  2. 再讓 Feature 只保留可讀的組裝與少量組態。
  3. 不為了讓 capability 類別更少,而把固定機制推回 Feature 或 reference implementation。
  4. 不以「拆出更多介面或檔案」冒充降複雜度;只有真實替換點才需要 SPI。

例如 RSA wire encoding、DAO authentication manager 的事件發布、resource-server filter 順序適合由 capability 承擔;金鑰來源、是否允許開發期 ephemeral key、登入鎖定是否套用於 M2M, 仍由應用組裝決定。

Feature 的設計不變量

一個 Feature 必須同時滿足:

  1. 中性:Feature 的 core slice 不得 import reference implementation 的 controller、service、 entity、repository 或產品業務域;附屬 reference domain 不屬 core,不受此句混淆。
  2. 低 drift:非 seam 檔應能由 /feature sync 零分歧覆蓋;持續 drift 表示分層錯位。
  3. 可移除:移除 optional Feature 後,不應在共用組態留下它的 bean、URL 或權限字串。
  4. 依賴顯式requires 是實際 import 邊與 beanRequires 的聯集;required Feature 不得依賴 optional Feature。
  5. 位址中立:Feature 與 capability 都不決定具體公開端點位址。
  6. 框架收納:Feature 一律位於 {basePkg}.feature.*;root 其餘業務域屬應用。

Feature 可以附帶 service、repository 或 entity 嗎?

可以,但它們不是 Feature core。若某 optional Feature 需要一套「開箱可跑、下游可重塑」的資源 實作,在 catalog 以 referenceDomains 附帶:

"service-account": {
"kind": "optional",
"requires": ["auth", "cache"],
"beanRequires": ["cache"],
"referenceDomains": ["serviceaccount"],
"seams": [
"src/main/java/io/leandev/app/feature/serviceaccount/ServiceAccountConfig.java"
]
}

這會形成兩種不同生命週期:

部分例子provisionsyncownership
Feature core / seamfeature/serviceaccount/ServiceAccountConfig安裝中性組裝core 可同步;seam 只顯示 diff由 variant-resolved source 提供
Attached reference domaincontroller/serviceaccountservice/serviceaccountentity/serviceaccountrepository/serviceaccountsecurity/serviceaccount隨 Feature 一起安裝並 stamp不盲覆蓋;逐檔顯示上游差異可用同路徑 overlay

因此「Feature 有 entity」的精確說法是:Feature 可以帶一個預設 entity reference implementation,但 entity 不會因此變成可零分歧同步的 Feature contract。移除 optional Feature 時,core、設定、seed、測試與所有 referenceDomains 必須整組離開。

Seam 不是逃生門

Seam 是下游擁有的組態接合點,通常是 *Config*Propertiesconfig/feature-{id}.yml。每個 config fragment 必須登錄為同名 Feature 的 seam,並逐檔標成 defaultableapplication-owned;它不會被 legacy sync 覆蓋,release upgrade 則依分類與 可信基線快轉或要求明確 resolution。

如果非組態檔反覆需要客製,不應只把它加入 seam:

  • 反覆變異的是一個值 → 變成設定。
  • 反覆變異的是一個行為點 → 抽 SPI,機制上收 capability。
  • 整個資源或 API 面都會因產品而異 → 降級 reference implementation。

檔名不是判準。名為 Config 但只做固定註冊的類別仍可同步;controller 若承載產品資源的 wire contract,屬 reference implementation;只把 capability 接成固定 Actuator/health/ system handshake 的 canonical adapter,則屬 Feature core。

公開端點的所有權

產品資源的具體 URL 與 HTTP method 由 reference implementation 決定。Capability 提供 filter、 parser、service、錯誤語意與 matcher 的消費機制,不擁有應用 URL。Feature 則可以擁有固定的 application integration surface:例如 instance-scoped Actuator endpoint、health/info contributor,以及不依賴產品模型的 system handshake。判準是是否預期由消費端重塑,不是 annotation 或路徑前綴。詳見 ADR-026

一般使用者/SPA 可達的 /api/**reference-first。即使 controller 只依賴 capability,path property 也只外部化位址,不會自動交還 HTTP method、操作集合、DTO、 錯誤與 authorization。StagingFileController 就是此修訂的例子:storage 與 cleanup 機制中性,但 prepare/PUT/batch/metadata/delete 與 isAuthenticated() 是消費端 API 政策,因此 controller 與 multipart/batch 設定住 controller/file reference domain。

以 auth 為例:

內容所屬
JWT/resource-server 固定 filter 組裝與順序Capability:ResourceServerSecurity
API-key header 解析與 principal lookup SPICapability
SecurityConfig 組裝 bean 與接受設定值Feature
POST /api/v1/auth/loginPOST /api/v1/auth/logout 等位址與公開政策Reference implementation
AuthControllerClientTokenController 與回應 DTOReference implementation
staging upload REST、wire、multipart/batch 與授權Reference implementation:controller/file
Account、service-account CRUD、API-key entity/repositoryReference implementation
/actuator/mail、mail health 與 mail:ops 授權Feature
/actuator/configcheck/api/v1/system/info 與隨行授權Feature

Reference implementation 的命名亦表達這條邊界:

  • ServiceAccountManagementService 只負責服務帳號與 API-key 的治理生命週期,不是認證引擎。
  • ServiceAccountApiKeyPrincipalLookupServiceAccountApiKey → AuthPrincipal 的 capability SPI adapter,不是 API-key 管理服務。

server-tenantless 下游直接從 app-tenantless-server 同 type source 選取 required auth 與使用者選擇的 optional Feature。選擇 service-account 時,來源內容會:

  • app.security.m2m.require-tenant 設為 false
  • 使用 tenantless 的 service-account account seed;
  • 透過該 Feature 的 referenceDomains: ["serviceaccount"] 保留附屬 reference domain;
  • 使用 ownership-native 的 ServiceAccountManagementPolicy

因此 service-account 直接取得 ownership 版;未選則 core、reference domain、設定與 seed 一起裁掉,不經 variant 組裝或文字轉換。

因此框架指南出現 /api/v1/auth/... 時,那只是 app-tenant-server參考路徑,不是 capability 或 Feature 的固定契約。下游可以改路徑,但必須同步修改 controller mapping、security matcher、 OpenAPI、client 與測試。

公開規則要精確

Reference implementation 應以 HTTP method + exact path 宣告匿名端點,不使用 permitAll("/api/v1/auth/**")。新增在同一 prefix 下的端點會因此預設落入 authenticated(),不會靜默公開。

目前 app-tenant-server 的參考白名單是:

MethodPath原因
POST/api/v1/auth/login取得憑證前無法先認證
POST/api/v1/auth/refreshrefresh token 由端點驗證
POST/api/v1/auth/logout未帶 token 也要能冪等成功,供過期 session 清理 UI 狀態
POST/api/v1/auth/tokenoptional service-account Feature 的 reference endpoint;M2M client credentials 由 capability 驗證
POST/api/v1/auth/exchange一次性 code 交換 session
GET/api/v1/auth/link/consume簽章連結兌換

這份表是 reference implementation 現況,不是 Feature catalog。特別注意:

  • /api/v1/auth/token 的 exact matcher 由 ServiceAccountSecurityContributor 與 controller 一起隨 optional Feature provision;required auth 不預留此位址。
  • permitAll 只讓匿名請求到達 controller;若請求攜帶無效或過期 Bearer token,標準 resource-server filter 仍可能在 controller 前回 401
  • logout 刻意接受「沒有 Authorization header」並回成功;若產品要求連無效 Bearer 都忽略, 必須另外設計 bearer resolver 的排除規則,不能靠 permitAll 達成。

如何設計一個新 Feature

1. 先找 capability

先問:「這段機制放進 jar,會不會奪走應用的宣告權或替換權?」

  • 不會:先在 appfuse-server 建 capability。
  • 會:留下最小組裝點,並讓 capability 以介面接受差異。
  • 涉及產品資源、資料模型或 HTTP contract:直接放 reference implementation。

新增 capability 時,同一個發版週期應交付對應 Feature、catalog entry、測試與 CHANGELOG;否則 下游升級只拿到 API,卻沒有正確接法。

2. 建立中性 slice

Java package 放在:

src/main/java/{basePkgPath}/feature/{featurePkg}/

設定片段放在:

src/main/resources/config/feature-{id}.yml

Feature 只 import:

  • JDK、Spring 與第三方函式庫;
  • io.leandev.appfuse.* capability;
  • catalog 宣告的其他 Feature。

Feature 不 import root 的業務 package。對業務資料的需求應轉為 capability SPI;SPI 的具體 adapter 住 reference implementation。

3. 登錄依賴與 seam

在 source features.json 登錄:

  • kind:required 或 optional;
  • requires:可由 import 與 bean 需求解釋的直接依賴;
  • beanRequires:編譯圖看不見的 bean 依賴;
  • referenceDomains:選填,隨 Feature provision/scaffold 的預設 reference implementation domain;其檔案可客製、sync 不盲覆蓋;
  • seams:真正由下游擁有的組態接合檔。

不要把產品 resource controller、entity 或業務 service 登錄成 seam 來保留它們;它們應先 離開 feature/。零 drift 的 canonical application adapter 留在 core,不登錄為 seam。

app-tenant-server 目前的直接相依

下表是 source catalog 的直接邊;安裝時仍須取遞移閉包。ownership 的「支援」表示該 Feature 會進入 ownership 組裝樹並通過 I4,不代表每個產品都必須安裝它。

Featurekind直接 requiresreferenceDomainsownership
cacherequired支援
schedulingoptional支援
authrequiredcache支援
actingoptionalacting支援
calendaroptionalcalendar支援
tenantoptionaltenant排除
almanacoptionalcache支援
auditoptionalauthaudit支援
delegationoptionalacting, authdelegation支援
fileoptionalschedulingfile支援(overlay)
persistence-encryptionoptional支援
mailoptionalauth, persistence-encryptionmail支援(overlay)
platform-infooptionalauth支援
reference-dataoptionalauthreferencedata支援(overlay)
service-accountoptionalauth, cacheserviceaccount支援(overlay)
signed-linkoptionalauth, cachesignedlink支援
notificationoptionalauth, mail, scheduling, signed-linknotification支援(overlay)
notification-tenant-overrideoptionalnotification, tenantnotificationtenantoverride排除
impersonationoptionalacting, auth, notification, signed-linkimpersonation支援

notification-tenant-override 是刻意的 reference-only micro Feature:中性貢獻契約由 notification 提供,本 Feature 只附帶 tenant 專用的範本覆寫資源域。ownership 因排除 tenant,會透過 requires 閉包一併排除它。

persistence-encryption 是應用組裝 Feature:framework jar 提供 crypto primitive,Feature 決定 root key ring 的設定 namespace 與 Spring bean。mail 以固定 purpose 取得 TextCipher, 不另增部署 key;notification 的 Outbox 預設使用 identity,沒有直接依賴。由於 notification 仍依賴 mail,provision 的遞移閉包會間接包含本 Feature;business data encryption 則由應用依 資料分類另外選配。

4. 用測試守住邊界

至少驗證:

  • Feature 不依賴 reference implementation;
  • required 不依賴 optional;
  • capability 不依賴 app package;
  • optional 功能關閉或 lookup 缺席時 fail-closed;
  • reference implementation 的 exact public endpoint matrix。

如何使用 /feature

Feature 採「來源發布、下游升級」:來源端發布不可變 app release 並盤點 fleet,下游在自己的 工作區執行有損的升級交易。

:::caution 實作過渡期

ADR-012 已接受,但 /feature upgrade、Feature Changelog 與新 catalog state 尚待依 ADR 的 實作順序落地。現行 /feature sync 只視為 legacy 機制;過渡期不得用無 target 的 broadcast write 代替正式發布/升級流程。

:::

# 來源端(唯讀盤點/發布)
/feature status
/feature status <target>
/release-app

# 下游端(寫入)
/feature provision <id> --from app-v<CalVer>
/feature upgrade [<id>] --to app-v<CalVer>
操作用途是否寫檔
status查看 fleet 或單一 target 的已裝 Feature、drift 與落後 commit
provision自動判斷 absent→install、present→migrate、partial→補齊
upgrade依 app release 更新 canonical core,並對 seam 做有基線的 three-way merge

操作紀律

  • 一次 provision 一個 Feature;每一步先看 drift 分類,再 build 與執行 catalog invariant check。
  • provision 會解 requires 遞移閉包,不應手動只複製部分檔案。
  • core 自動覆蓋只適合中性 core slice;若準備把某檔永久列為 localSeams,先判斷它是否其實應上收 capability 或降級 reference implementation。
  • coreRef 是 per-Feature;seam 的 reconciledRef 是 per-file。只更新實際完成的 surface, 不以 core 成功替未處理的 seam 推進基線。
  • target 的模組 type 決定同 type source:server-tenantserver-tenantless 各自升級, 不跨 type upgrade;共同 app tag 只提供 release 座標。

完整的 migrate 與 drift 分類規則見 方法論 ADR-007ADR-012ADR-010

Review 快速判定

問題
每個應用都應以相同方式實作,而且錯了是技術或安全問題?Capability繼續問
是應用必須看見並選擇的中性組裝嗎?Feature繼續問
含 entity、repository、產品 DTO/角色詞彙/資源政策嗎?Reference implementation繼續問
是只依賴 capability、且 wire/操作/授權皆無合理 retarget 需求的固定 adapter 嗎?Feature coreReference implementation
Feature 非組態檔持續 drift 嗎?抽 capability 或降級 reference implementation可維持 canonical
新增公開端點只靠共用 prefix 自動放行嗎?改成 exact method + path保持