背景
前阵子已经用compose开发了一款跨平台app,现在尝试用compose写server代码,学会做一个优秀的demo全栈工程师。
工程配置
MySQL的安装
server的搭建离不开数据存储、api定义主要2个步骤(进阶:高并发、锁机制不在这里提及)。我们电脑首先要下载并安装MySQL,这里安装流程省略,问下AI就会搞了。
Compose核心依赖
这里我是以ktor版本3.3.3为例,kotlin版本为2.3.0为前提做分析
- 数据库定义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")
- 数据库驱动,主要依赖如下
// MySQL 驱动
implementation("mysql:mysql-connector-java:8.0.33")
- 数据库连接池,主要依赖如下
implementation("com.zaxxer:HikariCP:5.0.1")
- 解决跨域问题依赖,主要依赖如下(不明白跨域定义可以看我以前文章有提及)
implementation("io.ktor:ktor-server-cors-jvm:2.3.3")
- 客户端发送的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
)
代码结构
数据库相关
- 定义和数据库表对应结构的class对象,我这里是id自增唯一键对象,所以直接继承IntIdTable而不是继承Table。
object Users : IntIdTable("user_message", "id") { // 第二个参数是列名,默认就是 "id"
val name = text("name")
val parentPhone = text("parent_phone")
val sex = integer("sex")
}
- 数据库初始化配置和事务接口封装, 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() }
}
- 数据库交互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]
)
}
接口定义
- 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, "参数错误"))
}
}
}
}
-
接口调用,这里我让AI帮我写一个H5页面,这里就不拿出来分析了,这里直接贴2个效果图代表下就好了。网络传输这一块我是用同个局域网下的ip直连的,如果要走域名+云服务的话也可以,不过不在这个章节说了。
查询.png
插入数据.png
数据库数据.png
错误分析
- 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 // 强制转换输入值(非常有用!)
})
}
}
总结
- kmm把server复杂的概念用语法糖直接进行优化,让搭建server变得越来越简单,这里非常合适写自己的毕业设计或者做自己小型项目研发,毕竟成本非常低。
- 后续会研究kmm如何保证数据的安全性这方向,现在通过kmm简单实现了server后,我可以把我之前双端的app接入到自己开发的server上了😄😄。


