如何使用KMongo在Ktor项目中将图片存储到MongoDB集合中
Ktor + KMongo 实现图片存储到MongoDB的实现思路
实现原理说明
MongoDB存储二进制文件有两种主流方案:
- 单文件体积小于16MB时,直接用BSON的Binary字段存储即可,实现最简单
- 单文件体积大于等于16MB时,使用MongoDB原生的GridFS组件存储,KMongo已经封装了GridFS的全量操作API,无需额外引入依赖
前置依赖配置
在build.gradle.kts中添加核心依赖:
// 适配Ktor协程模型的KMongo依赖 implementation("org.litote.kmongo:kmongo-coroutine:4.10.0") // Ktor多部分表单上传支持 implementation("io.ktor:ktor-server-core:$ktorVersion") implementation("io.ktor:ktor-server-content-negotiation:$ktorVersion")
方案1:小体积图片直接存储(<16MB)
1. 定义数据存储实体
import org.litote.kmongo.Id import org.litote.kmongo.newId data class Image( val id: Id<Image> = newId(), val fileName: String, val contentType: String, val size: Long, val content: ByteArray // 存储图片二进制内容 ) { // 重写方法解决ByteArray默认比较的问题 override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false other as Image return id == other.id && content.contentEquals(other.content) } override fun hashCode(): Int { var result = id.hashCode() result = 31 * result + content.contentHashCode() return result } }
2. 实现图片上传接口
import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* import io.ktor.http.content.* import org.litote.kmongo.coroutine.CoroutineDatabase fun Route.imageUploadRoute(db: CoroutineDatabase) { val imageCol = db.getCollection<Image>("images") post("/upload/image") { val multipart = call.receiveMultipart() var saveResult: String? = null multipart.forEachPart { part -> if (part is PartData.FileItem) { val byteContent = part.streamProvider().readBytes() val image = Image( fileName = part.originalFileName ?: "unnamed_image", contentType = part.contentType?.toString() ?: "image/jpeg", size = byteContent.size.toLong(), content = byteContent ) imageCol.insertOne(image) saveResult = image.id.toString() } part.dispose() } saveResult?.let { call.respond(mapOf("code" to 200, "imageId" to it)) return@post } call.respond(mapOf("code" to 400, "msg" to "未读取到有效图片")) } }
3. 实现图片读取接口
get("/image/{id}") { val imageId = call.parameters["id"] ?: return@get call.respond(mapOf("code" to 400, "msg" => "缺少图片ID")) val image = db.getCollection<Image>("images").findById(imageId) ?: return@get call.respond(mapOf("code" to 404, "msg" => "图片不存在")) call.respondBytes(image.content, io.ktor.http.ContentType.parse(image.contentType)) }
方案2:大体积图片GridFS存储(>=16MB)
1. 实现大文件上传接口
post("/upload/large-image") { val multipart = call.receiveMultipart() val gridFs = db.getGridFS() var fileId: String? = null multipart.forEachPart { part -> if (part is PartData.FileItem) { val fileName = part.originalFileName ?: "unnamed_large_image" val contentType = part.contentType?.toString() ?: "image/jpeg" // 直接将流写入GridFS,无需一次性读取全部内容到内存 val id = gridFs.uploadFromStream( fileName, part.streamProvider(), org.bson.Document("contentType", contentType) ) fileId = id.toString() } part.dispose() } fileId?.let { call.respond(mapOf("code" to 200, "fileId" to it)) return@post } call.respond(mapOf("code" to 400, "msg" to "未读取到有效文件")) }
2. 实现大文件读取接口
get("/large-image/{id}") { val fileId = call.parameters["id"] ?: return@get call.respond(mapOf("code" to 400, "msg" => "缺少文件ID")) val gridFs = db.getGridFS() val file = gridFs.find(org.bson.Document("_id", org.bson.types.ObjectId(fileId))).first() ?: return@get call.respond(mapOf("code" to 404, "msg" => "文件不存在")) val outStream = java.io.ByteArrayOutputStream() gridFs.downloadToStream(file.id, outStream) val contentType = file.metadata?.getString("contentType") ?: "image/jpeg" call.respondBytes(outStream.toByteArray(), io.ktor.http.ContentType.parse(contentType)) }
注意事项
- 生产环境需要添加文件格式、大小校验逻辑,避免恶意文件上传
- 高频访问的图片建议加二级缓存,降低MongoDB的读取压力
- 高并发场景下建议GridFS和业务数据库集群物理分离部署
内容的提问来源于stack exchange,提问作者Astro
相关产品推荐
相关产品推荐

