Skip to content

中间件

ACE Framework 的中间件采用洋葱模型,每个中间件包裹下一层,请求由外向内穿透,响应由内向外回程。这与 Koa.js 的设计完全一致,却以仓颉的静态类型和编译期宏实现。

洋葱模型

请求进入


┌─────────────────────────────┐  order=1(最外层)
│  Logger                     │
│  ┌───────────────────────┐  │
│  │  CORS                 │  │  order=10
│  │  ┌─────────────────┐  │  │
│  │  │  JWT Auth        │  │  │  order=20
│  │  │  ┌───────────┐  │  │  │
│  │  │  │  Router   │  │  │  │  order=99(最内层)
│  │  │  └───────────┘  │  │  │
│  │  │  next() 返回 ↑  │  │  │
│  │  └─────────────────┘  │  │
│  └───────────────────────┘  │
└─────────────────────────────┘


响应回程(逆序执行 next() 之后的代码)

每个中间件调用 next() 将控制权交给内层,next() 返回后继续执行回程逻辑(如追加响应头、记录耗时)。

@Middleware 宏

@Middleware[order] 装饰一个无参数、返回 Middleware 的函数,容器自动收集并按 order 从小到大(最外层)排列:

cangjie
package com.example.middleware

import ace.framework.*
import ace.web.*

@Middleware[1]
func requestLogger(): Middleware {
    return { ctx, next in
        let start = currentTimeMillis()
        await next()
        let elapsed = currentTimeMillis() - start
        println("[${ctx.method}] ${ctx.path} ${ctx.status} ${elapsed}ms")
    }
}

@Middleware 参数说明

参数类型说明
orderInt64执行顺序,数值越小越靠外层;默认值为 100

order 约定

建议内置基础设施类中间件使用 120,业务鉴权类使用 2050,业务逻辑类使用 5090,路由层保持 99 以上。

内置中间件

ACE 提供一组开箱即用的中间件函数,通过 App().use(...) 手动挂载或配合 @Middleware 宏自动注册。

bodyParser()

解析 application/x-www-form-urlencodedapplication/json 请求体,结果写入 ctx.body

cangjie
app.use(bodyParser())

gzip()

对响应体自动 gzip 压缩(当客户端 Accept-Encoding 含 gzip 时生效):

cangjie
app.use(gzip())

timeoutHandler(ms)

超过指定毫秒未完成则返回 503 Service Unavailable

cangjie
app.use(timeoutHandler(5000))   // 5 秒超时

jwtAuth(secret)

验证 Authorization: Bearer <token>,校验通过后将 payload 写入 ctx.state["jwt"]

cangjie
app.use(jwtAuth("my-secret-key"))

// 在 Handler 中读取
@Get["/profile"]
func profile(@Inject ctx: Context): String {
    let payload = ctx.state["jwt"] as JwtPayload
    return payload.sub
}

serveStatic(dir, prefix)

挂载本地静态文件目录:

cangjie
app.use(serveStatic("./public", "/static"))
// 访问 /static/logo.png → 读取 ./public/logo.png

cors()

添加跨域响应头,支持预检请求(OPTIONS):

cangjie
app.use(cors())                              // 允许所有来源
app.use(cors(origins: ["https://example.com"]))  // 白名单

自定义中间件示例

以下示例展示一个完整的请求日志中间件,记录方法、路径、状态码和耗时:

cangjie
package com.example.middleware

import ace.framework.*
import ace.web.*
import std.time.*

@Middleware[5]
func accessLog(): Middleware {
    return { ctx, next in
        let start = DateTime.now()
        var err: ?Exception = None

        try {
            await next()
        } catch (e: Exception) {
            err = e
        }

        let ms = (DateTime.now() - start).toMilliseconds()
        let level = if (ctx.status >= 500) { "ERROR" } else { "INFO" }
        println("[${level}] ${ctx.method} ${ctx.path} ${ctx.status} ${ms}ms")

        if (let e <- err) {
            throw e   // 重新抛出,交由 @Catch 处理
        }
    }
}

手动注册(无宏时)

不使用 @Middleware 宏时,可直接调用 App.use()

cangjie
import ace.web.*
import ace.http.*

func main() {
    let app = App()
    app.use(timeoutHandler(3000))
    app.use(cors())
    app.use(bodyParser())
    app.use(router.routes())
    listen(app, "0.0.0.0", 8080)
}

手动注册时中间件的执行顺序与 use() 调用顺序一致(先 use 的在最外层)。

中间件 vs @Catch 过滤器

中间件和 @Catch 过滤器都能处理异常,但职责不同:

特性中间件@Catch 过滤器
注册位置@Middleware[order]app.use()@Catch[ExceptionType] 标注在类上
作用范围全局或路由组按异常类型精确匹配
执行时机请求全程(前后都可介入)仅当指定异常被抛出时
适合场景日志、鉴权、超时、CORS业务异常映射为 HTTP 响应
cangjie
// @Catch 示例:将 NotFoundException 映射为 404
@Catch[NotFoundException]
class NotFoundFilter <: ExceptionFilter<NotFoundException> {
    func catch(e: NotFoundException, ctx: Context): Unit {
        ctx.status = 404
        ctx.json({ "error": e.message })
    }
}

中间件中的异常处理

中间件捕获异常后若不重新抛出,@Catch 过滤器将不会触发。建议日志中间件仅记录后重新 throw,让过滤器负责最终的响应格式。

基于 Apache-2.0 许可证发布