配置管理:HOCON、Typesafe Config、环境变量、配置文件加密
配置管理这事儿,说大不大,说小不小。我见过太多项目,代码写得漂漂亮亮,结果配置一团糟——硬编码的数据库密码、散落在各处的环境变量、还有那永远对不上的测试环境配置。嗯,今天咱们就来聊聊,怎么把配置管理这件事做得体面。
为什么需要一套配置管理方案?
你想想看,一个正经的 Kotlin 项目,至少得面对开发、测试、预发布、生产四个环境。每个环境的数据库地址不一样,Redis 密码不一样,日志级别也不一样。如果全靠手动改代码来切换环境,那迟早要出事。
我个人习惯用 Typesafe Config 这套方案。它基于 HOCON 格式,说白了就是 Human-Optimized Config Object Notation——人类友好的配置格式。比 JSON 更灵活,比 Properties 更强大。
HOCON 基础语法
先看一个最简单的例子:
// application.conf
app {
name = "Kotlin实战项目"
version = "1.0.0"
port = 8080
database {
host = "localhost"
port = 5432
name = "kotlin_demo"
}
features {
enableCache = true
maxRetries = 3
}
}
HOCON 有几个我很喜欢的特性:
- 省略引号:键和字符串值可以不加引号,清爽很多
- 层级嵌套:用点号或缩进都能表达层级关系
- 引用替换:可以引用其他配置值,避免重复
- 文件包含:支持 include 其他配置文件
核心要点:HOCON 的引用替换功能非常实用。比如你可以定义一个基础配置,然后在不同环境里覆盖特定字段。
Typesafe Config 实战
在 Kotlin 项目里引入 Typesafe Config 很简单,加个依赖就行:
// build.gradle.kts
implementation("com.typesafe:config:1.4.2")
然后就可以在代码里读取配置了:
import com.typesafe.config.ConfigFactory
fun main() {
val config = ConfigFactory.load()
val appName = config.getString("app.name")
val dbHost = config.getString("app.database.host")
val cacheEnabled = config.getBoolean("app.features.enableCache")
println("应用名称: $appName")
println("数据库地址: $dbHost")
println("缓存开启: $cacheEnabled")
}
我在项目中遇到过一个问题:默认的 ConfigFactory.load() 会按固定顺序查找配置文件——先找 application.conf,再找 application.json,最后是 application.properties。如果你同时存在多个格式的文件,可能会产生意想不到的覆盖行为。
我的建议:统一使用 HOCON 格式,不要混用。团队里约定好只用 .conf 文件,省去很多麻烦。
环境变量集成
生产环境里,敏感信息绝对不能硬编码在配置文件里。Typesafe Config 天然支持环境变量覆盖,你只需要在配置里这么写:
// application.conf
app {
database {
// 优先读取环境变量,如果没有则使用默认值
password = ${?DB_PASSWORD}
password = "dev_password"
}
}
这里有个小技巧:${?DB_PASSWORD} 的意思是「如果环境变量 DB_PASSWORD 存在就用它,否则忽略」。然后下一行设置默认值。这样组合起来的效果就是:环境变量优先,没有环境变量就用默认值。
我曾经踩过一个坑:环境变量名里带点号。比如 app.database.password 这种命名,在 Linux 系统里是合法的,但在 Windows 上就不行。后来我统一改用下划线,比如 APP_DATABASE_PASSWORD,跨平台就稳了。
注意:Typesafe Config 读取环境变量时,默认会把点号替换成下划线。所以配置里的 ${?APP_DATABASE_PASSWORD} 会匹配环境变量 APP_DATABASE_PASSWORD,而不是 APP.DATABASE.PASSWORD。
多环境配置策略
实际项目中,我通常这样组织配置文件:
src/main/resources/
├── application.conf # 公共配置
├── application-dev.conf # 开发环境
├── application-test.conf # 测试环境
└── application-prod.conf # 生产环境
然后在启动时指定环境:
// 通过 JVM 参数指定
java -Dconfig.resource=application-dev.conf -jar app.jar
// 或者通过环境变量
export CONFIG_RESOURCE=application-prod.conf
公共配置里放所有环境都一样的值,环境配置里只放差异项。这样维护起来很清晰。
配置文件加密
说到敏感信息,密码、密钥这些总不能明文存着吧?我常用的方案是结合 Jasypt 来做配置加密。
先引入依赖:
implementation("org.jasypt:jasypt:1.9.3")
然后在配置里这样写:
// application.conf
app {
database {
password = "ENC(加密后的密文)"
}
}
代码里解密:
import org.jasypt.encryption.pbe.StandardPBEStringEncryptor
fun decryptConfig(encryptedValue: String, masterKey: String): String {
val encryptor = StandardPBEStringEncryptor().apply {
setPassword(masterKey)
setAlgorithm("PBEWithMD5AndDES")
}
return encryptor.decrypt(encryptedValue.removePrefix("ENC(").removeSuffix(")"))
}
关键点:主密钥(masterKey)绝对不能写在配置文件里。我一般通过环境变量传入,或者从专门的密钥管理服务获取。
知识体系总览
下面这张图概括了配置管理的核心脉络:
避坑指南
最后分享几个我踩过的坑:
- 我曾经把数据库密码直接提交到 Git 仓库——还好那是内部测试库,不然就出大事了。从那以后,我强制团队所有配置文件里的敏感字段都必须加密。
- 我曾经被配置覆盖顺序坑过:Typesafe Config 的加载顺序是:系统属性 > 环境变量 > application.conf。如果你在系统属性里设了一个值,它会覆盖配置文件里的同名值。这个优先级一定要记清楚。
- 我曾经在 Docker 容器里忘记传环境变量——结果应用启动时用了默认的 dev 配置,连到了开发数据库。后来我加了一个启动检查,如果检测到生产环境但某些关键配置还是默认值,就直接报错退出。
一个小建议:写一个 ConfigValidator 类,在应用启动时校验所有必要配置项是否都已正确加载。这样能提前发现问题,而不是等到运行时才报错。
配置管理这件事,说白了就是「把变化的东西和不变的东西分开」。HOCON + Typesafe Config + 环境变量 + 加密,这套组合拳打下来,基本能覆盖 90% 的场景。剩下的 10%,等你真正遇到了再说吧——毕竟,过度设计也是种病。
公众号:蓝海资料掘金营,微信deep3321