9、中间件与拦截器:自定义插件、请求日志、异常处理、CORS配置、速率限制
说到Ktor的中间件,我脑子里第一个蹦出来的词就是「管道」。Ktor整个请求处理流程,说白了就是一条管道。请求进来,经过一层层处理,最后到达你的业务代码。每一层都可以做点事情——记录日志、检查权限、转换数据、甚至直接掐断请求。
我个人习惯把中间件叫做「插件」,因为Ktor里就是这么命名的。你写一个类实现ApplicationPlugin,或者直接用install方法挂载官方提供的插件。今天我们就来聊聊怎么自己写插件,以及几个最常用的中间件场景。
9.1 自定义插件:从零开始写一个中间件
先看一个最简单的自定义插件。假设我想在每个请求处理前后打印一条日志:
class MyLoggingPlugin : ApplicationPlugin<Unit> {
override val key = AttributeKey<MyLoggingPlugin>("MyLoggingPlugin")
override fun install(pipeline: Application, configure: Unit.() -> Unit) {
pipeline.intercept(ApplicationCallPipeline.Monitoring) {
println("请求进来了: ${call.request.uri}")
try {
proceed()
} finally {
println("请求处理完毕: ${call.request.uri}")
}
}
}
}
fun Application.myLogging() {
install(MyLoggingPlugin)
}
这里要注意ApplicationCallPipeline.Monitoring这个阶段。Ktor把请求处理分成了几个阶段:Setup、Monitoring、Call、Fallback。我一般把日志、统计这类「只观察不干预」的逻辑放在Monitoring阶段。如果你要修改请求或响应,那就得用Call阶段。
AttributeKey的泛型参数可以是你自己定义的配置类。这样就能像官方插件一样,在install时传入配置参数。
9.2 请求日志:别用println,用Logger
刚才的例子用了println,生产环境可不能这么干。Ktor自带了CallLogging插件,配置一下就能用:
install(CallLogging) {
level = Level.INFO
filter { call ->
call.request.uri.startsWith("/api")
}
format { call ->
"${call.request.httpMethod} ${call.request.uri} - ${call.response.status()}"
}
}
我在项目中遇到过一个问题:日志里全是静态资源的请求,把真正的API日志淹没了。所以加了个filter,只记录/api开头的请求。另外,format可以自定义输出格式,我习惯把响应状态码也带上,方便排查问题。
9.3 异常处理:统一返回错误格式
我曾经接手过一个项目,每个接口的错误格式都不一样。有的返回{ "error": "xxx" },有的返回{ "message": "xxx" },前端对接的时候苦不堪言。后来我用StatusPages插件统一处理:
install(StatusPages) {
exception<Throwable> { call, cause ->
val status = when (cause) {
is BadRequestException -> HttpStatusCode.BadRequest
is NotFoundException -> HttpStatusCode.NotFound
else -> HttpStatusCode.InternalServerError
}
call.respond(status, mapOf(
"code" to status.value,
"message" to (cause.message ?: "未知错误")
))
}
}
这里有个坑:exception<Throwable>会捕获所有异常,包括KotlinNullPointerException这种。我建议你按具体异常类型分开处理,比如先处理BadRequestException,再处理NotFoundException,最后兜底用Throwable。顺序很重要,Ktor会匹配第一个符合条件的处理器。
9.4 CORS配置:别让前端骂娘
CORS(跨域资源共享)这个问题,说白了就是浏览器的一种安全机制。你的前端跑在localhost:3000,后端跑在localhost:8080,浏览器就会拦截跨域请求。Ktor的CORS插件配置起来很简单:
install(CORS) {
allowHost("localhost:3000")
allowHost("myapp.com", schemes = listOf("https"))
allowMethod(HttpMethod.Options)
allowHeader(HttpHeaders.ContentType)
allowHeader(HttpHeaders.Authorization)
allowCredentials = true
maxAge = Duration.ofHours(1)
}
嗯,这里要注意几点:
allowCredentials = true表示允许携带Cookie。如果你用了这个,allowHost就不能用通配符*,必须明确指定域名。maxAge是预检请求(OPTIONS)的缓存时间。设长一点能减少网络开销,我一般设1小时。- 如果你用了自定义Header,记得在
allowHeader里加上,否则前端会报错。
allowHost写成了allowHost("*.myapp.com"),结果通配符只支持一级,sub.sub.myapp.com就不行。后来改成了allowHost("myapp.com", subDomains = listOf("sub"))才解决。
9.5 速率限制:保护你的API
速率限制(Rate Limiting)是防止API被滥用的重要手段。Ktor官方没有内置这个插件,但我们可以自己写一个简单的实现。思路是用一个Map记录每个IP的请求次数和时间:
class RateLimiter(
private val maxRequests: Int = 100,
private val windowMs: Long = 60_000
) {
private val requests = mutableMapOf<String, MutableList<Long>>()
fun isAllowed(ip: String): Boolean {
val now = System.currentTimeMillis()
val timestamps = requests.getOrPut(ip) { mutableListOf() }
// 移除窗口外的记录
timestamps.removeAll { it < now - windowMs }
return if (timestamps.size < maxRequests) {
timestamps.add(now)
true
} else {
false
}
}
}
然后在插件里调用:
install(RateLimitingPlugin) {
maxRequests = 100
windowMs = 60_000
// 超过限制时返回429
onExceed = { call ->
call.respond(HttpStatusCode.TooManyRequests, "请求过于频繁,请稍后再试")
}
}
X-RateLimit-Remaining,让客户端知道还剩多少次。
9.6 知识体系总览
下面这张图总结了本章的核心内容,你可以把它当作一个速查表:
9.7 总结
中间件和拦截器是Ktor框架的灵魂。你想想看,如果没有它们,每个接口都要自己写日志、自己处理异常、自己判断跨域,那代码得多臃肿。Ktor的插件机制让我们可以把这些横切关注点(cross-cutting concerns)集中管理,代码既干净又容易维护。
我个人建议,在项目初期就把这些基础插件搭好。别等到上线了才发现日志没配、CORS报错、异常信息泄露了敏感数据。嗯,这些都是我踩过的坑,希望你不用再踩一遍。
公众号:蓝海资料掘金营,微信deep3321