Skip to content

上下文 Context

Context 是 ACE 请求生命周期的核心载体。每次 HTTP 请求到达时,AceDistributor 构造一个新的 Context 实例,传递给洋葱中间件链,直到响应写回后销毁。

生命周期

HTTP 请求
  └─ AceDistributor.handle(req, resp)
       └─ 构造 Context(method, path, headers, rawBody, ...)
            └─ App.handle(ctx)          ← 洋葱入口
                 ├─ 中间件 1 (before)
                 ├─ 中间件 2 (before)
                 │    └─ 路由匹配 → Controller 方法
                 ├─ 中间件 2 (after)
                 └─ 中间件 1 (after)
       └─ 将 ctx.status / ctx.body 写回 HTTP 响应

每个请求独享一个 Context 实例,中间件之间通过 ctx.state 安全传递数据,无需全局变量。

字段列表

字段类型说明
methodStringHTTP 方法(GET/POST 等,大写)
pathString请求路径(不含 query string),如 /users/42
paramsHashMap<String, String>路由参数,如 :id"42"
queryHashMap<String, String>URL 查询参数,如 ?page=2"page"→"2"
headersHashMap<String, String>请求头(键已转小写)
stateHashMap<String, Any>中间件间共享数据的标准通道
statusUInt16响应状态码(默认 200u16
bodyString响应体文本
rawBody() -> String惰性读取请求原始体的函数
responseHeadersHashMap<String, String>待写回的响应头

TIP

headers 的键在框架层统一转为小写(如 Content-Typecontent-type),读取时务必使用小写键。

ctx.state —— 中间件数据通道

ctx.stateHashMap<String, Any> 类型,用于在洋葱各层之间传递计算结果,是替代全局变量的标准方式。

cangjie
// 认证中间件:将当前用户写入 state
let authMiddleware: Middleware = { ctx, next in
    let token = ctx.headers.getOrDefault("authorization", "")
    let user = verifyToken(token)   // 返回 Option<User>
    match user {
        case Some(u) => ctx.state["currentUser"] = u
        case None    => ctx.status = 401u16; return
    }
    await next()
}

// 业务中间件:从 state 读取用户
let profileMiddleware: Middleware = { ctx, next in
    let user = ctx.state["currentUser"] as User
    ctx.body = user.name
}

WARNING

state 取值后需通过 as 进行类型转换,转换失败会 panic。建议在写入时保持类型一致,并在读取侧做 Option 判断。

测试构造

不依赖 HTTP 服务即可在单测中构造 Context

cangjie
import std.unittest.*
import std.unittest.testmacro.*
import ace_web.Context

@Test
class ResponseTest {
    @TestCase
    func testJsonResponse(): Unit {
        let ctx = Context.of("GET", "/users/1")
        ctx.params["id"] = "1"

        // 直接调用中间件或控制器逻辑
        myHandler(ctx)

        @Expect(ctx.status, 200u16)
        @Expect(ctx.body.contains("\"id\":1"), true)
    }

    @TestCase
    func testNotFound(): Unit {
        let ctx = Context.of("GET", "/missing")
        notFoundHandler(ctx)
        @Expect(ctx.status, 404u16)
    }
}

Context.of 签名:

cangjie
static func of(method: String, path: String): Context
static func of(method: String, path: String, body: String): Context

@Inject 活属性注入

@Service@Controller 中,可通过 @Inject 注入 Context。注入的是活属性(live property),每次访问时动态返回当前请求的 Context,而非在构造时固化引用:

cangjie
@Service
class UserService {
    @Inject
    var ctx: Context   // 每次访问均返回当前协程绑定的 Context

    func currentUserId(): Int64 {
        let id = ctx.params.getOrDefault("id", "0")
        return Int64.parse(id) ?? 0
    }
}

DANGER

不要将 ctx 赋值给实例字段后缓存使用。@Service 默认为单例,若将某次请求的 Context 保存到成员变量,后续请求将读到脏数据。活属性注入正是为了杜绝此类错误。

RequestContextHolder —— Service 层读取当前请求

@Inject 不方便使用时(如工具类、拦截器),可通过 RequestContextHolder 读取协程本地绑定的 Context

cangjie
import ace_framework.runtime.RequestContextHolder

class AuditLogger {
    static func log(action: String): Unit {
        let ctx = RequestContextHolder.current()  // 返回 Option<Context>
        match ctx {
            case Some(c) =>
                let ip = c.headers.getOrDefault("x-forwarded-for", "unknown")
                println("[AUDIT] ${action} from ${ip} ${c.path}")
            case None =>
                println("[AUDIT] ${action} (no request context)")
        }
    }
}

RequestContextHolder 基于仓颉协程本地存储实现,每个并发请求的协程持有独立的 Context 引用,天然线程安全。

方法返回值说明
RequestContextHolder.current()Option<Context>获取当前协程绑定的 Context
RequestContextHolder.bind(ctx)Unit框架内部使用,绑定 Context 到当前协程
RequestContextHolder.clear()Unit框架内部使用,请求结束后清理

基于 Apache-2.0 许可证发布