KMM搭建Server初探

背景

前阵子已经用compose开发了一款跨平台app,现在尝试用compose写server代码,学会做一个优秀的demo全栈工程师。

工程配置

MySQL的安装

server的搭建离不开数据存储、api定义主要2个步骤(进阶:高并发、锁机制不在这里提及)。我们电脑首先要下载并安装MySQL,这里安装流程省略,问下AI就会搞了。

Compose核心依赖

这里我是以ktor版本3.3.3为例,kotlin版本为2.3.0为前提做分析

  1. 数据库定义Dao层,主要依赖如下
// Exposed ORM 核心
    implementation("org.jetbrains.exposed:exposed-core:0.58.0")
    implementation("org.jetbrains.exposed:exposed-dao:0.58.0") // DAO 层(可选)
    implementation("org.jetbrains.exposed:exposed-jdbc:0.58.0")
  1. 数据库驱动,主要依赖如下
// MySQL 驱动
    implementation("mysql:mysql-connector-java:8.0.33")
  1. 数据库连接池,主要依赖如下
implementation("com.zaxxer:HikariCP:5.0.1")
  1. 解决跨域问题依赖,主要依赖如下(不明白跨域定义可以看我以前文章有提及)
implementation("io.ktor:ktor-server-cors-jvm:2.3.3")
  1. 客户端发送的json数据到Server反序列化为class的插件,主要依赖如下
implementation("io.ktor:ktor-server-content-negotiation-jvm:3.0.0")
implementation("io.ktor:ktor-serialization-kotlinx-json-jvm:3.0.0")

5.1 这里反序列化还需要在server module的plugin插件声明下,用于编译期间动画生成对应json对象。

plugins {
    //这个是根据Serializable动态编译生成对应的json对象,必须要
    kotlin("plugin.serialization") version "2.3.0"
    alias(libs.plugins.kotlinJvm)
    alias(libs.plugins.ktor)
    application
}

5.2 定义好接收json数据的对象,我这里demo比较简单,所以只定义3个字段。必须用@Serializable修饰接收数据的对象。

import kotlinx.serialization.Serializable

@Serializable
data class UserRequest(
    val name: String = "",
    val parentPhone: String = "",
    val sex: Int = 0
)

代码结构

数据库相关
  1. 定义和数据库表对应结构的class对象,我这里是id自增唯一键对象,所以直接继承IntIdTable而不是继承Table。
object Users : IntIdTable("user_message", "id") { // 第二个参数是列名,默认就是 "id"
    val name = text("name")
    val parentPhone = text("parent_phone")
    val sex = integer("sex")
}
  1. 数据库初始化配置和事务接口封装, jdbcUrl的作用可以自行问AI。
object DatabaseFactory {
    // 初始化数据库连接(调用一次即可,如在 Ktor 启动时)
    fun init() {
        val config = HikariConfig().apply {
            jdbcUrl = "jdbc:mysql://localhost:3306/user_db?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true"
//            driverClassName = "com.mysql.cj.jdbc.Driver"
            username = "root" // 你的 MySQL 用户名
            password = "12345678" // 你的 MySQL 密码
            maximumPoolSize = 10 // 连接池最大连接数
        }
        val dataSource = HikariDataSource(config)
        // 绑定 Exposed 到数据库连接
        Database.connect(dataSource)
    }

    // 封装协程事务(适配 Ktor 协程,推荐)
    suspend fun <T> dbQuery(block: suspend () -> T): T =
        newSuspendedTransaction { block() }

    // 封装普通事务(非协程场景)
    fun <T> dbSyncQuery(block: () -> T): T =
        transaction { block() }
}
  1. 数据库交互Api层封装
class UserDao {
    // 1. 新增数据(存储用户到数据库)
    suspend fun createUser(user: User): Int = DatabaseFactory.dbQuery {
        // 前置校验
        require(user.name.isNotBlank()) { "name 不能为空" }
        require(user.parentPhone.matches(Regex("^1[3-9]\\d{9}$"))) { "手机号格式错误" }

        // 使用 insertAndGetId 直接获取生成的 EntityID
        val generatedId = Users.insertAndGetId { row ->
            row[name] = user.name
            row[parentPhone] = user.parentPhone
            row[sex] = user.sex
        }

        generatedId.value // 返回 Int 值
    }

    // 2. 查询数据
    suspend fun getUserById(id: Int): User? = DatabaseFactory.dbQuery {
        Users.selectAll().where { Users.id eq id }
            .singleOrNull()
            ?.mapToUser()
    }

    // 抽取一个扩展函数,方便重复使用
    private fun ResultRow.mapToUser() = User(
        id = this[Users.id].value, // 注意这里要用 .value
        name = this[Users.name],
        parentPhone = this[Users.parentPhone],
        sex = this[Users.sex]
    )
}
接口定义
  1. kmm通过大多数语法糖高度封装了server相关概念,这里我们只需要知道通过routing 定义相应API的逻辑即可。下面我定义一个插入数据逻辑和一个查询数据逻辑。
fun main() {
    embeddedServer(Netty, port = SERVER_PORT, host = HOST, module = Application::module)
        .start(wait = true)
}

fun Application.module() {
    ……
    DatabaseFactory.init()
    val userDao = UserDao()
    routing {
        // 新增用户接口(POST /user)
        post("/user") {
            val user = call.receive<UserRequest>() // 接收前端传的用户数据
            println("Ktor 收到原始字符串: $user")
            val saveUer = User(null, user.name, user.parentPhone, user.sex)
            val userId = userDao.createUser(saveUer) // 存储到数据库
            call.respondText("用户创建成功,ID:$userId")
        }
        // 查询用户接口(GET /user/{id})
        get("/user/{id}") {
            val id = call.parameters["id"]?.toInt() ?: throw IllegalArgumentException("ID不能为空")
            val user = userDao.getUserById(id)
            if (user != null) {
                call.respond(user) // 返回用户数据
            } else {
                call.respondText("用户不存在", status = HttpStatusCode(404, "参数错误"))
            }
        }
    }
}
  1. 接口调用,这里我让AI帮我写一个H5页面,这里就不拿出来分析了,这里直接贴2个效果图代表下就好了。网络传输这一块我是用同个局域网下的ip直连的,如果要走域名+云服务的话也可以,不过不在这个章节说了。


    查询.png

    插入数据.png

    数据库数据.png

错误分析

  1. POST请求时候提示415问题,原因是依赖添加上了,但是没有在module初始化反序列化插件,添加下面代码即可解决此类问题。
fun Application.module() {
    // 必须安装这个插件,否则 POST 的 JSON 无法被识别 (415 根源)
    install(ContentNegotiation) {
        json(Json {
            prettyPrint = true
            isLenient = true
            ignoreUnknownKeys = true // 建议开启,防止前端多传字段导致报错
            isLenient = true          // 宽容模式,允许不规范的 JSON(如 key 没加引号)
            coerceInputValues = true // 强制转换输入值(非常有用!)
        })
    }
}

总结

  1. kmm把server复杂的概念用语法糖直接进行优化,让搭建server变得越来越简单,这里非常合适写自己的毕业设计或者做自己小型项目研发,毕竟成本非常低。
  2. 后续会研究kmm如何保证数据的安全性这方向,现在通过kmm简单实现了server后,我可以把我之前双端的app接入到自己开发的server上了😄😄。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容