配置管理:HOCON配置文件、环境变量、多环境配置、配置热加载
配置管理这件事,说大不大,说小不小。我见过不少项目,代码写得挺漂亮,结果配置一团糟——开发环境连的是生产数据库,测试环境用的密钥直接硬编码在代码里。嗯,这种坑我踩过不止一次。
今天我们就来聊聊Ktor项目里怎么把配置管好。说白了,就是让同一个代码包,在不同环境里能自动适配,而且改配置不用重新打包部署。
HOCON配置文件:Ktor的默认选择
Ktor默认使用HOCON(Human-Optimized Config Object Notation)格式。如果你用过Lightbend的Config库,应该不陌生。它的语法比JSON简洁,支持继承、引用、环境变量替换。
一个典型的application.conf长这样:
ktor {
deployment {
port = 8080
port = ${?PORT} // 环境变量覆盖
}
application {
modules = [ com.example.ApplicationKt.module ]
}
}
database {
url = "jdbc:postgresql://localhost:5432/myapp"
url = ${?DATABASE_URL}
pool {
maxSize = 10
connectionTimeout = 30000
}
}
redis {
host = "localhost"
host = ${?REDIS_HOST}
port = 6379
}
注意那个${?PORT}的写法。问号表示「如果环境变量存在则覆盖,不存在则用默认值」。我个人习惯把所有敏感信息都用这种方式处理,避免把密码写死在配置文件里。
环境变量:运行时动态注入
为什么需要环境变量?你想想看,代码要部署到开发、测试、生产三个环境,数据库地址肯定不一样。如果每次部署都改配置文件,那运维同学会疯掉的。
Ktor读取环境变量的方式很灵活。除了刚才说的${?VAR}语法,你还可以在代码里直接获取:
val dbUrl = System.getenv("DATABASE_URL") ?: "jdbc:postgresql://localhost:5432/default"
不过我不建议到处写System.getenv()。更好的做法是统一在配置文件中定义好,然后通过依赖注入或者配置对象来访问。
我在项目中遇到过一个问题:环境变量名拼写错误,导致应用一直用默认配置连接本地数据库,上线后才发现。从那以后,我养成了一个习惯——在应用启动时打印关键配置项,方便排查。
多环境配置:开发、测试、生产各司其职
多环境配置的核心思路是「共用基础配置,按需覆盖」。Ktor/HOCON支持通过include指令实现:
// application.conf(基础配置)
include "common.conf"
// 根据环境变量加载不同配置
include required(classpath("application.${?ENV}.conf"))
然后你准备三个文件:
application.dev.conf:开发环境,日志级别DEBUG,连接本地数据库application.test.conf:测试环境,使用内存数据库,开启Mock服务application.prod.conf:生产环境,连接池调大,开启监控
启动时指定环境变量即可:
ENV=prod java -jar myapp.jar
我个人习惯在application.conf里放所有环境的默认值,然后每个环境的配置文件只写差异部分。这样即使忘记指定ENV,也能用默认配置跑起来,不至于直接报错。
| 配置项 | 开发环境 | 测试环境 | 生产环境 |
|---|---|---|---|
| 数据库地址 | localhost:5432 | test-db:5432 | prod-db:5432 |
| 日志级别 | DEBUG | INFO | WARN |
| 连接池大小 | 5 | 10 | 50 |
| 缓存TTL | 10秒 | 60秒 | 300秒 |
配置热加载:不改代码,不停服务
配置热加载是个很实用的功能。想象一下:线上某个限流阈值需要调整,你不想重启服务(因为重启意味着连接断开、缓存失效),这时候热加载就派上用场了。
Ktor本身不直接提供配置热加载,但我们可以结合Kotlin的协程和文件监控来实现。我的做法是这样的:
fun Application.configureHotReload() {
val configFile = File("application.conf")
val watcher = FileSystems.getDefault().newWatchService()
configFile.parentFile.toPath().register(
watcher,
StandardWatchEventKinds.ENTRY_MODIFY
)
launch(Dispatchers.IO) {
while (isActive) {
val key = watcher.take()
key.pollEvents().forEach { event ->
if (event.context().toString() == configFile.name) {
log.info("配置文件变更,重新加载...")
// 重新加载配置的逻辑
reloadConfig()
}
}
key.reset()
}
}
}
当然,这只是一个简化版本。实际项目中我还会加上防抖处理——文件保存时可能会触发多次修改事件,需要合并成一次重新加载。
知识体系总览
下面这张图概括了配置管理的整体结构,你可以对照着梳理自己的项目:
配置管理看起来琐碎,但做好了能省很多事。我见过太多项目因为配置混乱导致线上事故——数据库连错、缓存穿透、日志爆炸。花半小时把配置体系搭好,比事后排查问题划算得多。
最后说一句:配置也是代码,需要版本管理、需要评审、需要测试。别把它当成「随便改改就行」的东西。