请求
每个请求进入洋葱后,ACE 会构造一个 Context 对象并沿中间件链传递。Controller 方法可以通过注解直接绑定所需数据,也可以通过 ctx 对象手动读取。
Context 基础属性
在中间件或未使用宏的路由处理函数中,可通过 ctx 直接访问:
cangjie
app.use { ctx, next =>
let method = ctx.method // "GET" / "POST" / ...
let path = ctx.path // "/api/tasks/42"
let query = ctx.query // "page=1&size=20"
let body = ctx.rawBody() // 原始请求体字节 Array<UInt8>
let token = ctx.header("x-token") ?? "" // 读请求头(不区分大小写)
let sid = ctx.cookie("session_id") ?? "" // 读 Cookie
await next(ctx)
}ctx.params 在路由匹配后自动填充路径参数:
cangjie
router.get("/tasks/:id") { ctx, next =>
let id = ctx.params["id"] ?? "unknown"
ctx.json("{\"id\":\"${id}\"}")
}参数绑定注解
在 @Controller 方法中,推荐使用注解自动绑定,ACE 在编译期生成提取和校验代码。
路径参数 @PathParam
cangjie
@Get["/:id"]
func get(@PathParam id: String): String {
// id 已从 ctx.params["id"] 提取
"task ${id}"
}查询参数 @Query
| 写法 | 含义 | 缺失时 |
|---|---|---|
@Query["page"] page: Int64 | 可选 | 返回 None(需 Option<Int64>) |
@Query["page!"] page: Int64 | 必填 | 自动返回 400 Bad Request |
cangjie
@Get[""]
func list(
@Query["page"] page: Option<Int64>,
@Query["size!"] size: Int64 // 缺失则 400
): String {
let p = page ?? 1
"page=${p}, size=${size}"
}表单参数 @Param
从 application/x-www-form-urlencoded 或 multipart/form-data 中提取字段:
cangjie
@Post["/login"]
func login(
@Param["username!"] username: String,
@Param["password!"] password: String
): String { ... }必填标记 !
在键名末尾加 !(如 "username!")表示必填。参数缺失时框架自动返回 400 Bad Request,无需手动校验。
Body 反序列化
接收原始 JSON 字符串后手动解析
cangjie
import ace_framework_runtime.* // parseJson / fieldStr / fieldInt64
@Post[""]
func create(@Body raw: String): String {
let json = parseJson(raw)
let title = fieldStr(json, "title") ?? ""
let priority = fieldInt64(json, "priority") ?? 0
// ... 业务逻辑
"{\"id\":\"new\"}"
}使用 @Dto 宏自动反序列化
定义 DTO struct 并标注 @Dto,ACE 在编译期生成 fromJson 实现:
cangjie
@Dto
struct CreateTaskDto {
let title: String
let priority: Int64
let tags: Array<String>
}
@Post[""]
func create(@Body dto: CreateTaskDto): String {
// dto 已从请求体自动解析,字段类型不匹配时返回 400
"created: ${dto.title}"
}Content-Type 要求
@Body 自动反序列化要求请求头 Content-Type: application/json。发送表单数据请使用 @Param。
文件上传
单文件 @File
cangjie
@Post["/upload"]
func upload(@File["avatar!"] avatar: UploadedFile): String {
let name = avatar.filename // 原始文件名
let bytes = avatar.bytes // Array<UInt8> 文件内容
let contentType = avatar.contentType // "image/png" 等
// 存储到本地或对象存储...
"{\"size\":${bytes.size}}"
}多文件 @Files
cangjie
@Post["/batch-upload"]
func batchUpload(@Files["docs"] docs: Array<UploadedFile>): String {
for doc in docs {
// 处理每个文件
println("received: ${doc.filename}")
}
"{\"count\":${docs.size}}"
}UploadedFile API
| 属性 | 类型 | 说明 |
|---|---|---|
filename | String | 客户端上传的原始文件名 |
bytes | Array<UInt8> | 文件二进制内容 |
contentType | String | MIME 类型,如 image/jpeg |
size | Int64 | 文件字节大小 |
内存限制
UploadedFile.bytes 将整个文件加载到内存。上传大文件场景请在应用层做大小校验(如 avatar.size > 10 * 1024 * 1024 时返回 413),或等待后续流式上传 API。
Multipart 表单混合数据
同一请求中同时包含文本字段和文件时,文本字段仍用 @Param 绑定:
cangjie
@Post["/articles"]
func createArticle(
@Param["title!"] title: String,
@Param["content!"] content: String,
@File["cover"] cover: Option<UploadedFile>
): String {
let hasCover = cover.isSome()
"title=${title}, hasCover=${hasCover}"
}直接读取 ctx
在不使用宏的场景(如自定义中间件),可从 ctx 手动提取所有信息:
cangjie
// 读取所有查询参数
let queryStr = ctx.query // "foo=1&bar=2"
// 读取请求头
let accept = ctx.header("accept") ?? "*/*"
// 读取 Cookie
let lang = ctx.cookie("lang") ?? "zh"
// 读取原始 Body(bytes)
let rawBytes = ctx.rawBody()
let bodyStr = String.fromUtf8(rawBytes)rawBody 只能读一次
ctx.rawBody() 返回完整请求体字节,ace-bodyparser 中间件执行后会将解析结果缓存到 ctx.state。若手动读取 rawBody,请在 bodyparser 中间件之前调用,避免重复消费。