https://github.com/cymoo/colleen
A lightweight web framework for Kotlin and Java
https://github.com/cymoo/colleen
java kotlin microservice openapi spring-alternative web-framework websocket
Last synced: about 3 hours ago
JSON representation
A lightweight web framework for Kotlin and Java
- Host: GitHub
- URL: https://github.com/cymoo/colleen
- Owner: cymoo
- License: mit
- Created: 2026-02-13T16:03:47.000Z (4 months ago)
- Default Branch: main
- Last Pushed: 2026-04-20T16:27:51.000Z (2 months ago)
- Last Synced: 2026-04-20T18:36:38.264Z (2 months ago)
- Topics: java, kotlin, microservice, openapi, spring-alternative, web-framework, websocket
- Language: Kotlin
- Homepage:
- Size: 815 KB
- Stars: 4
- Watchers: 0
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README-zh.md
- License: LICENSE
Awesome Lists containing this project
README
# Colleen Web 框架
[English Documentation](README.md)
Colleen 是一个基于 Undertow 构建的轻量级、类型安全 Kotlin / Java Web 框架。
它强调显式应用代码、可组合中间件、自动请求参数提取,以及 Java 21+ 上的同步 handler 编程模型。
```kotlin
fun getTodo(id: Path, service: TodoService): Todo =
service.find(id.value) ?: throw NotFound("Todo not found")
fun createTodo(body: Json, service: TodoService): Result =
Result.created(service.create(body.value.title))
app.get("/todos/{id}", ::getTodo)
app.post("/todos", ::createTodo)
```
## 为什么选择 Colleen?
- **类型安全的函数式 handler**:在函数签名中直接声明 `Path`、`Query`、
`Json` 或服务依赖。
- **默认显式**:没有 classpath scanning,没有隐藏的自动装配,也没有全局魔法。
- **可组合中间件**:类似 Koa 的洋葱模型,并保证下游异常时上游 after 逻辑仍会执行。
- **内置 OpenAPI**:函数式 handler 和 controller 可以自动生成有用的 OpenAPI 元数据,
默认在 `/docs` 提供 Swagger UI。
- **实时能力**:WebSocket 和 SSE 是一等 API,不需要额外框架拼接。
- **Kotlin 优先,Java 友好**:Kotlin API 简洁,同时提供显式的 Java 兼容写法。
当你希望框架帮你处理路由、绑定、中间件、文档、测试和实时端点,但又希望应用结构
清楚地留在代码里时,Colleen 会很合适。
## 目录
1. [快速开始](#快速开始)
2. [一个小型 Todo API](#一个小型-todo-api)
3. [核心概念](#核心概念)
4. [API 参考](#api-参考)
5. [WebSocket 与 SSE](#websocket-与-sse)
6. [OpenAPI](#openapi)
7. [测试](#测试)
8. [Java 支持](#java-支持)
9. [配置](#配置)
10. [生产建议](#生产建议)
11. [示例](#示例)
---
## 快速开始
### 要求
- Java 21 或更高版本
- Kotlin 或 Java
- Maven 或 Gradle
### 安装
**Maven**
```xml
io.github.cymoo
colleen
0.4.7
```
**Gradle (Kotlin DSL)**
```kotlin
implementation("io.github.cymoo:colleen:0.4.7")
```
### Hello World
```kotlin
import io.github.cymoo.colleen.*
fun main() {
val app = Colleen()
app.get("/") { "hello world" }
app.listen(8000)
}
```
打开 。
---
## 一个小型 Todo API
这个示例刻意保持短小,但覆盖了 Colleen 的主要工作流:函数式 handler、类型安全参数
提取、服务注入、结构化响应、HTTP 错误和 OpenAPI。
```kotlin
import io.github.cymoo.colleen.*
data class CreateTodo(val title: String)
data class Todo(val id: Int, val title: String, val completed: Boolean = false)
class TodoService {
private val todos = linkedMapOf(
1 to Todo(1, "Try Colleen"),
2 to Todo(2, "Open /docs"),
)
private var nextId = 3
fun list(completed: Boolean?): List =
todos.values.filter { completed == null || it.completed == completed }
fun find(id: Int): Todo? = todos[id]
fun create(title: String): Todo {
val todo = Todo(nextId++, title)
todos[todo.id] = todo
return todo
}
fun complete(id: Int): Todo? {
val todo = todos[id] ?: return null
val updated = todo.copy(completed = true)
todos[id] = updated
return updated
}
fun delete(id: Int): Boolean = todos.remove(id) != null
}
fun listTodos(completed: Query, service: TodoService): List =
service.list(completed.value)
fun getTodo(id: Path, service: TodoService): Todo =
service.find(id.value) ?: throw NotFound("Todo not found")
fun createTodo(body: Json, service: TodoService): Result =
Result.created(service.create(body.value.title))
fun completeTodo(id: Path, service: TodoService): Todo =
service.complete(id.value) ?: throw NotFound("Todo not found")
fun deleteTodo(id: Path, service: TodoService) {
if (!service.delete(id.value)) throw NotFound("Todo not found")
}
fun main() {
val app = Colleen()
app.provide(TodoService())
app.openApi(
title = "Todo API",
version = "1.0.0",
description = "A small Colleen API"
)
app.get("/todos", ::listTodos)
app.get("/todos/{id}", ::getTodo)
app.post("/todos", ::createTodo)
app.post("/todos/{id}/complete", ::completeTodo)
app.delete("/todos/{id}", ::deleteTodo)
app.listen(8000)
}
```
试一下:
```shell
curl http://localhost:8000/todos
curl http://localhost:8000/todos/1
curl -X POST http://localhost:8000/todos \
-H 'Content-Type: application/json' \
-d '{"title":"Ship docs"}'
```
OpenAPI JSON 位于 `/openapi.json`;Swagger UI 位于 `/docs`。
---
## 核心概念
### Handler 风格
Colleen 支持三种 handler 风格。根据路由复杂度选择最简单合适的一种。
| 风格 | 适合场景 | 示例 |
|---|---|---|
| Lambda | 极小的内联路由 | `app.get("/") { "ok" }` |
| Function-style | 大多数应用 handler | `app.get("/todos/{id}", ::getTodo)` |
| Controller | 更大的分组 API | `app.addController(TodoController(service))` |
对于非平凡路由,推荐默认使用 function-style handler:它是普通函数,便于测试,也能向
参数绑定和 OpenAPI 暴露更完整的类型信息。
### 路由
```kotlin
app.get("/todos") { }
app.post("/todos") { }
app.put("/todos/{id}") { }
app.delete("/todos/{id}") { }
app.patch("/todos/{id}") { }
app.head("/todos/{id}") { }
app.options("/todos") { }
app.all("/health") { }
```
路径参数使用 `{name}`,通配符使用 `{path...}`。
```kotlin
app.get("/todos/{id}", ::getTodo)
app.get("/files/{path...}") { ctx -> ctx.pathParam("path") }
app.get("/images/{name}.{ext}") { ctx -> "${ctx.pathParam("name")}.${ctx.pathParam("ext")}" }
```
在复合路径段中,参数会捕获到下一个静态分隔符首次出现的位置。例如
`/files/{name}-{version}.txt` 匹配 `/files/foo-bar-1.txt` 时,
`name = "foo"`,`version = "bar-1"`。
路由匹配优先级是确定的:静态段、复合段、参数段、通配符段。
### 路由组
路由组会在注册路由时添加统一路径前缀,也可以为一组相关路由附加中间件。
```kotlin
app.group("/api") {
use(ApiKeyMiddleware())
get("/todos", ::listTodos) // GET /api/todos
post("/todos", ::createTodo) // POST /api/todos
group("/admin") {
use(AdminOnly())
get("/stats") { statsService.snapshot() } // GET /api/admin/stats
}
}
```
路由组用于组织路由声明。前缀中间件不同:它注册一次,并在运行时匹配该前缀下的请求。
### Controller 风格路由
```kotlin
@Controller("/todos")
class TodoController(private val service: TodoService) {
@Get
fun list(completed: Query): List =
service.list(completed.value)
@Get("/{id}")
fun get(id: Path): Todo =
service.find(id.value) ?: throw NotFound("Todo not found")
@Post
fun create(body: Json): Result =
Result.created(service.create(body.value.title))
}
app.addController(TodoController(TodoService()))
```
支持的 HTTP 注解包括 `@Get`、`@Post`、`@Put`、`@Delete` 和 `@Patch`。
当需要覆盖逻辑参数名时使用 `@Param("name")`,Java 中尤其常用。
### 中间件
中间件签名是 `(Context, Next) -> Unit`。
```kotlin
val logger = Middleware { ctx, next ->
val start = System.currentTimeMillis()
println("-> ${ctx.method} ${ctx.path}")
next() // 下游异常会被捕获到 ctx.error
val status = if (ctx.error != null) {
(ctx.error?.cause as? HttpException)?.status ?: 500
} else {
ctx.response.status
}
println("<- $status in ${System.currentTimeMillis() - start}ms")
}
app.use(logger)
```
Colleen 使用洋葱模型:
```text
middleware1 before
middleware2 before
handler
middleware2 after
middleware1 after
```
只要某个中间件的 before 部分开始执行,它的 after 部分就会执行,即使下游抛出异常。
`next()` 不会立即向外抛异常;捕获的异常可通过 `ctx.error` 读取,并在中间件链完全
展开后重新抛出,除非被标记为 handled。
短路时,不调用 `next()` 即可:
```kotlin
app.use { ctx, next ->
if (ctx.path == "/health") {
ctx.text("ok")
return@use
}
next()
}
```
#### 中间件可以应用在哪里
| 作用域 | API | 执行时机 |
|---|---|---|
| 全局 | `app.use(middleware)` | 每个 HTTP 请求 |
| 前缀 | `app.use("/api", middleware)` | `/api` 下的路径 |
| 条件 | `app.use({ ctx -> ... }, middleware)` | predicate 返回 `true` 时 |
| 路由级 | `app.get("/todos").use(middleware).handle { ... }` | 某个 method + path |
| 路由组 | `app.group("/api") { use(middleware) }` | 组内注册的路由 |
```kotlin
app.use(RequestLogger())
app.use("/api", ApiKeyMiddleware())
app.use({ ctx -> ctx.accepts("json") }, JsonOnlyMiddleware())
app.get("/todos/{id}")
.use(AuthMiddleware())
.handle(::getTodo)
```
使用路由组把相关路由放在一起;当同一个中间件需要覆盖多个位置注册的同前缀路径时,
使用前缀中间件。
### 依赖注入
服务必须显式注册。依赖服务的路由应在服务注册之后添加。
```kotlin
app.provide(TodoService()) // 现有单例
app.provide { TodoService() } // 延迟单例
app.provide(singleton = false) { TodoService() } // 瞬态
```
可以从 `Context` 显式解析服务:
```kotlin
app.get("/todos") { ctx ->
ctx.getService().list(completed = null)
}
```
也可以把服务声明为 function/controller 参数:
```kotlin
fun listTodos(service: TodoService): List =
service.list(completed = null)
```
Qualifier 用于区分同类型的多个服务:
```kotlin
object Primary
object Replica
app.provide(qualifier = Primary) { primaryDataSource }
app.provide(qualifier = Replica) { replicaDataSource }
fun report(@Qualifier("Replica") ds: DataSource): DataSource = ds
```
子应用中的服务会先从当前应用解析,再向父应用查找。
### 错误处理
在 handler 或中间件中直接抛 HTTP 异常:
```kotlin
throw BadRequest("Invalid input")
throw Unauthorized("Authentication required")
throw Forbidden("Access denied")
throw NotFound("Todo not found")
throw Conflict("Already exists")
throw TooManyRequests("Rate limit exceeded")
```
按异常类型注册错误处理器:
```kotlin
app.onError { e, ctx ->
ctx.status(422).json(mapOf("error" to "validation_failed", "fields" to e.errors))
}
app.onError { e, ctx ->
ctx.status(e.status).json(mapOf("code" to e.code, "message" to e.message))
}
```
如果没有匹配的自定义处理器,Colleen 会根据内容协商返回 JSON 或 HTML。
服务端错误会自动记录日志。
### 数据验证
```kotlin
app.post("/todos") { ctx ->
val body = ctx.json() ?: throw BadRequest("Missing body")
expect {
field("title", body.title)
.required()
.notBlank()
.maxSize(100)
}
Result.created(ctx.getService().create(body.title))
}
```
验证规则默认是可选的,`.required()` 表示必填,所有字段错误会聚合到一个
`ValidationException` 中。
### 子应用
子应用可以把大型服务拆成多个相互隔离的 Colleen app。它适合 API 版本、管理后台、
内部工具、功能模块或插件式组合。
```kotlin
val api = Colleen()
api.get("/todos", ::listTodos)
val app = Colleen()
app.mount("/api", api)
```
在子应用内部,路由相对于挂载路径书写:
| 值 | `GET /api/todos` 示例 |
|---|---|
| `ctx.path` | `/todos` |
| `ctx.fullPath` | `/api/todos` |
| `ctx.pattern` | `/todos` |
| `ctx.fullPattern` | `/api/todos` |
每个子应用都有独立的路由、中间件、服务、错误处理器和配置。部分请求上下文会通过父子链共享。
| 边界 | 行为 |
|---|---|
| 路由和中间件 | 每个 app 独立 |
| 服务 | 先查当前 app,再查父 app |
| 状态 | 子上下文可以读取父上下文状态 |
| 错误处理器 | 子应用优先;未处理错误可交给父应用 |
| 配置 | 每个 app 独立 |
| 事件 | 执行事件可以向父应用冒泡 |
```kotlin
val root = Colleen()
root.provide(Database())
val admin = Colleen()
admin.use(AdminOnly())
admin.get("/stats") { ctx ->
ctx.getService().stats()
}
root.mount("/admin", admin)
```
默认情况下,子应用中未处理的异常可以向父应用传播。当子应用需要独立错误边界时可关闭:
```kotlin
admin.config {
propagateExceptions = false
}
```
限制:子应用必须在启动前挂载;同一个 app 实例只能挂载一次。
### 事件
事件是同步的观察 hook,适合用于指标、追踪、日志和框架扩展。
```kotlin
app.on { event ->
println("${event.ctx.method} ${event.ctx.fullPath} " +
"${event.ctx.response.status} ${event.total.inWholeMilliseconds}ms")
}
```
常见事件:
| 类别 | 事件 |
|---|---|
| Server 生命周期 | `ServerStarting`, `ServerStarted`, `ServerStopping`, `ServerStopped` |
| 请求生命周期 | `RequestReceived`, `ResponseReady`, `ResponseSent` |
| 执行过程 | `MiddlewareExecuting`, `MiddlewareExecuted`, `HandlerExecuting`, `HandlerExecuted`, `SubAppExecuting`, `SubAppExecuted` |
| 异常 | `ExceptionCaught`, `ExceptionHandled` |
需要改变请求流程时使用中间件和 handler;事件更适合观察和扩展。
---
## API 参考
### 参数提取器
这些类型可以声明在 function-style handler 或 controller 方法中。
| 类型 | 来源 | 必填/可选语义 |
|---|---|---|
| `Path` | 路由路径段 | 必填;转换失败返回 400 |
| `Query` | 查询字符串 | 由可空性和默认值控制是否必填 |
| `Form` | 表单字段或表单 DTO | 与 `Query` 类似,来源为表单 |
| `Json` | JSON 请求体 | 使用配置的 JSON mapper 解析 |
| `Header` | HTTP 请求头 | 始终可空 |
| `Cookie` | 请求 Cookie | 始终可空 |
| `Text` | 文本请求体 | 为空时可空 |
| `Stream` | 原始请求体流 | 可空,只能读取一次 |
| `UploadedFile` | multipart 文件 | 缺失时可空 |
| `Context` | 请求上下文 | 直接注入 |
| 其他类型 | 服务容器 | 作为服务解析 |
示例:
```kotlin
fun search(
q: Query,
limit: Query = Query(20),
tags: Query>,
): List = emptyList()
fun upload(file: UploadedFile): Map =
mapOf("filename" to file.value?.filename, "size" to file.value?.size)
```
`Query` 和 `Form` 规则:
| 声明 | 输入缺失时 |
|---|---|
| `Query` | 400 Bad Request |
| `Query` | `null` |
| `Query = Query(1)` | 使用默认值 |
| `Query>` | `emptyList()` |
| `Query>` | `emptyMap()` |
| `Query>>` | `emptyMap()` |
自定义提取器实现 `ExtractorFactory`,也可以描述其 OpenAPI 表现。
### 自定义参数提取器
自定义提取器可以把请求解析逻辑从 handler 中移出,并变成可复用、类型安全的参数。
```kotlin
import io.github.cymoo.colleen.*
import io.github.cymoo.colleen.openapi.*
import java.lang.reflect.Parameter
class BearerToken(value: String?) : ParamExtractor(value) {
companion object : ExtractorFactory {
override fun build(paramName: String, param: Parameter): (Context) -> BearerToken {
return { ctx ->
val token = ctx.header("Authorization")
?.removePrefix("Bearer ")
?.trim()
?.takeIf { it.isNotEmpty() }
BearerToken(token)
}
}
override fun describeOpenApi(paramName: String, param: Parameter) = OpenApiParamSpec(
parameters = listOf(
OpenApiParameter(
name = "Authorization",
location = "header",
schema = mapOf("type" to "string"),
description = "Bearer token. Format: `Bearer `",
)
)
)
}
fun require(): String =
value ?: throw Unauthorized("Bearer token is required")
}
fun me(token: BearerToken, service: UserService): UserProfile =
service.profile(token.require())
```
`build` 负责提取,`describeOpenApi` 让该提取器出现在生成的 API 文档中。当缺失必填值
应由框架报告,而不是像 `require()` 这样在 handler 逻辑中处理时,再添加 `missingMessage`。
### Context API
多数 handler 只需要 `Context` 的一小部分能力。
| API | 用途 |
|---|---|
| `ctx.method`, `ctx.path`, `ctx.fullPath` | 请求方法和路径 |
| `ctx.pattern`, `ctx.fullPattern` | 路由匹配完成后的模式 |
| `ctx.pathParam("id")` | 路径参数 |
| `ctx.query("q")`, `ctx.queries()` | 查询参数 |
| `ctx.queries()` | 将查询参数绑定到 DTO |
| `ctx.form("name")`, `ctx.forms()` | 表单值 |
| `ctx.header("Authorization")` | 请求头 |
| `ctx.accepts("json")` | 内容协商 |
| `ctx.acceptsLang("zh-CN")` | 语言协商 |
| `ctx.text()`, `ctx.json()` | 请求体解析 |
| `ctx.file("avatar")` | 上传文件 |
| `ctx.getService()` | 必需服务 |
| `ctx.getServiceOrNull()` | 可选服务 |
| `ctx.setState(key, value)` | 请求状态 |
| `ctx.getState(key)` | 必需状态 |
| `ctx.getStateOrNull(key)` | 可选状态 |
响应辅助方法:
| API | 响应 |
|---|---|
| `ctx.status(201)` | 设置状态码 |
| `ctx.header("X-Trace", id)` | 设置响应头 |
| `ctx.text("ok")` | `text/plain` |
| `ctx.html(html)` | `text/html` |
| `ctx.json(data)` | JSON |
| `ctx.json(data, stream = true)` | 流式 JSON |
| `ctx.bytes(bytes, contentType)` | 二进制响应 |
| `ctx.stream(input, contentType)` | 流式响应 |
| `ctx.sendFile(path, baseDir = "...")` | 带内容协商的文件响应 |
| `ctx.redirect("/new")` | 重定向 |
| `ctx.sse { conn -> ... }` | Server-Sent Events |
直接解析请求体、查询或表单的 API 在输入缺失或为空时返回 `null`,输入存在但格式错误时抛
`BadRequest`。
### Handler 返回值
如果 handler 返回一个值,并且响应尚未被显式写入,Colleen 会将返回值映射为 HTTP 响应。
| 返回值 | 响应 |
|---|---|
| `Unit` / Java `void` | 204 No Content |
| `String` | `text/plain` |
| `ByteArray` | `application/octet-stream` |
| `InputStream` | 流式 octet-stream |
| `Map<*, *>`, `List<*>` | JSON |
| 其他对象 | JSON |
| `Int`, `Long`, `Status` | 空 body 的状态码响应 |
| `Result` | 状态码 + 响应头 + 映射后的 body |
| `ResponseBody` | 原始响应体 |
| `null` | 错误;使用 `Unit` 或 `ctx.json(null)` |
```kotlin
app.get("/todos") { listOf(Todo(1, "Try Colleen")) }
app.post("/todos") { Result.created(Todo(2, "Write docs")) }
app.delete("/todos/{id}") { Result.noContent() }
app.get("/status") { Status(204) }
```
### 内置中间件
| 中间件 | 用途 |
|---|---|
| `ServeStatic` | 静态文件服务,带安全检查和缓存 |
| `BasicAuth` | HTTP Basic 认证 |
| `Cors` | CORS 与预检请求处理 |
| `RateLimiter` | 令牌桶限流 |
| `RequestId` | 请求 ID 传递 |
| `RequestLogger` | 简单访问日志 |
| `SecurityHeaders` | 常用 HTTP 安全头 |
| `SignedCookie` | 支持密钥轮换的签名 Cookie |
| `Heartbeat` | 健康检查端点 |
| `NoCache` | 禁用客户端和代理缓存 |
| `Sunset` | RFC 8594 API 弃用头 |
```kotlin
app.use(RequestId())
app.use(RequestLogger())
app.use(Cors.permissive())
app.use(ServeStatic(root = "./public", baseUrl = "/static"))
```
---
## WebSocket 与 SSE
### WebSocket
```kotlin
app.ws("/chat/{room}") { conn ->
val room = conn.pathParam("room")
val name = conn.query("name") ?: "anonymous"
conn.onMessage { text ->
conn.send("[$room] $name: $text")
}
conn.onBinary { bytes ->
conn.send(bytes)
}
}
```
WebSocket 中间件在握手阶段运行:
```kotlin
app.wsUse("/chat") { ctx, next ->
if (ctx.header("Authorization") == null) {
ctx.status(401).text("Unauthorized")
return@wsUse
}
next()
}
```
`WsConnection` 可以访问路径参数、查询参数、握手请求头、服务,以及中间件阶段捕获的状态。
也支持 controller 风格 WebSocket:
```kotlin
@Controller("/notifications")
class NotificationController {
@Ws("/live")
fun live(conn: WsConnection) {
conn.onMessage { msg -> conn.send("ack") }
}
}
```
### Server-Sent Events
```kotlin
app.get("/events") { ctx ->
ctx.sse { conn ->
conn.keepAlive(15)
conn.onClose { reason -> println("closed: $reason") }
conn.send("hello")
}
}
```
单向服务器推送用 SSE;双向实时通信使用 WebSocket。
---
## OpenAPI
启用 OpenAPI 和 Swagger UI:
```kotlin
app.openApi(
title = "Todo API",
version = "1.0.0",
description = "A small Colleen API"
)
```
默认值:
| 选项 | 默认值 |
|---|---|
| `path` | `/openapi.json` |
| `uiPath` | `/docs` |
| `uiHtml` | Swagger UI |
Function-style 和 controller handler 比 lambda handler 能提供更丰富的元数据,因为
Colleen 可以检查它们的函数签名。
```kotlin
@Tags("todos")
@Summary("Get a todo")
@Description("Returns one todo by id.")
@ParamDesc(name = "id", description = "Todo id")
@ResponseDesc(404, "Todo not found")
fun getTodo(id: Path, service: TodoService): Todo =
service.find(id.value) ?: throw NotFound("Todo not found")
```
Schema 注解可以补充 DTO 信息:
```kotlin
data class Todo(
@Schema(description = "Todo id", example = "1")
val id: Int,
@Schema(description = "Task title", example = "Ship docs")
val title: String,
@Schema(hidden = true)
val internalVersion: Int = 0,
)
```
常用注解:
| 注解 | 用途 |
|---|---|
| `@Summary` | 操作摘要 |
| `@Description` | 操作描述 |
| `@Tags` | Swagger/ReDoc 分组 |
| `@ParamDesc` | 参数说明和 required 覆盖 |
| `@ResponseDesc` | 按状态码说明响应 |
| `@Schema` | 字段级 schema 元数据 |
| `@Hidden` | 排除 handler 或 controller |
OpenAPI 会包含已挂载子应用中的路由。基于注解排除用 `@Hidden`,按路径/方法排除用 `filter`。
---
## 测试
`TestClient` 会在进程内执行请求,并走与生产环境相同的路由、中间件、参数提取、验证、
依赖注入和错误处理管线。
```kotlin
val app = Colleen()
app.provide(TodoService())
app.post("/todos", ::createTodo)
val client = TestClient(app)
val response = client.post("/todos")
.json(mapOf("title" to "Write tests"))
.send()
response.assertStatus(201)
val todo = response.json()!!
check(todo.title == "Write tests")
```
适合用 `TestClient` 做 handler 测试、中间件/安全测试,以及不绑定端口的轻量集成测试。
---
## Java 支持
Colleen 使用 Kotlin 编写,但提供 Java 友好的 API。
Java 中的主要差异是:需要显式运行时类型、显式参数名,以及用 `ch(...)` 包装方法引用。
```java
import io.github.cymoo.colleen.*;
import static io.github.cymoo.colleen.lambda.ch;
class App {
static Todo getTodo(@Param("id") Path id, TodoService service) {
var todo = service.find(id.value);
if (todo == null) throw new NotFound("Todo not found");
return todo;
}
public static void main(String[] args) {
var app = new Colleen();
app.provide(TodoService.class, new TodoService());
app.get("/todos/{id}", ch(App::getTodo));
app.listen(8000);
}
}
```
需要记住的规则:
| Kotlin | Java |
|---|---|
| `ctx.json()` | `ctx.json(Todo.class)` |
| `ctx.getService()` | `ctx.getService(TodoService.class)` |
| `app.onError { ... }` | `app.onError(BadRequest.class, (e, ctx) -> { ... })` |
| Kotlin 函数引用 | Java 方法引用需要 `ch(...)` |
| 参数名自动保留 | 使用 `@Param("name")` 或 `-parameters` |
Java 中解析泛型 JSON 时使用 `TypeRef`:
```java
List todos = ctx.json(TypeRef.listOf(Todo.class));
Map byId = ctx.json(TypeRef.mapOf(String.class, Todo.class));
```
---
## 配置
```kotlin
app.config {
server {
host = "127.0.0.1"
port = 8000
useVirtualThreads = true
maxThreads = Runtime.getRuntime().availableProcessors() * 8
maxConcurrentRequests = 0
maxRequestSize = 30 * 1024 * 1024
maxFileSize = 10 * 1024 * 1024
fileSizeThreshold = 256 * 1024
shutdownTimeout = 30_000
idleTimeout = 30_000
}
ws {
idleTimeoutMs = 300_000
maxMessageSizeBytes = 64 * 1024
pingIntervalMs = 30_000
pingTimeoutMs = 10_000
maxConnections = 0
}
json {
pretty = false
includeNulls = false
failOnUnknownProperties = true
failOnNullForPrimitives = true
failOnEmptyBeans = false
acceptSingleValueAsArray = false
writeDatesAsTimestamps = false
dateFormat = null
writeEnumsUsingToString = false
readEnumsUsingToString = false
}
propagateExceptions = true
}
```
需要时可以替换 JSON mapper:
```kotlin
app.config {
jsonMapper(MyCustomJsonMapper())
}
```
---
## 生产建议
- **虚拟线程**:Java 21+ 默认启用。它让同步 handler 更适合 IO 密集场景,但上线前仍应
基于自己的负载压测。
- **限制**:公网服务建议显式设置 `maxConcurrentRequests`、`maxRequestSize` 和 `maxFileSize`。
- **流式 JSON**:大响应可用 `ctx.json(data, stream = true)`;小响应无需启用。
- **全局中间件**:每个全局中间件都会处理每个请求。能使用前缀中间件时优先使用前缀。
- **结构化日志**:需要最终状态码、耗时、发送字节数时,优先使用 `Event.ResponseSent`。
- **静态文件**:路径包含用户输入时,使用 `sendFile(..., baseDir = "...")` 或 `ServeStatic`。
可复现的基准压测配置见 `examples/benchmark-api/README.md`。
---
## 示例
建议从这里开始:
### 入门
| 示例 | 内容 |
|---|---|
| [hello-world](examples/hello-world) | 最小可运行 Colleen 应用。 |
| [todo-app](examples/todo-app) | 包含 validation 和 CORS 的 JSON CRUD API。 |
| [testing](examples/testing) | 使用 `TestClient` 进行进程内请求测试。 |
| [openapi](examples/openapi) | OpenAPI 注解和 Swagger UI。 |
### 核心 API
| 示例 | 内容 |
|---|---|
| [extractor](examples/extractor) | 内置 path、query、form、JSON、header、cookie 和 file 提取。 |
| [custom-extractor](examples/custom-extractor) | Bearer token、分页等领域化参数提取器。 |
| [validator](examples/validator) | 验证 DSL 和聚合字段错误。 |
| [middleware-showcase](examples/middleware-showcase) | 内置中间件和常见中间件模式。 |
| [auth-app](examples/auth-app) | 使用自定义中间件和服务注入实现认证。 |
| [error-handling](examples/error-handling) | 全局错误处理器和子应用错误传播。 |
| [sub-app](examples/sub-app) | 使用挂载子应用构建模块化应用。 |
| [event-system](examples/event-system) | 生命周期和请求/响应事件。 |
### 实时与文件
| 示例 | 内容 |
|---|---|
| [websocket](examples/websocket) | WebSocket 路由、中间件和 controller 风格处理。 |
| [sse](examples/sse) | 带 keep-alive 和关闭处理的 Server-Sent Events。 |
| [upload-app](examples/upload-app) | Multipart 上传和文件下载。 |
| [serve-static](examples/serve-static) | 带缓存和安全控制的静态文件服务。 |
| [render-html](examples/render-html) | 使用 Pebble 模板渲染 HTML。 |
### 集成与运维
| 示例 | 内容 |
|---|---|
| [jdbc](examples/jdbc) | SQLite JDBC 集成和批量执行。 |
| [jooq-sqlite](examples/jooq-sqlite) | jOOQ 代码生成和类型安全 SQLite 查询。 |
| [redis](examples/redis) | Redis 支持的响应缓存中间件。 |
| [auto-reload](examples/auto-reload) | 开发期自动重载流程。 |
| [benchmark-api](examples/benchmark-api) | 可复现的基准压测配置和负载场景。 |
---
## License
MIT