路由与控制器
本章介绍 Neton 的路由系统,包括 Controller 注解路由、HTTP 方法注解、路由组与挂载、以及 DSL 路由。
基本概念
Neton 提供两种方式定义路由:
- 注解路由(推荐) -- 使用
@Controller+ HTTP 方法注解,配合 KSP 在编译期自动生成路由代码 - DSL 路由 -- 在
routing { }块中使用 DSL 语法手动注册路由,适合简单场景
注解路由
基础控制器
使用 @Controller 注解定义控制器类,指定基础路径。使用 @Get、@Post 等注解标记处理方法:
@Controller("/simple")
class SimpleController {
@Get("/hello")
fun hello(): String {
return "Hello from SimpleController!"
}
@Get("/user/{id}")
fun getUser(@PathVariable("id") userId: Int): String {
return "User ID: $userId"
}
@Post("/user")
fun createUser(@Body user: CreateUserRequest): String {
return "Created: ${user.name}, ${user.email}"
}
}在上面的示例中:
@Controller("/simple")定义了控制器的基础路径为/simple@Get("/hello")注册GET /simple/hello路由@Get("/user/{id}")注册带路径参数的路由,{id}是路径占位符@Post("/user")注册POST /simple/user路由,@Body表示从请求体反序列化参数
请求体数据类需要添加 @Serializable 注解:
@Serializable
data class CreateUserRequest(
val name: String,
val email: String,
val age: Int? = null
)多路径参数
控制器方法可以接收多个路径参数:
@Controller("/simple")
class SimpleController {
@Get("/user/{userId}/post/{postId}")
fun getUserPost(
@PathVariable("userId") userId: Int,
@PathVariable("postId") postId: Int
): String {
return "用户 $userId 的帖子 $postId"
}
}上下文对象注入
控制器方法可以直接注入框架提供的上下文对象,无需注解:
@Controller("/simple")
class SimpleController {
@Get("/request-info")
fun getRequestInfo(request: HttpRequest): String {
return "请求方法: ${request.method}, 路径: ${request.path}"
}
}支持自动注入的类型包括 HttpContext、HttpRequest、HttpResponse。
认证用户注入
使用 @CurrentUser 注解或直接声明 Identity 类型参数注入当前认证用户信息:
@Controller("/simple")
class SimpleController {
@Get("/profile")
@RequireAuth
fun getProfile(@CurrentUser identity: Identity): String {
return "当前用户: ${identity.id}, 角色: ${identity.roles}"
}
@Get("/visitor")
@AllowAnonymous
fun visitor(identity: Identity?): String {
// Identity 类型自动注入,无需 @CurrentUser
return identity?.id ?: "未认证用户"
}
}HTTP 方法注解
Neton 支持所有标准 HTTP 方法,每种方法对应一个注解:
| 注解 | HTTP 方法 | 典型用途 |
|---|---|---|
@Get | GET | 查询资源 |
@Post | POST | 创建资源 |
@Put | PUT | 完整更新资源 |
@Patch | PATCH | 部分更新资源 |
@Delete | DELETE | 删除资源 |
@Head | HEAD | 获取资源元信息(不返回实体) |
@Options | OPTIONS | 获取资源支持的方法 |
以下是一个完整的 RESTful API 控制器示例:
@Controller("/api/products")
class HttpMethodController {
@Get("/")
fun getProducts(): String {
return "GET /api/products - 获取所有产品列表"
}
@Get("/{id}")
fun getProduct(): String {
return "GET /api/products/{id} - 获取指定产品详情"
}
@Post("/")
fun createProduct(): String {
return "POST /api/products - 创建新产品"
}
@Put("/{id}")
fun updateProduct(): String {
return "PUT /api/products/{id} - 完整更新产品信息"
}
@Patch("/{id}")
fun patchProduct(): String {
return "PATCH /api/products/{id} - 部分更新产品信息"
}
@Delete("/{id}")
fun deleteProduct(): String {
return "DELETE /api/products/{id} - 删除指定产品"
}
@Head("/{id}")
fun headProduct(): String {
return "HEAD /api/products/{id} - 获取产品元信息"
}
@Options("/")
fun optionsProducts(): String {
return "OPTIONS /api/products - 支持的方法: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS"
}
}文件上传路由
控制器参数使用 UploadFile、List<UploadFile> 或 UploadFiles 类型即可接收上传文件。KSP 按参数名匹配表单 fieldName 自动绑定:
@Controller("/api/files")
class FileController {
// 参数名 "avatar" 匹配表单 fieldName "avatar"
@Post("/avatar")
suspend fun upload(avatar: UploadFile): Map<String, Any> {
return mapOf("filename" to avatar.filename, "size" to avatar.size)
}
// 参数名 "photos" 匹配表单 fieldName "photos"
@Post("/batch")
suspend fun batchUpload(photos: List<UploadFile>): Map<String, Any> {
return mapOf("count" to photos.size)
}
// UploadFiles 注入完整结构化视图,可按 fieldName 查询
@Post("/mixed")
suspend fun mixedUpload(files: UploadFiles): Map<String, Any> {
val avatar = files.require("avatar")
val gallery = files.get("gallery")
return mapOf("avatar" to avatar.filename, "galleryCount" to gallery.size)
}
}KSP 会自动识别 UploadFile / List<UploadFile> / UploadFiles 参数类型,从 multipart/form-data 请求中解析文件。
路由组与挂载
路由组用于将控制器按照业务模块进行分组,并为每组分配 URL 前缀。
配置路由组
在 config/routing.conf 中定义路由组:
[[groups]]
group = "admin"
mount = "/admin"
requireAuth = true
allowAnonymous = ["/login", "/register"]
[[groups]]
group = "app"
mount = "/app"| 字段 | 类型 | 说明 |
|---|---|---|
group | String | 路由组标识 |
mount | String | URL 前缀 |
requireAuth | Boolean | 该组是否默认要求认证(默认 false) |
allowAnonymous | List<String> | 白名单路径,即使 requireAuth=true 也允许匿名(默认空) |
目录约定
路由组与控制器的包路径存在对应关系。KSP 根据控制器所在的包名自动识别其所属路由组:
controller/
├── HomeController.kt # 默认组,路由: /
├── SimpleController.kt # 默认组,路由: /simple/...
├── admin/
│ ├── IndexController.kt # admin 组,路由: /admin/index/...
│ └── AdminHomeController.kt # admin 组,路由: /admin/...
└── app/
└── IndexController.kt # app 组,路由: /app/index/...例如,以下控制器位于 controller.admin 包下:
package controller.admin
@Controller("/index")
@RequireAuth
class AdminIndexController {
@Get("")
suspend fun index(): String = "admin ok"
@Get("/public")
@AllowAnonymous
suspend fun public(): String = "admin public (allow anonymous)"
@Get("/dashboard")
suspend fun dashboard(): String = "admin dashboard"
}由于 admin 组的 mount 配置为 /admin,该控制器的最终路由为:
GET /admin/index-- 需要认证GET /admin/index/public-- 允许匿名访问GET /admin/index/dashboard-- 需要认证
安全注解
路由组中的控制器可以使用安全注解控制访问权限:
@RequireAuth-- 标记在类或方法上,要求认证@AllowAnonymous-- 标记在类或方法上,覆盖认证要求,允许匿名访问(优先级最高)@Permission("system:user:edit")-- 标记在类或方法上,要求指定权限@CurrentUser-- 标记在方法参数上,注入当前 Identity(Identity 类型参数可省略此注解)
模块化路由组
对于更复杂的项目,可以按业务模块组织控制器:
module/
└── payment/
└── controller/
├── IndexController.kt # 默认组: /payment/index
└── admin/
└── IndexController.kt # admin 组: /admin/payment/indexDSL 路由
对于不使用 KSP 的简单项目,可以在 routing { } 块中使用 DSL 语法直接定义路由:
fun main(args: Array<String>) {
Neton.run(args) {
http {
port = 8080
}
routing {
get("/") {
"Hello Neton!"
}
get("/users") {
"用户列表"
}
post("/api/data") { ctx ->
// 通过 ctx 访问请求上下文
"数据已创建"
}
group("admin") {
get("/dashboard") { ctx ->
"管理后台"
}
}
}
}
}DSL 路由支持以下方法:
| 方法 | 说明 |
|---|---|
get(path) { ... } | 注册 GET 路由 |
post(path) { ... } | 注册 POST 路由 |
put(path) { ... } | 注册 PUT 路由 |
delete(path) { ... } | 注册 DELETE 路由 |
patch(path) { ... } | 注册 PATCH 路由 |
group(name) { ... } | 定义路由组 |
DSL 与注解的选择
- DSL 路由:适合快速原型、简单微服务、无需 KSP 的场景
- 注解路由:适合大型项目,控制器结构更清晰,支持参数自动绑定、安全注解等高级功能
两种方式可以在同一项目中混合使用。
协程支持
控制器方法支持 suspend 修饰符,可以在方法内调用挂起函数:
@Controller("/api")
class AsyncController {
@Get("/data")
suspend fun fetchData(): String {
// 可以调用挂起函数
val result = someAsyncOperation()
return result
}
}