{"id":51158999,"url":"https://github.com/cymoo/colleen","last_synced_at":"2026-06-26T12:30:32.788Z","repository":{"id":338288623,"uuid":"1157257248","full_name":"cymoo/colleen","owner":"cymoo","description":"A lightweight web framework for Kotlin and Java","archived":false,"fork":false,"pushed_at":"2026-04-20T16:27:51.000Z","size":835,"stargazers_count":4,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-20T18:36:38.264Z","etag":null,"topics":["java","kotlin","microservice","openapi","spring-alternative","web-framework","websocket"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cymoo.png","metadata":{"files":{"readme":"README-zh.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-13T16:03:47.000Z","updated_at":"2026-04-20T16:27:55.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/cymoo/colleen","commit_stats":null,"previous_names":["cymoo/colleen"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/cymoo/colleen","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cymoo%2Fcolleen","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cymoo%2Fcolleen/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cymoo%2Fcolleen/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cymoo%2Fcolleen/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cymoo","download_url":"https://codeload.github.com/cymoo/colleen/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cymoo%2Fcolleen/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34817640,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-26T02:00:06.560Z","response_time":106,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["java","kotlin","microservice","openapi","spring-alternative","web-framework","websocket"],"created_at":"2026-06-26T12:30:31.568Z","updated_at":"2026-06-26T12:30:32.764Z","avatar_url":"https://github.com/cymoo.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Colleen Web 框架\n\n[English Documentation](README.md)\n\nColleen 是一个基于 Undertow 构建的轻量级、类型安全 Kotlin / Java Web 框架。\n它强调显式应用代码、可组合中间件、自动请求参数提取，以及 Java 21+ 上的同步 handler 编程模型。\n\n```kotlin\nfun getTodo(id: Path\u003cInt\u003e, service: TodoService): Todo =\n    service.find(id.value) ?: throw NotFound(\"Todo not found\")\n\nfun createTodo(body: Json\u003cCreateTodo\u003e, service: TodoService): Result\u003cTodo\u003e =\n    Result.created(service.create(body.value.title))\n\napp.get(\"/todos/{id}\", ::getTodo)\napp.post(\"/todos\", ::createTodo)\n```\n\n## 为什么选择 Colleen？\n\n- **类型安全的函数式 handler**：在函数签名中直接声明 `Path\u003cInt\u003e`、`Query\u003cString?\u003e`、\n  `Json\u003cCreateTodo\u003e` 或服务依赖。\n- **默认显式**：没有 classpath scanning，没有隐藏的自动装配，也没有全局魔法。\n- **可组合中间件**：类似 Koa 的洋葱模型，并保证下游异常时上游 after 逻辑仍会执行。\n- **内置 OpenAPI**：函数式 handler 和 controller 可以自动生成有用的 OpenAPI 元数据，\n  默认在 `/docs` 提供 Swagger UI。\n- **实时能力**：WebSocket 和 SSE 是一等 API，不需要额外框架拼接。\n- **Kotlin 优先，Java 友好**：Kotlin API 简洁，同时提供显式的 Java 兼容写法。\n\n当你希望框架帮你处理路由、绑定、中间件、文档、测试和实时端点，但又希望应用结构\n清楚地留在代码里时，Colleen 会很合适。\n\n## 目录\n\n1. [快速开始](#快速开始)\n2. [一个小型 Todo API](#一个小型-todo-api)\n3. [核心概念](#核心概念)\n4. [API 参考](#api-参考)\n5. [WebSocket 与 SSE](#websocket-与-sse)\n6. [OpenAPI](#openapi)\n7. [测试](#测试)\n8. [Java 支持](#java-支持)\n9. [配置](#配置)\n10. [生产建议](#生产建议)\n11. [示例](#示例)\n\n---\n\n## 快速开始\n\n### 要求\n\n- Java 21 或更高版本\n- Kotlin 或 Java\n- Maven 或 Gradle\n\n### 安装\n\n**Maven**\n\n```xml\n\u003cdependency\u003e\n    \u003cgroupId\u003eio.github.cymoo\u003c/groupId\u003e\n    \u003cartifactId\u003ecolleen\u003c/artifactId\u003e\n    \u003cversion\u003e0.4.7\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\n**Gradle (Kotlin DSL)**\n\n```kotlin\nimplementation(\"io.github.cymoo:colleen:0.4.7\")\n```\n\n### Hello World\n\n```kotlin\nimport io.github.cymoo.colleen.*\n\nfun main() {\n    val app = Colleen()\n\n    app.get(\"/\") { \"hello world\" }\n\n    app.listen(8000)\n}\n```\n\n打开 \u003chttp://localhost:8000\u003e。\n\n---\n\n## 一个小型 Todo API\n\n这个示例刻意保持短小，但覆盖了 Colleen 的主要工作流：函数式 handler、类型安全参数\n提取、服务注入、结构化响应、HTTP 错误和 OpenAPI。\n\n```kotlin\nimport io.github.cymoo.colleen.*\n\ndata class CreateTodo(val title: String)\ndata class Todo(val id: Int, val title: String, val completed: Boolean = false)\n\nclass TodoService {\n    private val todos = linkedMapOf(\n        1 to Todo(1, \"Try Colleen\"),\n        2 to Todo(2, \"Open /docs\"),\n    )\n    private var nextId = 3\n\n    fun list(completed: Boolean?): List\u003cTodo\u003e =\n        todos.values.filter { completed == null || it.completed == completed }\n\n    fun find(id: Int): Todo? = todos[id]\n\n    fun create(title: String): Todo {\n        val todo = Todo(nextId++, title)\n        todos[todo.id] = todo\n        return todo\n    }\n\n    fun complete(id: Int): Todo? {\n        val todo = todos[id] ?: return null\n        val updated = todo.copy(completed = true)\n        todos[id] = updated\n        return updated\n    }\n\n    fun delete(id: Int): Boolean = todos.remove(id) != null\n}\n\nfun listTodos(completed: Query\u003cBoolean?\u003e, service: TodoService): List\u003cTodo\u003e =\n    service.list(completed.value)\n\nfun getTodo(id: Path\u003cInt\u003e, service: TodoService): Todo =\n    service.find(id.value) ?: throw NotFound(\"Todo not found\")\n\nfun createTodo(body: Json\u003cCreateTodo\u003e, service: TodoService): Result\u003cTodo\u003e =\n    Result.created(service.create(body.value.title))\n\nfun completeTodo(id: Path\u003cInt\u003e, service: TodoService): Todo =\n    service.complete(id.value) ?: throw NotFound(\"Todo not found\")\n\nfun deleteTodo(id: Path\u003cInt\u003e, service: TodoService) {\n    if (!service.delete(id.value)) throw NotFound(\"Todo not found\")\n}\n\nfun main() {\n    val app = Colleen()\n\n    app.provide(TodoService())\n\n    app.openApi(\n        title = \"Todo API\",\n        version = \"1.0.0\",\n        description = \"A small Colleen API\"\n    )\n\n    app.get(\"/todos\", ::listTodos)\n    app.get(\"/todos/{id}\", ::getTodo)\n    app.post(\"/todos\", ::createTodo)\n    app.post(\"/todos/{id}/complete\", ::completeTodo)\n    app.delete(\"/todos/{id}\", ::deleteTodo)\n\n    app.listen(8000)\n}\n```\n\n试一下：\n\n```shell\ncurl http://localhost:8000/todos\ncurl http://localhost:8000/todos/1\ncurl -X POST http://localhost:8000/todos \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"title\":\"Ship docs\"}'\n```\n\nOpenAPI JSON 位于 `/openapi.json`；Swagger UI 位于 `/docs`。\n\n---\n\n## 核心概念\n\n### Handler 风格\n\nColleen 支持三种 handler 风格。根据路由复杂度选择最简单合适的一种。\n\n| 风格 | 适合场景 | 示例 |\n|---|---|---|\n| Lambda | 极小的内联路由 | `app.get(\"/\") { \"ok\" }` |\n| Function-style | 大多数应用 handler | `app.get(\"/todos/{id}\", ::getTodo)` |\n| Controller | 更大的分组 API | `app.addController(TodoController(service))` |\n\n对于非平凡路由，推荐默认使用 function-style handler：它是普通函数，便于测试，也能向\n参数绑定和 OpenAPI 暴露更完整的类型信息。\n\n### 路由\n\n```kotlin\napp.get(\"/todos\") { }\napp.post(\"/todos\") { }\napp.put(\"/todos/{id}\") { }\napp.delete(\"/todos/{id}\") { }\napp.patch(\"/todos/{id}\") { }\napp.head(\"/todos/{id}\") { }\napp.options(\"/todos\") { }\napp.all(\"/health\") { }\n```\n\n路径参数使用 `{name}`，通配符使用 `{path...}`。\n\n```kotlin\napp.get(\"/todos/{id}\", ::getTodo)\napp.get(\"/files/{path...}\") { ctx -\u003e ctx.pathParam(\"path\") }\napp.get(\"/images/{name}.{ext}\") { ctx -\u003e \"${ctx.pathParam(\"name\")}.${ctx.pathParam(\"ext\")}\" }\n```\n\n在复合路径段中，参数会捕获到下一个静态分隔符首次出现的位置。例如\n`/files/{name}-{version}.txt` 匹配 `/files/foo-bar-1.txt` 时，\n`name = \"foo\"`，`version = \"bar-1\"`。\n\n路由匹配优先级是确定的：静态段、复合段、参数段、通配符段。\n\n### 路由组\n\n路由组会在注册路由时添加统一路径前缀，也可以为一组相关路由附加中间件。\n\n```kotlin\napp.group(\"/api\") {\n    use(ApiKeyMiddleware())\n\n    get(\"/todos\", ::listTodos)          // GET /api/todos\n    post(\"/todos\", ::createTodo)        // POST /api/todos\n\n    group(\"/admin\") {\n        use(AdminOnly())\n        get(\"/stats\") { statsService.snapshot() }  // GET /api/admin/stats\n    }\n}\n```\n\n路由组用于组织路由声明。前缀中间件不同：它注册一次，并在运行时匹配该前缀下的请求。\n\n### Controller 风格路由\n\n```kotlin\n@Controller(\"/todos\")\nclass TodoController(private val service: TodoService) {\n    @Get\n    fun list(completed: Query\u003cBoolean?\u003e): List\u003cTodo\u003e =\n        service.list(completed.value)\n\n    @Get(\"/{id}\")\n    fun get(id: Path\u003cInt\u003e): Todo =\n        service.find(id.value) ?: throw NotFound(\"Todo not found\")\n\n    @Post\n    fun create(body: Json\u003cCreateTodo\u003e): Result\u003cTodo\u003e =\n        Result.created(service.create(body.value.title))\n}\n\napp.addController(TodoController(TodoService()))\n```\n\n支持的 HTTP 注解包括 `@Get`、`@Post`、`@Put`、`@Delete` 和 `@Patch`。\n当需要覆盖逻辑参数名时使用 `@Param(\"name\")`，Java 中尤其常用。\n\n### 中间件\n\n中间件签名是 `(Context, Next) -\u003e Unit`。\n\n```kotlin\nval logger = Middleware { ctx, next -\u003e\n    val start = System.currentTimeMillis()\n    println(\"-\u003e ${ctx.method} ${ctx.path}\")\n\n    next() // 下游异常会被捕获到 ctx.error\n\n    val status = if (ctx.error != null) {\n        (ctx.error?.cause as? HttpException)?.status ?: 500\n    } else {\n        ctx.response.status\n    }\n    println(\"\u003c- $status in ${System.currentTimeMillis() - start}ms\")\n}\n\napp.use(logger)\n```\n\nColleen 使用洋葱模型：\n\n```text\nmiddleware1 before\nmiddleware2 before\nhandler\nmiddleware2 after\nmiddleware1 after\n```\n\n只要某个中间件的 before 部分开始执行，它的 after 部分就会执行，即使下游抛出异常。\n`next()` 不会立即向外抛异常；捕获的异常可通过 `ctx.error` 读取，并在中间件链完全\n展开后重新抛出，除非被标记为 handled。\n\n短路时，不调用 `next()` 即可：\n\n```kotlin\napp.use { ctx, next -\u003e\n    if (ctx.path == \"/health\") {\n        ctx.text(\"ok\")\n        return@use\n    }\n    next()\n}\n```\n\n#### 中间件可以应用在哪里\n\n| 作用域 | API | 执行时机 |\n|---|---|---|\n| 全局 | `app.use(middleware)` | 每个 HTTP 请求 |\n| 前缀 | `app.use(\"/api\", middleware)` | `/api` 下的路径 |\n| 条件 | `app.use({ ctx -\u003e ... }, middleware)` | predicate 返回 `true` 时 |\n| 路由级 | `app.get(\"/todos\").use(middleware).handle { ... }` | 某个 method + path |\n| 路由组 | `app.group(\"/api\") { use(middleware) }` | 组内注册的路由 |\n\n```kotlin\napp.use(RequestLogger())\napp.use(\"/api\", ApiKeyMiddleware())\napp.use({ ctx -\u003e ctx.accepts(\"json\") }, JsonOnlyMiddleware())\n\napp.get(\"/todos/{id}\")\n    .use(AuthMiddleware())\n    .handle(::getTodo)\n```\n\n使用路由组把相关路由放在一起；当同一个中间件需要覆盖多个位置注册的同前缀路径时，\n使用前缀中间件。\n\n### 依赖注入\n\n服务必须显式注册。依赖服务的路由应在服务注册之后添加。\n\n```kotlin\napp.provide(TodoService())                         // 现有单例\napp.provide { TodoService() }                      // 延迟单例\napp.provide(singleton = false) { TodoService() }   // 瞬态\n```\n\n可以从 `Context` 显式解析服务：\n\n```kotlin\napp.get(\"/todos\") { ctx -\u003e\n    ctx.getService\u003cTodoService\u003e().list(completed = null)\n}\n```\n\n也可以把服务声明为 function/controller 参数：\n\n```kotlin\nfun listTodos(service: TodoService): List\u003cTodo\u003e =\n    service.list(completed = null)\n```\n\nQualifier 用于区分同类型的多个服务：\n\n```kotlin\nobject Primary\nobject Replica\n\napp.provide(qualifier = Primary) { primaryDataSource }\napp.provide(qualifier = Replica) { replicaDataSource }\n\nfun report(@Qualifier(\"Replica\") ds: DataSource): DataSource = ds\n```\n\n子应用中的服务会先从当前应用解析，再向父应用查找。\n\n### 错误处理\n\n在 handler 或中间件中直接抛 HTTP 异常：\n\n```kotlin\nthrow BadRequest(\"Invalid input\")\nthrow Unauthorized(\"Authentication required\")\nthrow Forbidden(\"Access denied\")\nthrow NotFound(\"Todo not found\")\nthrow Conflict(\"Already exists\")\nthrow TooManyRequests(\"Rate limit exceeded\")\n```\n\n按异常类型注册错误处理器：\n\n```kotlin\napp.onError\u003cValidationException\u003e { e, ctx -\u003e\n    ctx.status(422).json(mapOf(\"error\" to \"validation_failed\", \"fields\" to e.errors))\n}\n\napp.onError\u003cHttpException\u003e { e, ctx -\u003e\n    ctx.status(e.status).json(mapOf(\"code\" to e.code, \"message\" to e.message))\n}\n```\n\n如果没有匹配的自定义处理器，Colleen 会根据内容协商返回 JSON 或 HTML。\n服务端错误会自动记录日志。\n\n### 数据验证\n\n```kotlin\napp.post(\"/todos\") { ctx -\u003e\n    val body = ctx.json\u003cCreateTodo\u003e() ?: throw BadRequest(\"Missing body\")\n\n    expect {\n        field(\"title\", body.title)\n            .required()\n            .notBlank()\n            .maxSize(100)\n    }\n\n    Result.created(ctx.getService\u003cTodoService\u003e().create(body.title))\n}\n```\n\n验证规则默认是可选的，`.required()` 表示必填，所有字段错误会聚合到一个\n`ValidationException` 中。\n\n### 子应用\n\n子应用可以把大型服务拆成多个相互隔离的 Colleen app。它适合 API 版本、管理后台、\n内部工具、功能模块或插件式组合。\n\n```kotlin\nval api = Colleen()\napi.get(\"/todos\", ::listTodos)\n\nval app = Colleen()\napp.mount(\"/api\", api)\n```\n\n在子应用内部，路由相对于挂载路径书写：\n\n| 值 | `GET /api/todos` 示例 |\n|---|---|\n| `ctx.path` | `/todos` |\n| `ctx.fullPath` | `/api/todos` |\n| `ctx.pattern` | `/todos` |\n| `ctx.fullPattern` | `/api/todos` |\n\n每个子应用都有独立的路由、中间件、服务、错误处理器和配置。部分请求上下文会通过父子链共享。\n\n| 边界 | 行为 |\n|---|---|\n| 路由和中间件 | 每个 app 独立 |\n| 服务 | 先查当前 app，再查父 app |\n| 状态 | 子上下文可以读取父上下文状态 |\n| 错误处理器 | 子应用优先；未处理错误可交给父应用 |\n| 配置 | 每个 app 独立 |\n| 事件 | 执行事件可以向父应用冒泡 |\n\n```kotlin\nval root = Colleen()\nroot.provide(Database())\n\nval admin = Colleen()\nadmin.use(AdminOnly())\nadmin.get(\"/stats\") { ctx -\u003e\n    ctx.getService\u003cDatabase\u003e().stats()\n}\n\nroot.mount(\"/admin\", admin)\n```\n\n默认情况下，子应用中未处理的异常可以向父应用传播。当子应用需要独立错误边界时可关闭：\n\n```kotlin\nadmin.config {\n    propagateExceptions = false\n}\n```\n\n限制：子应用必须在启动前挂载；同一个 app 实例只能挂载一次。\n\n### 事件\n\n事件是同步的观察 hook，适合用于指标、追踪、日志和框架扩展。\n\n```kotlin\napp.on\u003cEvent.ResponseSent\u003e { event -\u003e\n    println(\"${event.ctx.method} ${event.ctx.fullPath} \" +\n        \"${event.ctx.response.status} ${event.total.inWholeMilliseconds}ms\")\n}\n```\n\n常见事件：\n\n| 类别 | 事件 |\n|---|---|\n| Server 生命周期 | `ServerStarting`, `ServerStarted`, `ServerStopping`, `ServerStopped` |\n| 请求生命周期 | `RequestReceived`, `ResponseReady`, `ResponseSent` |\n| 执行过程 | `MiddlewareExecuting`, `MiddlewareExecuted`, `HandlerExecuting`, `HandlerExecuted`, `SubAppExecuting`, `SubAppExecuted` |\n| 异常 | `ExceptionCaught`, `ExceptionHandled` |\n\n需要改变请求流程时使用中间件和 handler；事件更适合观察和扩展。\n\n---\n\n## API 参考\n\n### 参数提取器\n\n这些类型可以声明在 function-style handler 或 controller 方法中。\n\n| 类型 | 来源 | 必填/可选语义 |\n|---|---|---|\n| `Path\u003cT\u003e` | 路由路径段 | 必填；转换失败返回 400 |\n| `Query\u003cT\u003e` | 查询字符串 | 由可空性和默认值控制是否必填 |\n| `Form\u003cT\u003e` | 表单字段或表单 DTO | 与 `Query\u003cT\u003e` 类似，来源为表单 |\n| `Json\u003cT\u003e` | JSON 请求体 | 使用配置的 JSON mapper 解析 |\n| `Header` | HTTP 请求头 | 始终可空 |\n| `Cookie` | 请求 Cookie | 始终可空 |\n| `Text` | 文本请求体 | 为空时可空 |\n| `Stream` | 原始请求体流 | 可空，只能读取一次 |\n| `UploadedFile` | multipart 文件 | 缺失时可空 |\n| `Context` | 请求上下文 | 直接注入 |\n| 其他类型 | 服务容器 | 作为服务解析 |\n\n示例：\n\n```kotlin\nfun search(\n    q: Query\u003cString?\u003e,\n    limit: Query\u003cInt\u003e = Query(20),\n    tags: Query\u003cList\u003cString\u003e\u003e,\n): List\u003cTodo\u003e = emptyList()\n\nfun upload(file: UploadedFile): Map\u003cString, Any?\u003e =\n    mapOf(\"filename\" to file.value?.filename, \"size\" to file.value?.size)\n```\n\n`Query\u003cT\u003e` 和 `Form\u003cT\u003e` 规则：\n\n| 声明 | 输入缺失时 |\n|---|---|\n| `Query\u003cString\u003e` | 400 Bad Request |\n| `Query\u003cString?\u003e` | `null` |\n| `Query\u003cInt\u003e = Query(1)` | 使用默认值 |\n| `Query\u003cList\u003cT\u003e\u003e` | `emptyList()` |\n| `Query\u003cMap\u003cString, String\u003e\u003e` | `emptyMap()` |\n| `Query\u003cMap\u003cString, List\u003cString\u003e\u003e\u003e` | `emptyMap()` |\n\n自定义提取器实现 `ExtractorFactory`，也可以描述其 OpenAPI 表现。\n\n### 自定义参数提取器\n\n自定义提取器可以把请求解析逻辑从 handler 中移出，并变成可复用、类型安全的参数。\n\n```kotlin\nimport io.github.cymoo.colleen.*\nimport io.github.cymoo.colleen.openapi.*\nimport java.lang.reflect.Parameter\n\nclass BearerToken(value: String?) : ParamExtractor\u003cString?\u003e(value) {\n    companion object : ExtractorFactory\u003cBearerToken\u003e {\n        override fun build(paramName: String, param: Parameter): (Context) -\u003e BearerToken {\n            return { ctx -\u003e\n                val token = ctx.header(\"Authorization\")\n                    ?.removePrefix(\"Bearer \")\n                    ?.trim()\n                    ?.takeIf { it.isNotEmpty() }\n\n                BearerToken(token)\n            }\n        }\n\n        override fun describeOpenApi(paramName: String, param: Parameter) = OpenApiParamSpec(\n            parameters = listOf(\n                OpenApiParameter(\n                    name = \"Authorization\",\n                    location = \"header\",\n                    schema = mapOf(\"type\" to \"string\"),\n                    description = \"Bearer token. Format: `Bearer \u003ctoken\u003e`\",\n                )\n            )\n        )\n    }\n\n    fun require(): String =\n        value ?: throw Unauthorized(\"Bearer token is required\")\n}\n\nfun me(token: BearerToken, service: UserService): UserProfile =\n    service.profile(token.require())\n```\n\n`build` 负责提取，`describeOpenApi` 让该提取器出现在生成的 API 文档中。当缺失必填值\n应由框架报告，而不是像 `require()` 这样在 handler 逻辑中处理时，再添加 `missingMessage`。\n\n### Context API\n\n多数 handler 只需要 `Context` 的一小部分能力。\n\n| API | 用途 |\n|---|---|\n| `ctx.method`, `ctx.path`, `ctx.fullPath` | 请求方法和路径 |\n| `ctx.pattern`, `ctx.fullPattern` | 路由匹配完成后的模式 |\n| `ctx.pathParam(\"id\")` | 路径参数 |\n| `ctx.query(\"q\")`, `ctx.queries()` | 查询参数 |\n| `ctx.queries\u003cT\u003e()` | 将查询参数绑定到 DTO |\n| `ctx.form(\"name\")`, `ctx.forms\u003cT\u003e()` | 表单值 |\n| `ctx.header(\"Authorization\")` | 请求头 |\n| `ctx.accepts(\"json\")` | 内容协商 |\n| `ctx.acceptsLang(\"zh-CN\")` | 语言协商 |\n| `ctx.text()`, `ctx.json\u003cT\u003e()` | 请求体解析 |\n| `ctx.file(\"avatar\")` | 上传文件 |\n| `ctx.getService\u003cT\u003e()` | 必需服务 |\n| `ctx.getServiceOrNull\u003cT\u003e()` | 可选服务 |\n| `ctx.setState(key, value)` | 请求状态 |\n| `ctx.getState\u003cT\u003e(key)` | 必需状态 |\n| `ctx.getStateOrNull\u003cT\u003e(key)` | 可选状态 |\n\n响应辅助方法：\n\n| API | 响应 |\n|---|---|\n| `ctx.status(201)` | 设置状态码 |\n| `ctx.header(\"X-Trace\", id)` | 设置响应头 |\n| `ctx.text(\"ok\")` | `text/plain` |\n| `ctx.html(html)` | `text/html` |\n| `ctx.json(data)` | JSON |\n| `ctx.json(data, stream = true)` | 流式 JSON |\n| `ctx.bytes(bytes, contentType)` | 二进制响应 |\n| `ctx.stream(input, contentType)` | 流式响应 |\n| `ctx.sendFile(path, baseDir = \"...\")` | 带内容协商的文件响应 |\n| `ctx.redirect(\"/new\")` | 重定向 |\n| `ctx.sse { conn -\u003e ... }` | Server-Sent Events |\n\n直接解析请求体、查询或表单的 API 在输入缺失或为空时返回 `null`，输入存在但格式错误时抛\n`BadRequest`。\n\n### Handler 返回值\n\n如果 handler 返回一个值，并且响应尚未被显式写入，Colleen 会将返回值映射为 HTTP 响应。\n\n| 返回值 | 响应 |\n|---|---|\n| `Unit` / Java `void` | 204 No Content |\n| `String` | `text/plain` |\n| `ByteArray` | `application/octet-stream` |\n| `InputStream` | 流式 octet-stream |\n| `Map\u003c*, *\u003e`, `List\u003c*\u003e` | JSON |\n| 其他对象 | JSON |\n| `Int`, `Long`, `Status` | 空 body 的状态码响应 |\n| `Result\u003cT\u003e` | 状态码 + 响应头 + 映射后的 body |\n| `ResponseBody` | 原始响应体 |\n| `null` | 错误；使用 `Unit` 或 `ctx.json(null)` |\n\n```kotlin\napp.get(\"/todos\") { listOf(Todo(1, \"Try Colleen\")) }\napp.post(\"/todos\") { Result.created(Todo(2, \"Write docs\")) }\napp.delete(\"/todos/{id}\") { Result.noContent() }\napp.get(\"/status\") { Status(204) }\n```\n\n### 内置中间件\n\n| 中间件 | 用途 |\n|---|---|\n| `ServeStatic` | 静态文件服务，带安全检查和缓存 |\n| `BasicAuth` | HTTP Basic 认证 |\n| `Cors` | CORS 与预检请求处理 |\n| `RateLimiter` | 令牌桶限流 |\n| `RequestId` | 请求 ID 传递 |\n| `RequestLogger` | 简单访问日志 |\n| `SecurityHeaders` | 常用 HTTP 安全头 |\n| `SignedCookie` | 支持密钥轮换的签名 Cookie |\n| `Heartbeat` | 健康检查端点 |\n| `NoCache` | 禁用客户端和代理缓存 |\n| `Sunset` | RFC 8594 API 弃用头 |\n\n```kotlin\napp.use(RequestId())\napp.use(RequestLogger())\napp.use(Cors.permissive())\napp.use(ServeStatic(root = \"./public\", baseUrl = \"/static\"))\n```\n\n---\n\n## WebSocket 与 SSE\n\n### WebSocket\n\n```kotlin\napp.ws(\"/chat/{room}\") { conn -\u003e\n    val room = conn.pathParam(\"room\")\n    val name = conn.query(\"name\") ?: \"anonymous\"\n\n    conn.onMessage { text -\u003e\n        conn.send(\"[$room] $name: $text\")\n    }\n\n    conn.onBinary { bytes -\u003e\n        conn.send(bytes)\n    }\n}\n```\n\nWebSocket 中间件在握手阶段运行：\n\n```kotlin\napp.wsUse(\"/chat\") { ctx, next -\u003e\n    if (ctx.header(\"Authorization\") == null) {\n        ctx.status(401).text(\"Unauthorized\")\n        return@wsUse\n    }\n    next()\n}\n```\n\n`WsConnection` 可以访问路径参数、查询参数、握手请求头、服务，以及中间件阶段捕获的状态。\n\n也支持 controller 风格 WebSocket：\n\n```kotlin\n@Controller(\"/notifications\")\nclass NotificationController {\n    @Ws(\"/live\")\n    fun live(conn: WsConnection) {\n        conn.onMessage { msg -\u003e conn.send(\"ack\") }\n    }\n}\n```\n\n### Server-Sent Events\n\n```kotlin\napp.get(\"/events\") { ctx -\u003e\n    ctx.sse { conn -\u003e\n        conn.keepAlive(15)\n        conn.onClose { reason -\u003e println(\"closed: $reason\") }\n        conn.send(\"hello\")\n    }\n}\n```\n\n单向服务器推送用 SSE；双向实时通信使用 WebSocket。\n\n---\n\n## OpenAPI\n\n启用 OpenAPI 和 Swagger UI：\n\n```kotlin\napp.openApi(\n    title = \"Todo API\",\n    version = \"1.0.0\",\n    description = \"A small Colleen API\"\n)\n```\n\n默认值：\n\n| 选项 | 默认值 |\n|---|---|\n| `path` | `/openapi.json` |\n| `uiPath` | `/docs` |\n| `uiHtml` | Swagger UI |\n\nFunction-style 和 controller handler 比 lambda handler 能提供更丰富的元数据，因为\nColleen 可以检查它们的函数签名。\n\n```kotlin\n@Tags(\"todos\")\n@Summary(\"Get a todo\")\n@Description(\"Returns one todo by id.\")\n@ParamDesc(name = \"id\", description = \"Todo id\")\n@ResponseDesc(404, \"Todo not found\")\nfun getTodo(id: Path\u003cInt\u003e, service: TodoService): Todo =\n    service.find(id.value) ?: throw NotFound(\"Todo not found\")\n```\n\nSchema 注解可以补充 DTO 信息：\n\n```kotlin\ndata class Todo(\n    @Schema(description = \"Todo id\", example = \"1\")\n    val id: Int,\n    @Schema(description = \"Task title\", example = \"Ship docs\")\n    val title: String,\n    @Schema(hidden = true)\n    val internalVersion: Int = 0,\n)\n```\n\n常用注解：\n\n| 注解 | 用途 |\n|---|---|\n| `@Summary` | 操作摘要 |\n| `@Description` | 操作描述 |\n| `@Tags` | Swagger/ReDoc 分组 |\n| `@ParamDesc` | 参数说明和 required 覆盖 |\n| `@ResponseDesc` | 按状态码说明响应 |\n| `@Schema` | 字段级 schema 元数据 |\n| `@Hidden` | 排除 handler 或 controller |\n\nOpenAPI 会包含已挂载子应用中的路由。基于注解排除用 `@Hidden`，按路径/方法排除用 `filter`。\n\n---\n\n## 测试\n\n`TestClient` 会在进程内执行请求，并走与生产环境相同的路由、中间件、参数提取、验证、\n依赖注入和错误处理管线。\n\n```kotlin\nval app = Colleen()\napp.provide(TodoService())\napp.post(\"/todos\", ::createTodo)\n\nval client = TestClient(app)\n\nval response = client.post(\"/todos\")\n    .json(mapOf(\"title\" to \"Write tests\"))\n    .send()\n\nresponse.assertStatus(201)\n\nval todo = response.json\u003cTodo\u003e()!!\ncheck(todo.title == \"Write tests\")\n```\n\n适合用 `TestClient` 做 handler 测试、中间件/安全测试，以及不绑定端口的轻量集成测试。\n\n---\n\n## Java 支持\n\nColleen 使用 Kotlin 编写，但提供 Java 友好的 API。\n\nJava 中的主要差异是：需要显式运行时类型、显式参数名，以及用 `ch(...)` 包装方法引用。\n\n```java\nimport io.github.cymoo.colleen.*;\n\nimport static io.github.cymoo.colleen.lambda.ch;\n\nclass App {\n    static Todo getTodo(@Param(\"id\") Path\u003cInteger\u003e id, TodoService service) {\n        var todo = service.find(id.value);\n        if (todo == null) throw new NotFound(\"Todo not found\");\n        return todo;\n    }\n\n    public static void main(String[] args) {\n        var app = new Colleen();\n        app.provide(TodoService.class, new TodoService());\n        app.get(\"/todos/{id}\", ch(App::getTodo));\n        app.listen(8000);\n    }\n}\n```\n\n需要记住的规则：\n\n| Kotlin | Java |\n|---|---|\n| `ctx.json\u003cTodo\u003e()` | `ctx.json(Todo.class)` |\n| `ctx.getService\u003cTodoService\u003e()` | `ctx.getService(TodoService.class)` |\n| `app.onError\u003cBadRequest\u003e { ... }` | `app.onError(BadRequest.class, (e, ctx) -\u003e { ... })` |\n| Kotlin 函数引用 | Java 方法引用需要 `ch(...)` |\n| 参数名自动保留 | 使用 `@Param(\"name\")` 或 `-parameters` |\n\nJava 中解析泛型 JSON 时使用 `TypeRef`：\n\n```java\nList\u003cTodo\u003e todos = ctx.json(TypeRef.listOf(Todo.class));\nMap\u003cString, Todo\u003e byId = ctx.json(TypeRef.mapOf(String.class, Todo.class));\n```\n\n---\n\n## 配置\n\n```kotlin\napp.config {\n    server {\n        host = \"127.0.0.1\"\n        port = 8000\n        useVirtualThreads = true\n        maxThreads = Runtime.getRuntime().availableProcessors() * 8\n        maxConcurrentRequests = 0\n        maxRequestSize = 30 * 1024 * 1024\n        maxFileSize = 10 * 1024 * 1024\n        fileSizeThreshold = 256 * 1024\n        shutdownTimeout = 30_000\n        idleTimeout = 30_000\n    }\n\n    ws {\n        idleTimeoutMs = 300_000\n        maxMessageSizeBytes = 64 * 1024\n        pingIntervalMs = 30_000\n        pingTimeoutMs = 10_000\n        maxConnections = 0\n    }\n\n    json {\n        pretty = false\n        includeNulls = false\n        failOnUnknownProperties = true\n        failOnNullForPrimitives = true\n        failOnEmptyBeans = false\n        acceptSingleValueAsArray = false\n        writeDatesAsTimestamps = false\n        dateFormat = null\n        writeEnumsUsingToString = false\n        readEnumsUsingToString = false\n    }\n\n    propagateExceptions = true\n}\n```\n\n需要时可以替换 JSON mapper：\n\n```kotlin\napp.config {\n    jsonMapper(MyCustomJsonMapper())\n}\n```\n\n---\n\n## 生产建议\n\n- **虚拟线程**：Java 21+ 默认启用。它让同步 handler 更适合 IO 密集场景，但上线前仍应\n  基于自己的负载压测。\n- **限制**：公网服务建议显式设置 `maxConcurrentRequests`、`maxRequestSize` 和 `maxFileSize`。\n- **流式 JSON**：大响应可用 `ctx.json(data, stream = true)`；小响应无需启用。\n- **全局中间件**：每个全局中间件都会处理每个请求。能使用前缀中间件时优先使用前缀。\n- **结构化日志**：需要最终状态码、耗时、发送字节数时，优先使用 `Event.ResponseSent`。\n- **静态文件**：路径包含用户输入时，使用 `sendFile(..., baseDir = \"...\")` 或 `ServeStatic`。\n\n可复现的基准压测配置见 `examples/benchmark-api/README.md`。\n\n---\n\n## 示例\n\n建议从这里开始：\n\n### 入门\n\n| 示例 | 内容 |\n|---|---|\n| [hello-world](examples/hello-world) | 最小可运行 Colleen 应用。 |\n| [todo-app](examples/todo-app) | 包含 validation 和 CORS 的 JSON CRUD API。 |\n| [testing](examples/testing) | 使用 `TestClient` 进行进程内请求测试。 |\n| [openapi](examples/openapi) | OpenAPI 注解和 Swagger UI。 |\n\n### 核心 API\n\n| 示例 | 内容 |\n|---|---|\n| [extractor](examples/extractor) | 内置 path、query、form、JSON、header、cookie 和 file 提取。 |\n| [custom-extractor](examples/custom-extractor) | Bearer token、分页等领域化参数提取器。 |\n| [validator](examples/validator) | 验证 DSL 和聚合字段错误。 |\n| [middleware-showcase](examples/middleware-showcase) | 内置中间件和常见中间件模式。 |\n| [auth-app](examples/auth-app) | 使用自定义中间件和服务注入实现认证。 |\n| [error-handling](examples/error-handling) | 全局错误处理器和子应用错误传播。 |\n| [sub-app](examples/sub-app) | 使用挂载子应用构建模块化应用。 |\n| [event-system](examples/event-system) | 生命周期和请求/响应事件。 |\n\n### 实时与文件\n\n| 示例 | 内容 |\n|---|---|\n| [websocket](examples/websocket) | WebSocket 路由、中间件和 controller 风格处理。 |\n| [sse](examples/sse) | 带 keep-alive 和关闭处理的 Server-Sent Events。 |\n| [upload-app](examples/upload-app) | Multipart 上传和文件下载。 |\n| [serve-static](examples/serve-static) | 带缓存和安全控制的静态文件服务。 |\n| [render-html](examples/render-html) | 使用 Pebble 模板渲染 HTML。 |\n\n### 集成与运维\n\n| 示例 | 内容 |\n|---|---|\n| [jdbc](examples/jdbc) | SQLite JDBC 集成和批量执行。 |\n| [jooq-sqlite](examples/jooq-sqlite) | jOOQ 代码生成和类型安全 SQLite 查询。 |\n| [redis](examples/redis) | Redis 支持的响应缓存中间件。 |\n| [auto-reload](examples/auto-reload) | 开发期自动重载流程。 |\n| [benchmark-api](examples/benchmark-api) | 可复现的基准压测配置和负载场景。 |\n\n---\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcymoo%2Fcolleen","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcymoo%2Fcolleen","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcymoo%2Fcolleen/lists"}