Skip to content

路由

ACE 的路由系统分两个层次:底层 ace-router 提供 Router 对象与洋葱中间件集成;上层 ace-framework 宏层通过 @Controller@Get@Post 等注解在编译期将声明自动转换为 registerRoute 调用,无需运行时扫描。

路由注册原理

当编译器处理 @Controller/@Get 等宏时,会在当前编译单元的顶层生成一段等价的 let 初始化代码:

cangjie
// 你写的
@Controller["/tasks"]
class TaskController {
    @Get["/:id"]
    func getTask(@PathParam id: String): String { ... }
}

// 宏展开后(等价于)
let __ace_reg_TaskController_getTask = Registry.register(
    method: "GET",
    path: "/tasks/:id",
    handler: { ctx => TaskController().getTask(ctx) }
)

这段初始化代码在程序启动时自动执行完成注册,全程无反射、无枚举类列表

可审计展开结果

使用 cjc --debug-macro 可查看宏展开后的实际代码,方便排查注册行为。

路径参数语法

ACE 路由基于 path_to_regex 规则(已内置于 ace-router),支持以下语法:

语法含义示例匹配
:name必选具名参数/users/:id/users/42
:name?可选具名参数/files/:name?/files/files/a.txt
:name+一段或多段/path/:rest+/path/a/b/c
:name*零段或多段/path/:rest*/path/path/a/b
:name([0-9]+)带内联正则/items/:id([0-9]+)/items/123,拒绝 /items/abc
*wildcard匿名通配符/static/*file/static/css/main.css

正则语法限制

仓颉 std.regex 使用 POSIX 语法,不支持 \d\w 等转义类。内联正则请写 [0-9] 而非 \d

代码示例:

cangjie
@Controller["/files"]
class FileController {
    // 匹配 /files/123(仅数字)
    @Get["/:id([0-9]+)"]
    func getById(@PathParam id: String): String { ... }

    // 匹配 /files/docs/2024/report.pdf
    @Get["/*path"]
    func getByPath(@PathParam path: String): String { ... }
}

路由分组

使用 @Controller 同前缀分组

同一 @Controller 下的所有方法自动继承前缀:

cangjie
@Controller["/api/tasks"]
class TaskController {
    @Get[""]        // GET /api/tasks
    func list(): String { ... }

    @Get["/:id"]    // GET /api/tasks/:id
    func get(@PathParam id: String): String { ... }

    @Post[""]       // POST /api/tasks
    func create(@Body body: String): String { ... }

    @Delete["/:id"] // DELETE /api/tasks/:id
    func remove(@PathParam id: String): String { ... }
}

使用 Router 对象嵌套分组

需要更灵活的分组(如动态注册、子模块隔离)时,可直接操作 Router

cangjie
import ace_router.*
import ace_web.*

let apiRouter = Router()
let v1Router = Router()

v1Router.get("/users") { ctx =>
    ctx.json("{\"users\":[]}")
}

// 挂载 v1Router 到 /v1,再挂载到 apiRouter 的 /api
apiRouter.addRouter("/v1", v1Router)

let app = App()
app.use(apiRouter.routes())

挂载后访问路径为 /api/v1/users

分组中间件

@Controller 级别的中间件会在所有方法前执行:

cangjie
@Middleware(order: 1)
class AuthMiddleware <: Middleware {
    func handle(ctx: Context, next: Next): Unit { ... }
}

@Controller["/admin"]
@UseMiddleware[AuthMiddleware]
class AdminController {
    @Get["/dashboard"]
    func dashboard(): String { ... }  // 自动经过 AuthMiddleware
}

路由匹配优先级

当多条路由都能匹配同一请求时,ACE 按以下顺序决定优先级(由高到低):

  1. 精确匹配/users/me 优先于 /users/:id
  2. 参数路由/users/:id 优先于通配符
  3. 通配符*path 最后匹配
cangjie
@Controller["/users"]
class UserController {
    @Get["/me"]       // 优先级 1 —— 精确
    func me(): String { ... }

    @Get["/:id"]      // 优先级 2 —— 参数
    func get(@PathParam id: String): String { ... }

    @Get["/*rest"]    // 优先级 3 —— 通配符
    func wildcard(@PathParam rest: String): String { ... }
}

路由冲突检测

启动时 ACE Runtime 会对已注册路由做冲突检测,以下情况将抛出 RouteConflictException

  • 相同 HTTP Method + 相同路径模式重复注册
  • 两条参数路由在同一位置定义了不同参数名但结构相同(如 /:id/:name

冲突将导致启动失败

路由冲突在服务启动阶段即报错,不会静默覆盖,请检查是否有多个 @Controller 注册了相同路径。

bash
# 启动时错误示例
[ACE] RouteConflictException: duplicate route GET /tasks/:id
  registered at TaskController.get
  conflict with TaskV2Controller.getTask

基于 Apache-2.0 许可证发布