10 · 组件扩展与封装

这是 ACE 可扩展性的核心章节。ACE 的扩展单元是 Component(对标 MidwayJS Component / Spring Starter)。

没有独立的 @Plugin 机制——所有可插拔扩展统一用 @Component。OpenAPI 等内置能力的「自动生成」也不走 plugin,而是宏在编译期把元数据自注册、启动期零反射生成。

中间件扩展

最轻量的扩展是中间件。类型定义在 ace-web:

public type Middleware = (Context, Next) -> Unit
public type Next = () -> Unit

@Middleware[order]:声明式注册中间件

标注「返回 Middleware 的无参函数」,按 order 升序插入洋葱(小者先执行):

@Middleware[100]
public func requestTimer(): Middleware {
    return {ctx, next =>
        let start = nowMs()
        next()
        ctx.setHeader("X-Elapsed-Ms", "${nowMs() - start}")
    }
}

装配顺序(buildApp):错误处理 → requestLog → cors → health → OpenAPI/docs → 用户 @Middleware(按 order) → rangeHandler → 路由。即用户中间件排在内置链之后、路由之前。

@Catch:异常过滤器

把特定异常类型映射为定制响应(区别于洋葱中间件)。处理函数必须恰好 2 参 (ctx: Context, e: 某异常类型):

@Catch
public func onTaskNotFound(ctx: Context, e: TaskNotFoundError): Unit {
    ctx.status = 404
    ctx.json("{\"error\":\"task not found\",\"code\":\"TASK_NOT_FOUND\"}")
}

@Catch 过滤器优先于默认 jsonErrorHandler;按异常类型匹配,首个命中者处理。配套 @Exception 宏免写自定义异常的样板:

@Exception
public class TaskNotFoundError {        // 自动 <: Exception + 标准构造器
    // 可加自定义字段
}

Component 模型

Component 用于封装需要配置、生命周期、依赖编排或 Bean 注入的可复用能力。

Component 接口

public interface Component {
    func name(): String                                  // 必须实现:组件唯一名
    func dependsOn(): Array<String>                      // 默认 []:依赖的组件名,其 setup 先于本组件
    func defaults(): Array<(String, String)>             // 默认 []:默认配置(键含命名空间),仅用户未设时合并
    func enabled(config: Config): Bool                   // 默认 true:条件装配,false 则整个跳过
    func setup(ctx: ComponentContext): Unit              // 默认空:向应用贡献能力
    func onReady(): Unit                                 // 默认空:服务起监听前
    func onStop(): Unit                                  // 默认空:停机时(逆序)
}

ComponentContext:贡献能力的入口

setup(ctx) 里通过 ctx 向应用贡献中间件或 Bean,并读取命名空间配置:

public class ComponentContext {
    public let config: Config
    public func namespace(ns: String): ConfigView                  // 带 ns. 前缀的配置视图(隔离)
    public func useMiddleware(order: Int64, mw: Middleware): Unit   // 注册中间件
    public func provide(name: String, factory: () -> Any): Unit    // 注册单例 Bean
}

ConfigView(namespace 返回):get/getOr/getInt/getBool/has,键自动加前缀。

@Component:自注册

@Component 标注类,生成顶层 let __ace_component_X = registerComponent(X())——包被 import 时执行完成自注册。类须有无参 init()、至少实现 name():

package myapp.component
import aceboot::framework.*
import aceboot::framework_macros.*

@Component
public class GreetingComponent {
    public func name(): String { "greeting" }

    public func dependsOn(): Array<String> { [] }              // 可选

    public func defaults(): Array<(String, String)> {          // 可选,键含命名空间
        [("greeting.enabled", "true"), ("greeting.text", "hi")]
    }

    public func enabled(config: Config): Bool {                // 可选,条件装配
        config.getBool("greeting.enabled", true)
    }

    public func setup(ctx: ComponentContext): Unit {           // 贡献能力
        let cfg = ctx.namespace("greeting")
        let text = cfg.getOr("text", "hi")
        ctx.useMiddleware(50, {c, next => c.setHeader("X-Greeting", text); next()})
        ctx.provide("GreetingService", {=> GreetingService(text) as Any})
    }

    public func onReady(): Unit {}                             // 可选
    public func onStop(): Unit {}                              // 可选
}

自注册靠 import 触发。应用 main 必须 import 组件所在包(如 import myapp.component.*),否则组件不装配。也可显式 AceApplication.use(GreetingComponent())(须在 run 之前)。

生命周期与装配顺序

loadProfiles → setupComponents → buildApp → readyComponents → serve → (停机) stopComponents + @PreDestroy

setupComponents 内 resolveComponentOrder 严格三步:

  1. 合并默认配置:遍历每个组件 defaults(),仅在用户未设置时填入(不覆盖);
  2. enabled 过滤:enabled(config) 为 false 的剔除(此时已能读到 defaults 贡献的开关);
  3. 拓扑排序:按 dependsOn DFS,依赖先 setup;检测到依赖环抛异常;依赖未启用/不存在则静默忽略。

onStop 按装配逆序调用,try/catch 包裹(单个异常不影响其余)。

内置组件如何写的(范例)

ACE 自带能力分两类,是学习封装的最佳范例:

真·Component(可插拔,import 即装配)

  • RateLimitComponent(ace-security)—— 标准范例:defaults() 给 [ratelimit] 默认、enabled(config) 条件装配、setup 里 ctx.useMiddleware(20, rateLimit(...))。**「贡献中间件」**路径。
  • OrmComponent(ace-orm)—— setup 里从 ctx.namespace("datasource") 读配置(缺必填则 fail-fast 抛异常)、setGlobalDataSource、遍历实体仓储 createTable() 并 ctx.provide(name, factory) 注册为可注入 Bean。**「贡献 Bean + 依赖编排」**路径(SeedComponent 用 dependsOn ["orm"] 保证建表后再种子数据)。

不是 Component 的内置能力

  • 安全头/CSRF/JWT 是裸 Middleware 工厂(securityHeaders()/csrf()/jwtAuth()),由应用用 @Middleware 自行接入;
  • CORS/请求日志/健康检查/OpenAPI 直接在 buildApp 内置接入。

封装能力的复杂度梯度:简单横切 → 裸 Middleware 工厂;需配置/条件装配 → Component(贡献中间件);需 Bean 注入 + 依赖编排 → Component(贡献 Bean)。

把能力封装成可复用 module

完整步骤(对照 ace-orm/ace-security 的布局):

  1. 建 module:独立目录 + cjpm.toml,output-type = "static",包名如 package ace_foo。在根 cjpm.toml 的 [workspace].members 登记。
  2. 写能力本体:裸函数 / 中间件工厂 / Bean 类(如 fooMiddleware(): Middleware 或 class FooService)。
  3. 写 Component 封装:新文件 foo_component.cj,@Component public class FooComponent,实现 name(),按需 defaults()/enabled/dependsOn/setup。
  4. 在 setup 中贡献能力:用 ctx.namespace("foo") 读配置;ctx.useMiddleware 接中间件或 ctx.provide 注 Bean;缺必填配置 throw fail-fast。
  5. 应用接入:应用 cjpm.toml 依赖该 module,main 里 import ace_foo.*(触发 @Component 自注册),在 application-{env}.toml 配 [foo] 段。无需手写 App().use(...)。
// ace-foo/src/foo_component.cj
package ace_foo
import aceboot::framework.*
import aceboot::framework_macros.*

@Component
public class FooComponent {
    public func name(): String { "foo" }
    public func defaults(): Array<(String, String)> {
        [("foo.enabled", "true"), ("foo.size", "100")]
    }
    public func enabled(config: Config): Bool { config.getBool("foo.enabled", true) }
    public func setup(ctx: ComponentContext): Unit {
        let size = ctx.namespace("foo").getInt("size", 100)
        ctx.useMiddleware(20, fooMiddleware(size))
        // 或 ctx.provide("FooBean", {=> FooBean(size) as Any})
    }
}

可观测

ace-observability 提供进程内指标与结构化日志(无 stdx)。

指标

public let metrics = Metrics()            // 全局实例

metrics.requestCount(): Int64
metrics.errorCount(): Int64               // status >= 500 计入
metrics.avgLatencyMs(): Int64
metrics.errorRate(): Float64

接入用 metricsMiddleware()(后置测真实延迟并 record)+ metricsEndpoint("/metrics")(暴露 Prometheus 文本:ace_requests_total/ace_errors_total/ace_request_latency_ms_avg),经 @Middleware 或 App().use(...) 注册。指标经全局 metrics 实例读取(进程内),无 Prometheus scrape 端点。控制面 ace-autopilot 即读它驱动调参。

请求追踪 + 结构化日志

requestId(): Middleware                   // 生成/透传 X-Request-Id,写 ctx.state 与响应头
structuredRequestLog(): Middleware        // 后置输出一行 JSON:{requestId,method,path,status,latencyMs}

logger.info("msg", [("k", "v")])          // 全局结构化日志器:info/warn/error,输出一行 JSON

下一章:智能体运行时。

分布式追踪(W3C Trace Context)

tracingMiddleware()(ace-observability)解析入站 traceparent 沿用其 trace-id、否则新建,每请求新建 span-id,写入 ctx.state 并回写 traceparent 响应头;currentTraceId(ctx) 读取。跨服务传播:调用方读 currentTraceId 并随出站请求带上 traceparent 头。

@Middleware[0] public func tracingMw(): Middleware { tracingMiddleware() }
// 业务: currentTraceId(ctx) → 写日志 / 透传下游