Ktor框架下application.yaml引用环境变量时出现Char sequence is empty异常的问题
我之前帮同事排查过几乎一模一样的问题,一下子就认出这个坑了!咱们来拆解下问题根源和解决办法:
问题核心:混淆了「环境变量」和「系统属性」,Ktor解析YAML占位符的逻辑和你想的不一样
首先看你的错误栈,报错来自Ktor的YamlConfig.resolveValue,说明在解析application.yaml里的${PORT}时,Ktor找不到对应的值,拿到了空字符串才抛出这个异常。但你代码里用System.getenv("PORT")能读到值,这是因为:
- Ktor的YAML配置解析器默认是从「系统属性(System.getProperty)」里读取
${VAR}占位符的值,而不是「环境变量(System.getenv)」! - 你用
export PORT=xxx或者IntelliJ的「Environment Variables」配置的是环境变量,Ktor解析YAML时根本不会读这些,自然拿到空值报错。 - 你用
java -DPORT=xxx -jar能正常运行,是因为-D参数设置的是系统属性,刚好匹配Ktor的默认解析逻辑。
另外还有个隐形坑:你代码里已经手动指定了port和host,但Ktor启动时还是会自动加载application.yaml并解析其中的占位符——哪怕你代码里配置了端口,YAML里的${PORT}还是会被解析,这就是为什么明明代码能读到变量,却还是报错。
给你几个简单可行的解决办法
办法1:绕开YAML占位符,直接用代码控制配置(最省心)
既然你已经在代码里实现了环境变量读取和默认值逻辑,那可以让Ktor不要加载application.yaml的配置,彻底避免解析占位符的问题:
修改你的main函数,在启动服务器时指定禁用默认配置加载:
fun main(args: Array<String>) { val port = System.getenv("PORT")?.toIntOrNull() ?: 8081 embeddedServer(Netty, port = port, host = "0.0.0.0", configure = { // 禁用默认配置文件加载,避免解析YAML里的${PORT} config = EmptyConfig }) { module() }.start(wait = true) }
这样既保留了你代码里的灵活配置,又不会触发Ktor的YAML解析报错,而且硬编码YAML的方式也可以保留(如果需要的话,但这个配置已经不会被加载了)。
办法2:统一用「系统属性」替代环境变量
如果想保留application.yaml的配置方式,那把环境变量转成Ktor默认读取的系统属性就行:
- 终端运行时:用
java -DPORT=8081 -jar your-app.jar,或者如果已经设置了环境变量,用java -DPORT=$PORT -jar your-app.jar把环境变量转成系统属性。 - IntelliJ配置时:不要在「Environment Variables」里加PORT,而是在「VM options」里添加
-DPORT=8081。
这样YAML里的${PORT}就能被Ktor正常解析,不会再报错。
办法3:让Ktor支持从环境变量读取占位符(适合需要保留YAML配置的复杂场景)
如果一定要用环境变量给YAML占位符赋值,可以自定义Ktor的配置解析逻辑,让它优先读环境变量,再读系统属性,甚至支持默认值。比如写个自定义的ConfigFactory:
import io.ktor.server.config.* import io.ktor.server.config.yaml.YamlConfig import java.io.File fun main(args: Array<String>) { val customConfigFactory = object : ConfigFactory { override fun load(): ApplicationConfig { // 加载application.yaml文件 val yamlConfig = YamlConfig(File("src/main/resources/application.yaml").inputStream()) // 自定义占位符解析逻辑:支持${VAR}和${VAR:default} return object : ApplicationConfig by yamlConfig { override fun property(path: String): ApplicationConfigValue { val rawValue = yamlConfig.property(path).getString() // 正则匹配占位符,解析变量名和默认值 val resolvedValue = rawValue.replace(Regex("\\$\\{([^:}]+)(?::([^}]+))?}")) { matchResult -> val varName = matchResult.groupValues[1] val defaultValue = matchResult.groupValues.getOrNull(2) // 优先读环境变量,再读系统属性,最后用默认值 System.getenv(varName) ?: System.getProperty(varName) ?: defaultValue ?: throw IllegalArgumentException("Missing value for configuration variable: $varName") } return object : ApplicationConfigValue { override fun getString(): String = resolvedValue override fun getList(): List<String> = listOf(resolvedValue) override fun getProperty(subPath: String): ApplicationConfigValue = throw MissingApplicationConfigException(subPath) } } } } } // 用自定义配置创建环境并启动服务器 val environment = applicationEngineEnvironment { config = customConfigFactory.load() module { module() } connector { port = config.property("ktor.deployment.port").getString().toInt() host = config.property("ktor.deployment.host").getString() } } embeddedServer(Netty, environment).start(wait = true) }
这样你就能在YAML里写port: ${PORT:8081},既支持环境变量,又有默认值,不用再担心空值报错。
最后再验证下
你可以先试试办法1,因为改动最小,完全匹配你现在的代码逻辑,应该能立刻解决问题。如果需要保留YAML配置,再考虑办法2或3。
内容来源于stack exchange

