🚀 快速安装
复制以下命令并运行,立即安装此 Skill:
npx skills add https://skills.sh/affaan-m/everything-claude-code/kotlin-ktor-patterns
💡 提示:需要 Node.js 和 NPM
Ktor 服务端模式 (Ktor Server Patterns)
使用 Kotlin 协程构建健壮、可维护 HTTP 服务器的全面 Ktor 模式。
何时激活 (When to Activate)
- 构建 Ktor HTTP 服务器
- 配置 Ktor 插件(认证、CORS、内容协商、状态页面)
- 使用 Ktor 实现 REST API
- 使用 Koin 设置依赖注入
- 使用 testApplication 编写 Ktor 集成测试
- 在 Ktor 中使用 WebSockets
应用程序结构 (Application Structure)
标准 Ktor 项目布局 (Standard Ktor Project Layout)
src/main/kotlin/
├── com/example/
│ ├── Application.kt # 入口点,模块配置 (Entry point, module configuration)
│ ├── plugins/
│ │ ├── Routing.kt # 路由定义 (Route definitions)
│ │ ├── Serialization.kt # 内容协商设置 (Content negotiation setup)
│ │ ├── Authentication.kt # 认证配置 (Auth configuration)
│ │ ├── StatusPages.kt # 错误处理 (Error handling)
│ │ └── CORS.kt # CORS 配置 (CORS configuration)
│ ├── routes/
│ │ ├── UserRoutes.kt # /users 端点 (/users endpoints)
│ │ ├── AuthRoutes.kt # /auth 端点 (/auth endpoints)
│ │ └── HealthRoutes.kt # /health 端点 (/health endpoints)
│ ├── models/
│ │ ├── User.kt # 领域模型 (Domain models)
│ │ └── ApiResponse.kt # 响应封装 (Response envelopes)
│ ├── services/
│ │ ├── UserService.kt # 业务逻辑 (Business logic)
│ │ └── AuthService.kt # 认证逻辑 (Auth logic)
│ ├── repositories/
│ │ ├── UserRepository.kt # 数据访问接口 (Data access interface)
│ │ └── ExposedUserRepository.kt
│ └── di/
│ └── AppModule.kt # Koin 模块 (Koin modules)
src/test/kotlin/
├── com/example/
│ ├── routes/
│ │ └── UserRoutesTest.kt
│ └── services/
│ └── UserServiceTest.kt
应用程序入口点 (Application Entry Point)
// Application.kt
fun main() {
embeddedServer(Netty, port = 8080, module = Application::module).start(wait = true)
}
fun Application.module() {
configureSerialization()
configureAuthentication()
configureStatusPages()
configureCORS()
configureDI()
configureRouting()
}
路由 DSL (Routing DSL)
基础路由 (Basic Routes)
// plugins/Routing.kt
fun Application.configureRouting() {
routing {
userRoutes()
authRoutes()
healthRoutes()
}
}
// routes/UserRoutes.kt
fun Route.userRoutes() {
val userService by inject<UserService>()
route("/users") {
get {
val users = userService.getAll()
call.respond(users)
}
get("/{id}") {
val id = call.parameters["id"]
?: return@get call.respond(HttpStatusCode.BadRequest, "Missing id")
val user = userService.getById(id)
?: return@get call.respond(HttpStatusCode.NotFound)
call.respond(user)
}
post {
val request = call.receive<CreateUserRequest>()
val user = userService.create(request)
call.respond(HttpStatusCode.Created, user)
}
put("/{id}") {
val id = call.parameters["id"]
?: return@put call.respond(HttpStatusCode.BadRequest, "Missing id")
val request = call.receive<UpdateUserRequest>()
val user = userService.update(id, request)
?: return@put call.respond(HttpStatusCode.NotFound)
call.respond(user)
}
delete("/{id}") {
val id = call.parameters["id"]
?: return@delete call.respond(HttpStatusCode.BadRequest, "Missing id")
val deleted = userService.delete(id)
if (deleted) call.respond(HttpStatusCode.NoContent)
else call.respond(HttpStatusCode.NotFound)
}
}
}
带认证的路由组织 (Route Organization with Authenticated Routes)
fun Route.userRoutes() {
route("/users") {
// 公共路由 (Public routes)
get { /* 列出用户 (list users) */ }
get("/{id}") { /* 获取用户 (get user) */ }
// 受保护的路由 (Protected routes)
authenticate("jwt") {
post { /* 创建用户 - 需要认证 (create user - requires auth) */ }
put("/{id}") { /* 更新用户 - 需要认证 (update user - requires auth) */ }
delete("/{id}") { /* 删除用户 - 需要认证 (delete user - requires auth) */ }
}
}
}
内容协商与序列化 (Content Negotiation & Serialization)
kotlinx.serialization 设置 (kotlinx.serialization Setup)
// plugins/Serialization.kt
fun Application.configureSerialization() {
install(ContentNegotiation) {
json(Json {
prettyPrint = true
isLenient = false
ignoreUnknownKeys = true
encodeDefaults = true
explicitNulls = false
})
}
}
可序列化模型 (Serializable Models)
@Serializable
data class UserResponse(
val id: String,
val name: String,
val email: String,
val role: Role,
@Serializable(with = InstantSerializer::class)
val createdAt: Instant,
)
@Serializable
data class CreateUserRequest(
val name: String,
val email: String,
val role: Role = Role.USER,
)
@Serializable
data class ApiResponse<T>(
val success: Boolean,
val data: T? = null,
val error: String? = null,
) {
companion object {
fun <T> ok(data: T): ApiResponse<T> = ApiResponse(success = true, data = data)
fun <T> error(message: String): ApiResponse<T> = ApiResponse(success = false, error = message)
}
}
@Serializable
data class PaginatedResponse<T>(
val data: List<T>,
val total: Long,
val page: Int,
val limit: Int,
)
自定义序列化器 (Custom Serializers)
object InstantSerializer : KSerializer<Instant> {
override val descriptor = PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Instant) =
encoder.encodeString(value.toString())
override fun deserialize(decoder: Decoder): Instant =
Instant.parse(decoder.decodeString())
}
认证 (Authentication)
JWT 认证 (JWT Authentication)
// plugins/Authentication.kt
fun Application.configureAuthentication() {
val jwtSecret = environment.config.property("jwt.secret").getString()
val jwtIssuer = environment.config.property("jwt.issuer").getString()
val jwtAudience = environment.config.property("jwt.audience").getString()
val jwtRealm = environment.config.property("jwt.realm").getString()
install(Authentication) {
jwt("jwt") {
realm = jwtRealm
verifier(
JWT.require(Algorithm.HMAC256(jwtSecret))
.withAudience(jwtAudience)
.withIssuer(jwtIssuer)
.build()
)
validate { credential ->
if (credential.payload.audience.contains(jwtAudience)) {
JWTPrincipal(credential.payload)
} else {
null
}
}
challenge { _, _ ->
call.respond(HttpStatusCode.Unauthorized, ApiResponse.error<Unit>("Invalid or expired token"))
}
}
}
}
// 从 JWT 中提取用户 (Extracting user from JWT)
fun ApplicationCall.userId(): String =
principal<JWTPrincipal>()
?.payload
?.getClaim("userId")
?.asString()
?: throw AuthenticationException("No userId in token")
认证路由 (Auth Routes)
fun Route.authRoutes() {
val authService by inject<AuthService>()
route("/auth") {
post("/login") {
val request = call.receive<LoginRequest>()
val token = authService.login(request.email, request.password)
?: return@post call.respond(
HttpStatusCode.Unauthorized,
ApiResponse.error<Unit>("Invalid credentials"),
)
call.respond(ApiResponse.ok(TokenResponse(token)))
}
post("/register") {
val request = call.receive<RegisterRequest>()
val user = authService.register(request)
call.respond(HttpStatusCode.Created, ApiResponse.ok(user))
}
authenticate("jwt") {
get("/me") {
val userId = call.userId()
val user = authService.getProfile(userId)
call.respond(ApiResponse.ok(user))
}
}
}
}
状态页面(错误处理) (Status Pages (Error Handling))
// plugins/StatusPages.kt
fun Application.configureStatusPages() {
install(StatusPages) {
exception<ContentTransformationException> { call, cause ->
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<Unit>("Invalid request body: ${cause.message}"),
)
}
exception<IllegalArgumentException> { call, cause ->
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<Unit>(cause.message ?: "Bad request"),
)
}
exception<AuthenticationException> { call, _ ->
call.respond(
HttpStatusCode.Unauthorized,
ApiResponse.error<Unit>("Authentication required"),
)
}
exception<AuthorizationException> { call, _ ->
call.respond(
HttpStatusCode.Forbidden,
ApiResponse.error<Unit>("Access denied"),
)
}
exception<NotFoundException> { call, cause ->
call.respond(
HttpStatusCode.NotFound,
ApiResponse.error<Unit>(cause.message ?: "Resource not found"),
)
}
exception<Throwable> { call, cause ->
call.application.log.error("Unhandled exception", cause)
call.respond(
HttpStatusCode.InternalServerError,
ApiResponse.error<Unit>("Internal server error"),
)
}
status(HttpStatusCode.NotFound) { call, status ->
call.respond(status, ApiResponse.error<Unit>("Route not found"))
}
}
}
CORS 配置 (CORS Configuration)
// plugins/CORS.kt
fun Application.configureCORS() {
install(CORS) {
allowHost("localhost:3000")
allowHost("example.com", schemes = listOf("https"))
allowHeader(HttpHeaders.ContentType)
allowHeader(HttpHeaders.Authorization)
allowMethod(HttpMethod.Put)
allowMethod(HttpMethod.Delete)
allowMethod(HttpMethod.Patch)
allowCredentials = true
maxAgeInSeconds = 3600
}
}
Koin 依赖注入 (Koin Dependency Injection)
模块定义 (Module Definition)
// di/AppModule.kt
val appModule = module {
// 数据库 (Database)
single<Database> { DatabaseFactory.create(get()) }
// 仓库 (Repositories)
single<UserRepository> { ExposedUserRepository(get()) }
single<OrderRepository> { ExposedOrderRepository(get()) }
// 服务 (Services)
single { UserService(get()) }
single { OrderService(get(), get()) }
single { AuthService(get(), get()) }
}
// 应用程序设置 (Application setup)
fun Application.configureDI() {
install(Koin) {
modules(appModule)
}
}
在路由中使用 Koin (Using Koin in Routes)
fun Route.userRoutes() {
val userService by inject<UserService>()
route("/users") {
get {
val users = userService.getAll()
call.respond(ApiResponse.ok(users))
}
}
}
Koin 用于测试 (Koin for Testing)
class UserServiceTest : FunSpec(), KoinTest {
override fun extensions() = listOf(KoinExtension(testModule))
private val testModule = module {
single<UserRepository> { mockk() }
single { UserService(get()) }
}
private val repository by inject<UserRepository>()
private val service by inject<UserService>()
init {
test("getUser returns user") {
coEvery { repository.findById("1") } returns testUser
service.getById("1") shouldBe testUser
}
}
}
请求验证 (Request Validation)
// 在路由中验证请求数据 (Validate request data in routes)
fun Route.userRoutes() {
val userService by inject<UserService>()
post("/users") {
val request = call.receive<CreateUserRequest>()
// 验证 (Validate)
require(request.name.isNotBlank()) { "Name is required" }
require(request.name.length <= 100) { "Name must be 100 characters or less" }
require(request.email.matches(Regex(".+@.+\\..+"))) { "Invalid email format" }
val user = userService.create(request)
call.respond(HttpStatusCode.Created, ApiResponse.ok(user))
}
}
// 或者使用验证扩展 (Or use a validation extension)
fun CreateUserRequest.validate() {
require(name.isNotBlank()) { "Name is required" }
require(name.length <= 100) { "Name must be 100 characters or less" }
require(email.matches(Regex(".+@.+\\..+"))) { "Invalid email format" }
}
WebSockets
fun Application.configureWebSockets() {
install(WebSockets) {
pingPeriod = 15.seconds
timeout = 15.seconds
maxFrameSize = 64 * 1024 // 64 KiB — 仅当你的协议需要更大帧时才增加 (increase only if your protocol requires larger frames)
masking = false // 根据 RFC 6455,服务器到客户端的帧不进行掩码;Ktor 中客户端到服务器的帧总是进行掩码 (Server-to-client frames are unmasked per RFC 6455; client-to-server are always masked by Ktor)
}
}
fun Route.chatRoutes() {
val connections = Collections.synchronizedSet<Connection>(LinkedHashSet())
webSocket("/chat") {
val thisConnection = Connection(this)
connections += thisConnection
try {
send("Connected! Users online: ${connections.size}")
for (frame in incoming) {
frame as? Frame.Text ?: continue
val text = frame.readText()
val message = ChatMessage(thisConnection.name, text)
// 在锁下创建快照以避免 ConcurrentModificationException (Snapshot under lock to avoid ConcurrentModificationException)
val snapshot = synchronized(connections) { connections.toList() }
snapshot.forEach { conn ->
conn.session.send(Json.encodeToString(message))
}
}
} catch (e: Exception) {
logger.error("WebSocket error", e)
} finally {
connections -= thisConnection
}
}
}
data class Connection(val session: DefaultWebSocketSession) {
val name: String = "User-${counter.getAndIncrement()}"
companion object {
private val counter = AtomicInteger(0)
}
}
testApplication 测试 (testApplication Testing)
基础路由测试 (Basic Route Testing)
class UserRoutesTest : FunSpec({
test("GET /users returns list of users") {
testApplication {
application {
install(Koin) { modules(testModule) }
configureSerialization()
configureRouting()
}
val response = client.get("/users")
response.status shouldBe HttpStatusCode.OK
val body = response.body<ApiResponse<List<UserResponse>>>()
body.success shouldBe true
body.data.shouldNotBeNull().shouldNotBeEmpty()
}
}
test("POST /users creates a user") {
testApplication {
application {
install(Koin) { modules(testModule) }
configureSerialization()
configureStatusPages()
configureRouting()
}
val client = createClient {
install(io.ktor.client.plugins.contentnegotiation.ContentNegotiation) {
json()
}
}
val response = client.post("/users") {
contentType(ContentType.Application.Json)
setBody(CreateUserRequest("Alice", "alice@example.com"))
}
response.status shouldBe HttpStatusCode.Created
}
}
test("GET /users/{id} returns 404 for unknown id") {
testApplication {
application {
install(Koin) { modules(testModule) }
configureSerialization()
configureStatusPages()
configureRouting()
}
val response = client.get("/users/unknown-id")
response.status shouldBe HttpStatusCode.NotFound
}
}
})
测试认证路由 (Testing Authenticated Routes)
class AuthenticatedRoutesTest : FunSpec({
test("protected route requires JWT") {
testApplication {
application {
install(Koin) { modules(testModule) }
configureSerialization()
configureAuthentication()
configureRouting()
}
val response = client.post("/users") {
contentType(ContentType.Application.Json)
setBody(CreateUserRequest("Alice", "alice@example.com"))
}
response.status shouldBe HttpStatusCode.Unauthorized
}
}
test("protected route succeeds with valid JWT") {
testApplication {
application {
install(Koin) { modules(testModule) }
configureSerialization()
configureAuthentication()
configureRouting()
}
val token = generateTestJWT(userId = "test-user")
val client = createClient {
install(io.ktor.client.plugins.contentnegotiation.ContentNegotiation) { json() }
}
val response = client.post("/users") {
contentType(ContentType.Application.Json)
bearerAuth(token)
setBody(CreateUserRequest("Alice", "alice@example.com"))
}
response.status shouldBe HttpStatusCode.Created
}
}
})
配置 (Configuration)
application.yaml
ktor:
application:
modules:
- com.example.ApplicationKt.module
deployment:
port: 8080
jwt:
secret: ${JWT_SECRET}
issuer: "https://example.com"
audience: "https://example.com/api"
realm: "example"
database:
url: ${DATABASE_URL}
driver: "org.postgresql.Driver"
maxPoolSize: 10
读取配置 (Reading Config)
fun Application.configureDI() {
val dbUrl = environment.config.property("database.url").getString()
val dbDriver = environment.config.property("database.driver").getString()
val maxPoolSize = environment.config.property("database.maxPoolSize").getString().toInt()
install(Koin) {
modules(module {
single { DatabaseConfig(dbUrl, dbDriver, maxPoolSize) }
single { DatabaseFactory.create(get()) }
})
}
}
快速参考:Ktor 模式 (Quick Reference: Ktor Patterns)
| 模式 (Pattern) | 描述 (Description) |
|---|---|
route("/path") { get { } } |
使用 DSL 进行路由分组 (Route grouping with DSL) |
call.receive<T>() |
反序列化请求体 (Deserialize request body) |
call.respond(status, body) |
发送带状态的响应 (Send response with status) |
call.parameters["id"] |
读取路径参数 (Read path parameters) |
call.request.queryParameters["q"] |
读取查询参数 (Read query parameters) |
install(Plugin) { } |
安装并配置插件 (Install and configure plugin) |
authenticate("name") { } |
使用认证保护路由 (Protect routes with auth) |
by inject<T>() |
Koin 依赖注入 (Koin dependency injection) |
testApplication { } |
集成测试 (Integration testing) |
记住 (Remember):Ktor 是围绕 Kotlin 协程和 DSL 设计的。保持路由简洁,将逻辑推送到服务层,并使用 Koin 进行依赖注入。使用 testApplication 进行完整的集成测试覆盖。
📄 原始文档
完整文档(英文):
https://skills.sh/affaan-m/everything-claude-code/kotlin-ktor-patterns
💡 提示:点击上方链接查看 skills.sh 原始英文文档,方便对照翻译。
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。

评论(0)