03 · 核心内核

声明式宏层之下是一个框架无关、不依赖 stdx 的洋葱中间件内核(ace-web)。理解它有助于写中间件、调试请求流,以及在不使用宏的场景下直接编程。

App:应用与洋葱驱动

public class App {
    public func use(mw: Middleware): App          // 注册中间件(执行顺序 = 注册顺序),链式
    public func onError(handler: (Context, Exception) -> Unit): App  // 覆盖默认错误处理
    public func handle(ctx: Context): Unit         // 驱动整条洋葱处理一个上下文,顶层兜底异常
}

App.handle 把已注册的中间件 compose 成一条管线执行;任一中间件抛异常由 onError(默认 defaultErrorHandler)兜底。声明式应用里你通常不直接建 App——AceApplication.buildApp() 已组装好;但写测试或低层程序时可手动用。

Middleware 与洋葱模型

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

洋葱语义:中间件在 next() 之前执行前置逻辑、之后执行后置逻辑;不调用 next() 即短路(后续中间件不执行)。

let logging: Middleware = {ctx, next =>
    // 前置
    let start = nowMs()
    next()                       // 进入内层
    // 后置
    println("${ctx.method} ${ctx.path} -> ${ctx.status} (${nowMs() - start}ms)")
}

compose 用嵌套递归实现洋葱,并对每个中间件的 next() 加单次调用守卫——重复调用 next() 抛 "next() called multiple times in one middleware"。

仓颉坑:闭包捕获并重新赋值 var 不可靠。需在闭包内累积状态时,用引用类型(ArrayList/Array)调方法,而非重新赋值被捕获的变量。compose 的守卫即用 Array<Bool>(1) 实现。

Context:请求/响应载体

Context 是框架无关的请求/响应容器。HTTP 适配层(ace-http)负责在 stdx 的请求与 Context 之间转换。

请求侧(只读):

public let method: String                 // GET/POST/...
public let path: String                   // 请求路径(不含 query)
public let headers: HashMap<String, String>  // 请求头(键已统一小写化)
public let state: HashMap<String, Any>    // 中间件间传递数据的容器

public func header(name: String): ?String       // 读请求头(传小写名)
public func param(name: String): ?String         // 读路径参数(路由中间件写入)
public func queryParam(name: String): ?String    // 读查询参数
public func rawBody(): ?String                    // 原始请求体

响应侧(可写):

public var status: UInt16                  // 响应状态码,默认 200
public var contentType: String             // 响应 Content-Type
public mut prop body: String               // 响应体文本视图(见下方 ResponseBody)

public func setHeader(name: String, value: String): Unit
public func json(text: String): Unit        // 设 body + Content-Type: application/json
public func bytes(contentType: String, data: Array<UInt8>): Unit  // 二进制响应
public func setStreamBody(producer): Unit   // 流式响应(SSE/大文件,见 06 章)

头键统一小写:入站头在适配层经 toLowerAscii 归一,所以 ctx.header("authorization") 能匹配 Authorization。

ResponseBody:统一响应体

响应体内部是一个单槽多态枚举(借鉴 Koa/Midway「body 即一个多态槽」),消除「同时设了文本和二进制、靠优先级裁决」的隐患:

public enum ResponseBody {
    | NoBody
    | TextBody(String)
    | BytesBody(Array<UInt8>)
    | StreamBody(((Array<UInt8>) -> Unit) -> Unit)
}

body/bodyBytes()/streamBody()/json()/bytes() 都是这一个槽的视图——设其中一个会替换其它。适配层对 ctx.responseBody() 一次 match 分流:文本/二进制一次性写回,流式则以 chunked 边产边发。日常开发用 body/json/bytes/setStreamBody 即可,无需直接碰枚举。

错误处理

中间件内抛异常 → App 顶层 onError 兜底。声明式应用默认接的是 dispatchException:

  1. 先试所有 @Catch 异常过滤器(按类型匹配,见 10 章);
  2. 没人处理则走 jsonErrorHandler:HttpError(code, msg) → 对应状态码 + {"error":"..."};其它异常 → 500 {"error":"Internal Server Error"}(不泄露细节)。

业务里抛 HttpError 即可产出干净的 HTTP 错误:

import aceboot::web.*

throw HttpError(404, "task not found")     // → 404 {"error":"task not found"}
throw HttpError(401, "invalid credentials") // → 401 {"error":"invalid credentials"}

不用宏的低层用法

内核可独立使用(无 stdx,可单测):

let app = App()
    .onError(defaultErrorHandler)
    .use({ctx, next => ctx.setHeader("X-Powered-By", "ACE"); next()})
    .use({ctx, _ => ctx.json("{\"ok\":true}")})

let ctx = Context.of("GET", "/")
app.handle(ctx)
// ctx.status == 200, ctx.body == {"ok":true}

声明式宏层(下一章起)本质就是「生成往 App 注册中间件、往 IoC 容器注册 Bean 的等价代码」。

下一章:IoC 与配置。