Ktor踩坑,distributions部署时的资源问题

随着 Ktor 1.1.3 面世,distributions 部署方式也已经正式推出,这是一令人激动的特性,因为终于可以用任意服务器框架并且不再需要自己来配服务器了。

来看一下 distributions 方式的部署过程,假设现在有一个名为 sample 的项目,我们可以如此操作:

$ cd sample
$ gradle build
$ cd build/distributions
$ unzip sample.zip
$ cd sample/bin
$ sudo ./sample &

是不是很简单,其实把 distributions 的 zip 拷到任意目录,都可以用的这样操作方式来部署。

好了,那么下面就有一个问题,原先我们可以使用以下代码来访问一个 static 下的文件,函数如下:

@UseExperimental(KtorExperimentalAPI::class)
fun ApplicationCall.resolveFileContent(
    path: String,
    resourcePackage: String? = null,
    classLoader: ClassLoader = application.environment.classLoader
): String? {
    val packagePath = (resourcePackage?.replace('.', '/') ?: "").appendPathPart(path)
    val normalizedPath = Paths.get(packagePath).normalizeAndRelativize()
    val normalizedResource = normalizedPath.toString().replace(File.separatorChar, '/')
    for (url in classLoader.getResources(normalizedResource).asSequence()) {
        when (url.protocol) {
            "file" -> {
                val file = File(url.path.decodeURLPart())
                return if (file.isFile) file.readText() else null
            }
        }
    }
    return null
}

使用的时候只需要如此调用即可:

val text = call.resolveFileContent("index.html", "static")
println(text)

然而在 distributions 的情况下,采用这种方法来访问文件是不可以的,因为在编译时,所有的资源都被打包到了 jar 内,所以不存在 url.protocolfile 的情况。

那么在这种情况下要怎么办呢,其实解决方法也不难,只要判断是 jar,并且把 jar 里面的文件读出来即可:


@UseExperimental(KtorExperimentalAPI::class)
fun ApplicationCall.resolveFileContent(
    path: String,
    resourcePackage: String? = null,
    classLoader: ClassLoader = application.environment.classLoader
): String? {
    val packagePath = (resourcePackage?.replace('.', '/') ?: "").appendPathPart(path)
    val normalizedPath = Paths.get(packagePath).normalizeAndRelativize()
    val normalizedResource = normalizedPath.toString().replace(File.separatorChar, '/')
    for (url in classLoader.getResources(normalizedResource).asSequence()) {
        when (url.protocol) {
            "file" -> {
                val file = File(url.path.decodeURLPart())
                return if (file.isFile) file.readText() else null
            }
            "jar" -> {
                return if (packagePath.endsWith("/")) {
                    null
                } else {
                    val jar = JarFile(findContainingJarFile(url.toString()))
                    val jarEntry = jar.getJarEntry(normalizedResource)!!
                    val size = jarEntry.size
                    val b = ByteArray(size.toInt())
                    jar.getInputStream(jarEntry).read(b)
                    String(b)
                }
            }
        }
    }
    return null
}

好了,是不是很简单,只是一个标准的从压缩包内读取内容的操作。

似乎是解决了一个问题?但是又引出了另一个问题,比如说我要读 jar 里的图片怎么办?上面的代码其实是有 bug 的,因为:

val size = jarEntry.size
val b = ByteArray(size.toInt())

这里的 jarEntry.size 是 Long 类型,而对于 ByteArray 的声明,只能是 Int 类型,把 Long 强转成 Int 会发生什么,自己想想也知道了。所以当图片很大时,不能采用这种方法去读,那么就得寻找另外的办法了。

能想到的办法自然是先把文件拷出来,用流的方式,然后再想办法去操作,比如说这样:

@UseExperimental(KtorExperimentalAPI::class)
suspend fun ApplicationCall.resolveFileSave(
    dest: File,
    path: String,
    resourcePackage: String? = null,
    classLoader: ClassLoader = application.environment.classLoader
): Boolean {
    var ret = false
    val packagePath = (resourcePackage?.replace('.', '/') ?: "").appendPathPart(path)
    val normalizedPath = Paths.get(packagePath).normalizeAndRelativize()
    val normalizedResource = normalizedPath.toString().replace(File.separatorChar, '/')
    for (url in classLoader.getResources(normalizedResource).asSequence()) {
        when (url.protocol) {
            "file" -> {
                val file = File(url.path.decodeURLPart())
                if (file.isFile) {
                    file.copyTo(dest)
                    ret = true
                }
            }
            "jar" -> {
                if (!packagePath.endsWith("/")) {
                    val jar = JarFile(findContainingJarFile(url.toString()))
                    val jarEntry = jar.getJarEntry(normalizedResource)!!
                    jar.getInputStream(jarEntry).use { input ->
                        dest.outputStream().use { output ->
                            ret = input.copyToSuspend(output) > 0
                        }
                    }
                }
            }
        }
    }
    return ret
}

这样就可以把一个 jar 里的具体文件拷贝出来了,有了一个 File 类型的对象,自然想怎么操作都行啦。

推荐阅读更多精彩内容