上下文 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 安全传递数据,无需全局变量。
字段列表
| 字段 | 类型 | 说明 |
|---|---|---|
method | String | HTTP 方法(GET/POST 等,大写) |
path | String | 请求路径(不含 query string),如 /users/42 |
params | HashMap<String, String> | 路由参数,如 :id → "42" |
query | HashMap<String, String> | URL 查询参数,如 ?page=2 → "page"→"2" |
headers | HashMap<String, String> | 请求头(键已转小写) |
state | HashMap<String, Any> | 中间件间共享数据的标准通道 |
status | UInt16 | 响应状态码(默认 200u16) |
body | String | 响应体文本 |
rawBody | () -> String | 惰性读取请求原始体的函数 |
responseHeaders | HashMap<String, String> | 待写回的响应头 |
TIP
headers 的键在框架层统一转为小写(如 Content-Type → content-type),读取时务必使用小写键。
ctx.state —— 中间件数据通道
ctx.state 是 HashMap<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 | 框架内部使用,请求结束后清理 |