Ktor踩坑,distributions部署时的资源问题
2019-04-29 本文已影响15人
何晓杰Dev
随着 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.protocol
为 file
的情况。
那么在这种情况下要怎么办呢,其实解决方法也不难,只要判断是 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
类型的对象,自然想怎么操作都行啦。