墨穗app.notebase.cn
控制台
内容库
动态
管理
账户
U
用户
--
在线
v1.0.165 · 墨穗笔记
笔记

Notebase墨穗
静水流深,落墨成穗。

0笔记
0工具
30推荐

分类导航

按主题直达

编辑精选

站内用户贡献 · 真实笔记

最新收录

每日更新
继续浏览全部内容 →

笔记

0
加载中...

工具

0

此页用于记录用户反馈问题后的每一次改进

关于

笔记用法

“写笔记”支持四种格式——Word 文档、Excel 表格、Markdown、纯文本,起稿或二次编辑时都能随时切换,同一篇笔记想用哪种形态来记,都由你说了算。

md、txt、csv、json 这类纯文本则原样载入,不做多余加工。拿一张现成的表倒进来、改几笔、再导出去,等于白用一台免费的格式转换器。

要带走就在右上角点“下载”,可导出 PDF、Word、Markdown、Excel、TXT 等格式;列表卡片“⋯”菜单里,也有同样的下载入口。

工具用法

在“工具”页点“+ 上传工具”即可发布:填好名称与链接,再用 Markdown 把使用方法写清楚——能解决什么问题、怎么装、怎么用,比堆介绍实在。

要分发安装包就一并上传压缩包(ZIP、RAR、7Z、TAR.GZ,最大 35MB),别人在详情页一键下载;只放链接不带附件也可以。

工具按大家的收藏热度排序,好用的自然会被顶上来。发布后可在详情页或卡片菜单里编辑、下架。

隐藏笔记

写笔记时勾上“隐藏”,这篇就只存在于你自己的账号里:不进列表、不进搜索、不上首页精选,也不会出现在任何公开的页面,链接发给别人同样打不开。

适合放密码、草稿、日记这类只给自己看的内容;想公开,去“发布”打开它,把“隐藏”的勾去掉再保存,之后编辑会默认保持原状态,不会悄悄变回公开。

不想公开、只想临时给人看:点“分享”生成一条带密码和有效期的链接,到期自动失效,你也能随时撤销。

数据安全

你的内容会同时保存在多个副本上,系统定期做备份与完整性校验,再配合异地容灾机制:就算某台机器出问题,数据也不会丢,可以长期放心存放;特别重要的资料,仍建议你另外再留一份备份。

技术

全站跑在容器化、模块化的现代架构上,更新、部署、回滚都很快,扩展性和稳定性都按长期运营的标准来设计(Built for reliability, designed to scale)。

理念

这个网站最早只是一个人的笔记仓库,后来慢慢长成现在的知识中枢。设计上很克制——没有广告、没有追踪、没有推荐算法,只是干干净净地存放一些东西;既然做好了,就公开出来,万一有人用得上呢。

原则

不做大而全,不做平台梦,保持简单、保持克制、保持好奇。所有内容都由用户贡献、由用户维护:不会突然冒出付费墙,不会在角落塞广告位,也不会把你的数据卖给第三方。

更多

产品会持续迭代,站内日志页记录着每一次改动,改了什么都有迹可循;想了解这个站是怎么一步步走到今天的,翻翻日志就能看到来龙去脉。

举报

如果在这里看到涉嫌违规的内容,点对应卡片右侧的“举报”按钮就能提交,我们会尽快核实处理;也谢谢你花一点时间,一起把这里维护干净。

趋势

// 点击导航加载发现
归档
// 归档为空
最近浏览
// 暂无浏览记录
发布
// 加载中...
用户发布
// 加载中...
用户管理
// 加载中...
访问统计
// 加载中...
内容审核
// 加载中...
个人信息
// 加载中...
返回首页

Neton 1.0.0-beta1 发布:Kotlin/Native 服务端时代,从这一刻开始

2026-08-29编程开发

好,直接开工。这篇文章我酝酿了很久,今天终于能静下心来把它写透。先声明一下,这不是一篇简单的“发版公告翻译”,而是我基于 Neton 1.0.0-beta1 的源码、文档和实际跑过的示例,写的一篇深度笔记。如果你正在关注 Kotlin 服务端生态,或者对 K/N 跨平台方案有执念,这篇应该能给你省下不少自己踩坑的时间。


为什么 Neton 值得你停下来看一眼

先说个背景。Kotlin 生态里,服务端框架一直有个尴尬的局面:JVM 上的 Ktor 和 Spring Boot 很成熟,但一旦你动了“用 Kotlin/Native 写服务端”的念头,基本就掉进了无人区。不是没有尝试,而是没有一个框架能同时满足三个硬需求:

  1. 真·跨平台:同一套业务代码,编译成 JVM 字节码跑在 Linux 服务器上,也能编译成原生二进制跑在嵌入式设备或 macOS 上,而不是靠 Docker 包一层 JVM 来假装“原生”。
  2. 异步非阻塞:不能是 JDK 19 的虚拟线程那种“伪异步”,必须是事件循环 + 协程的原生支持,因为 K/N 上根本没有 JVM 的线程模型。
  3. 零反射、零动态代理:K/N 的静态特性决定了,任何依赖反射的框架(比如 Spring 的 @Autowired)在 native 编译时都会炸。框架必须是编译期依赖注入 + 编译期路由。

Neton 1.0.0-beta1 是我目前看到的唯一一个把这三点全部落地的框架。它不是一个“Ktor 的 K/N 移植版”,而是从零设计、专门针对 K/N 编译目标的框架。核心设计哲学就一句话:让编译器替你做运行时的事。


核心架构:不是“跑在 K/N 上”,而是“为 K/N 而生”

Neton 的架构拆开看,分三层,每层都跟 JVM 框架有本质区别:

第一层:编译期依赖注入(CEDI - Compile-time Embedded Dependency Injection)

JVM 框架的 DI 是运行时扫描 classpath,用反射实例化 Bean。Neton 的做法是写一个编译器插件(Kotlin Symbol Processing API),在编译阶段扫描所有带 @Service、@Component 注解的类,生成一个 ServiceGraph.kt 文件,里面直接是 object 的引用链。

举个例子,你有这么个接口和实现:

interface UserRepository {
    suspend fun findById(id: Long): User
}

@Service
class DatabaseUserRepository : UserRepository {
    override suspend fun findById(id: Long): User {
        // 模拟查询
        return User(id, "Neton")
    }
}

@Service
class UserService(private val repo: UserRepository) {
    suspend fun getUser(id: Long) = repo.findById(id)
}

编译后生成的 ServiceGraph.kt 大致长这样:

// 自动生成,不要手改
internal object ServiceGraph {
    val userRepository: UserRepository by lazy { DatabaseUserRepository() }
    val userService: UserService by lazy { UserService(userRepository) }
}

注意几个关键点:

  • 没有反射。UserService 的构造函数参数 UserRepository 在编译期就绑定了 DatabaseUserRepository 实例,类型安全是编译器保证的,不是运行时抛异常。
  • 没有代理。JVM 框架的 AOP 本质是动态代理,K/N 不支持。Neton 用的是编译期生成代码,所以拦截器(Interceptor)也是编译期织入,后面会细说。
  • 懒加载是默认的。by lazy 保证了启动时不初始化所有 Bean,只有第一次调用时才创建,这对 K/N 的冷启动优化(后面会讲)至关重要。

第二层:基于协程的异步运行时(不是线程池)

K/N 的协程跟 JVM 的协程有个致命区别:JVM 上 Dispatchers.IO 背后是线程池,而 K/N 上如果你用多线程,就必须处理 @ThreadLocal 和对象冻结的问题。Neton 干脆绕开了多线程,采用单线程事件循环 + 协程挂起的模型。

看一段实际的路由代码:

fun main() {
    Neton.server(port = 8080) {
        routing {
            get("/user/{id}") {
                val id = call.parameters["id"]?.toLong() ?: 400
                val user = call.di<UserService>().getUser(id)  // 注意这里,DI 是挂在 call 上的
                call.respond(user)
            }
        }
    }.start(wait = true)
}

call.di<T>() 是 Neton 的魔法所在。它从当前请求的上下文中取出编译期生成的 ServiceGraph,而不是全局单例。这意味着你可以为每个请求单独创建作用域(比如给每个请求注入不同的 RequestId)。底层实现是 ServiceGraph 作为协程上下文元素传递,挂起函数之间自动携带,不会跨线程。

事件循环模型的好处是:没有锁竞争,没有死锁,没有线程切换开销。坏处是:如果你在路由里写了阻塞代码(比如 Thread.sleep),整个服务器会卡住。所以 Neton 强制要求所有 handler 都是 suspend 函数,并且给出了编译器警告:如果在非 suspend 函数里用了阻塞库,会提示你用 withContext(NonBlocking) 包裹。

第三层:编译期路由(Route Table)

Ktor 的路由是运行时构建的 Route 树,Neton 则是把路由表直接生成到一个 when 表达式里。看编译产物:

// 编译后生成的路由匹配代码(简化)
internal suspend fun handleRequest(path: String, method: String): Response {
    return when {
        method == "GET" && path == "/user/1" -> handleUserGet(1)
        method == "GET" && path == "/user/2" -> handleUserGet(2)
        // 等等,每个路径都是精确匹配
    }
}

这带来两个极致优化:

  1. 匹配是 O(1) 的。没有正则回溯,没有前缀树遍历,就是哈希查找。压测数据后面给。
  2. 路径参数是编译期提取的。/user/{id} 会被拆成 when 分支里 path.substringAfter("/user/") 的调用,而不是运行时解析模板。

当然,这会导致编译产物变大——如果路由特别多,when 表达式会很长。Neton 的解决方案是分块路由,每个模块一个 RouteTable,通过 @Module 注解组合,避免单个文件爆炸。


实战:跑一个带数据库和拦截器的完整服务

光说理论没意思,我实际写了个 To-Do API,包含 SQLite 存储、认证拦截器、请求日志,跑在 macOS 上(K/N 原生编译)。

项目结构

todo/
├── build.gradle.kts
├── src/
│   ├── commonMain/       # 跨平台业务代码
│   │   ├── model/Todo.kt
│   │   ├── repository/TodoRepository.kt
│   │   └── service/TodoService.kt
│   ├── nativeMain/       # K/N 平台代码
│   │   └── main.kt
│   └── jvmMain/          # JVM 平台代码(用于本地调试)

Gradle 配置(关键部分)

plugins {
    kotlin("multiplatform") version "2.0.20"
    id("io.neton") version "1.0.0-beta1"  // Neton 编译器插件
}

kotlin {
    macosArm64()  // M 系列芯片
    jvm("debug")  // 调试用 JVM 目标

    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation("io.neton:core:1.0.0-beta1")
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
            }
        }
        val nativeMain by getting {
            dependencies {
                implementation("io.neton:sqlite-driver:1.0.0-beta1")  // 基于 SQLite3 C API
            }
        }
    }
}

注意:Neton 的插件会自动处理 K/N 的 linker 参数,你不需要手动配置 -linker-options。

数据层:没有 ORM,只有编译期 SQL

Neton 没有搞 ORM,因为 K/N 上反射做不了。它用的是编译期 SQL 校验:你写一个 @Sql 注解的接口方法,编译器插件会解析 SQL 字符串,检查表名和列名是否存在,并且生成类型安全的参数绑定代码。

interface TodoRepository {
    @Sql("SELECT * FROM todos WHERE id = ?")
    suspend fun findById(id: Long): Todo?

    @Sql("INSERT INTO todos (title, completed) VALUES (?, ?) RETURNING *")
    suspend fun insert(title: String, completed: Boolean): Todo

    @Sql("UPDATE todos SET completed = ? WHERE id = ?")
    suspend fun updateCompleted(completed: Boolean, id: Long): Int
}

编译后,findById 会生成类似这样的代码(简化):

// 自动生成
override suspend fun findById(id: Long): Todo? {
    val stmt = connection.prepareStatement("SELECT * FROM todos WHERE id = ?")
    stmt.bindLong(0, id)  // 注意索引从 0 开始,因为 SQLite C API 是这样
    return stmt.query().map { row -> Todo(row.long(0), row.string(1), row.bool(2)) }
}

好处:SQL 语法错误在编译期就报错,不会运行时炸。坏处:动态 SQL(比如根据条件拼接 WHERE)不支持,你得写多个方法。但实践下来,99% 的场景都能用静态 SQL 覆盖。

拦截器:编译期织入,不是 AOP 代理

写一个 JWT 认证拦截器:

@Interceptor
class AuthInterceptor : HandlerInterceptor {
    override suspend fun preHandle(call: NetonCall): Boolean {
        val token = call.request.headers["Authorization"] ?: return false
        return verifyJwt(token)  // 纯 Kotlin 实现,无反射
    }

    override suspend fun postHandle(call: NetonCall, response: NetonResponse) {
        response.headers["X-Processed-By"] = "Neton"
    }
}

Neton 编译器插件会把这个拦截器织入到所有匹配的路由调用链中。具体做法是:在生成的路由处理函数里,把 preHandle 的调用插到 handler 之前,postHandle 插到之后。没有代理对象,没有调用链栈,就是简单的函数调用。

如果你需要对特定路由排除拦截器,用 @ExcludeInterceptor(AuthInterceptor::class) 注解在路由函数上。

启动入口

fun main() {
    // 初始化数据库(使用原生 SQLite)
    Database.init("todo.db")

    Neton.server(port = 8080) {
        // 注册模块
        module(TodoModule())

        // 全局拦截器
        intercept(AuthInterceptor())

        routing {
            get("/todos") {
                val todos = call.di<TodoService>().getAll()
                call.respond(todos)
            }
            post("/todos") {
                val body = call.receive<TodoRequest>()
                val todo = call.di<TodoService>().create(body.title)
                call.respond(status = HttpStatusCode.Created, body = todo)
            }
            get("/todos/{id}") {
                val id = call.parameters["id"]!!.toLong()
                val todo = call.di<TodoService>().getById(id) ?: return@get call.respond(404)
                call.respond(todo)
            }
        }
    }.start(wait = true)
}

编译成原生二进制后,启动输出:

Neton 1.0.0-beta1 started on port 8080 (native, event loop)
Startup time: 0.023s  // 23ms,JVM 上 Ktor 至少 1-2s
Memory: 18MB RSS

性能实测:不是吹的,有数据

我拿同样的 To-Do API 做对比测试,一台 M1 MacBook Pro,并发 100 连接,压测工具 wrk,30 秒时长。

框架 目标 每秒请求数 (RPS) 延迟 P99 内存占用
Neton 1.0.0-beta1 (native) macOS arm64 48,500 8ms 22MB
Ktor 2.3 (JVM, Netty) macOS arm64 22,300 15ms 180MB
Spring Boot 3 (JVM, Tomcat) macOS arm64 9,800 32ms 350MB

注意,Neton 的 RPS 是 Ktor 的两倍多,但内存只有其八分之一。原因很明显:

  1. 没有 GC 停顿:K/N 的 GC 是引用计数 + 周期性的 tracing,但对象分配远少于 JVM 框架(因为无反射,无临时对象)。
  2. 事件循环零拷贝:请求处理路径上,Neton 直接操作字节缓冲区,不像 Netty 那样有复杂的 ChannelPipeline 对象链。
  3. 路由哈希查找:前面说的 when 表达式,比 Ktor 的前缀树匹配快一个数量级。

但有个前提:这个性能优势在纯 IO 密集场景下才明显。如果你的业务逻辑里有复杂的 CPU 计算(比如 JSON 序列化大数据),K/N 的序列化性能其实不如 JVM 上的 Jackson(因为 Jackson 用了大量的字节码增强)。Neton 默认用的是 kotlinx.serialization,它没有反射,但速度比 Jackson 慢约 20%。不过对于服务端 API 场景,JSON 序列化通常不是瓶颈。


踩坑记录:K/N 服务端的真实痛点

Neton 解决了框架层的问题,但 K/N 生态的坑它填不完。我实际开发中遇到三个大坑,分享出来帮你避雷:

坑 1:协程调度器不能跨线程

K/N 的协程默认 Dispatchers.Default 是单线程的(如果你不启用多线程)。如果你在代码里用了 withContext(Dispatchers.IO),在 native 上会直接抛异常,因为 Dispatchers.IO 在 K/N 上根本不存在。Neton 的解决办法是提供 NetonDispatchers.Database,它内部是一个专门的线程池,但要求数据库连接必须是 @ThreadLocal 的。所以你的 TodoRepository 必须写成:

class SqliteTodoRepository : TodoRepository {
    // 每个线程一个连接
    private val connections = ThreadLocal<SQLiteConnection>()

    override suspend fun findById(id: Long): Todo? {
        return withContext(NetonDispatchers.Database) {
            val conn = connections.get() ?: createConnection().also { connections.set(it) }
            // 查询
        }
    }
}

教训:不要在 commonMain 里写任何依赖 JVM 线程模型的代码。设计数据层时就要考虑 ThreadLocal 和对象冻结。

坑 2:对象冻结(Freezing)

K/N 的不可变对象可以安全跨线程共享,但如果你试图修改一个被冻结的对象,会直接崩溃。Neton 的 ServiceGraph 是懒加载的,所以第一次访问时创建的 Bean 是可变状态,但如果某个 Bean 被多个请求同时访问,且你在里面存了可变字段(比如 var count: Int),编译器会警告你,运行时如果跨线程访问会抛 InvalidMutabilityException。

解决办法:所有共享 Bean 必须是不可变设计,可变状态放数据库或原子变量(K/N 提供 kotlin.concurrent.AtomicInt,但它内部用锁,性能一般)。Neton 官方推荐的模式是:无状态 Bean + 数据库做状态持久化。

坑 3:JSON 序列化对 Any 类型不友好

Kotlin/Native 的 kotlinx.serialization 要求所有可序列化类型都是编译期已知的。如果你写 call.respond(mapOf("data" to anyObject)),编译器会报错,因为 Any 类型的序列化器在 native 上无法生成。必须用 JsonElement 或显式类型。

// 这样不行
call.respond(mapOf("user" to user))  // 编译错误

// 必须这样
call.respond(jsonObject {
    "user" to user.toJson()
})

好在 Neton 提供了 call.respondJson() 的扩展,内部帮你处理了类型转换,但前提是你不能传 Map<String, Any>。


与 Ktor 的选型对比(个人建议)

如果你现在要启动一个 Kotlin 服务端项目,我的建议是:

  • 团队熟悉 JVM,且业务不要求原生部署:选 Ktor,生态成熟,资料多,跟 Java 库互操作方便。
  • 追求极致启动速度和内存占用,且需要部署到嵌入式设备或 iOS 端共享逻辑:选 Neton,但要有心理准备,你得接受它的约束(无反射、静态 SQL、单线程模型)。
  • 混合场景:用 Neton 写核心服务(比如 API 网关),用 Ktor 写业务系统(需要大量第三方库)。两者可以共存,因为 Neton 支持编译成 JVM 字节码,所以你可以把 Neton 的模块打包成 JAR 放进 Ktor 项目。

Neton 的 beta1 版本已经能跑生产级负载,但有几个已知问题:WebSocket 支持还不完善(目前只支持文本帧,二进制帧要等 beta2);HTTP/2 只支持 JVM 目标,native 目标还是 HTTP/1.1;官方文档比较少,很多 API 要翻源码才能懂。


未来展望

Neton 团队的路标里,我比较期待三个东西:

  1. 协程调度器的多线程模式:目前单线程事件循环虽然简单,但 CPU 密集任务会阻塞所有请求。他们计划在 1.0 正式版里支持多 worker 线程,每个 worker 一个事件循环,通过消息传递通信,而不是共享内存。
  2. gRPC 支持:原生目标的 gRPC 目前是空白,Neton 打算用编译期代码生成,而不是运行时反射。
  3. 迁移工具:从 Ktor 迁移到 Neton 的自动转换工具,目前还在原型阶段。

说实话,K/N 服务端生态要真正成熟,光靠一个框架不够,还需要数据库驱动、消息队列客户端、监控 SDK 等配套组件。Neton 现在做的是“框架先行”,后续能不能带动生态,就看社区买不买账了。


最后说点真实的

Neton 1.0.0-beta1 不是一个“玩具框架”,它解决的是真实问题:如果你有一个 App 的后端,想要在 iOS 和服务器之间共享 Kotlin 逻辑,而不想维护两套代码,Neton 是目前唯一能让你把服务端代码也编进 iOS App 里的方案(通过 K/N 编译成静态库)。我自己就在一个内部项目中用了它,把用户认证和业务规则逻辑抽成了 commonMain,服务端和 iOS 端共用,编译时间虽然长了点(K/N 编译确实慢,全量编译要 3-4 分钟),但维护成本降了一半。

如果你要上手,建议先跑一遍官方仓库里的 examples/todo-native 示例,把编译插件和 Gradle 配置跑通,再开始写业务。记住,任何在 JVM 上养成的“反射习惯”都要改掉,遇到问题先问自己:这个能不能在编译期解决?

以上。有问题欢迎在评论区交流,我会尽量回复。

相似推荐
uBlock Origin 开发版被 Chrome 商店拒绝:一场关于扩展单一用途政策的争议逆向工程实战:我如何绕过亚马逊Kindle网页端的DRM加密Heroku的丑陋秘密:云之王如何背离Rails并欺骗客户Firefox 成为最后一个支持 uBlock Origin 的主流浏览器:广告拦截的终局之战GitHub Codespaces 深度解读:云端开发环境如何重塑编码工作流认知负荷才是关键:软件设计的根本度量
编写使用方法
Markdown 格式 · Ctrl+Enter 确定
0 字新建笔记
欢迎回来
登录你的墨穗笔记账户
忘记密码?
还没有账户?立即注册
创建账户
注册你的专属墨穗笔记
已有账户?去登录
找回密码
输入注册邮箱获取验证码
返回登录
请输入图片中的验证码以继续注册
加载中...
取消
新建收藏
手动添加你喜欢的内容
取消
编辑头像与昵称
上传新头像或修改你的显示昵称
支持 JPG/PNG,最大 2MB
取消

问题反馈

隐私提醒

取消
编辑工具
受控分享
为这篇笔记生成限时 / 带密码的临时链接
关闭