笔记
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 服务端时代,从这一刻开始
好,直接开工。这篇文章我酝酿了很久,今天终于能静下心来把它写透。先声明一下,这不是一篇简单的“发版公告翻译”,而是我基于 Neton 1.0.0-beta1 的源码、文档和实际跑过的示例,写的一篇深度笔记。如果你正在关注 Kotlin 服务端生态,或者对 K/N 跨平台方案有执念,这篇应该能给你省下不少自己踩坑的时间。
为什么 Neton 值得你停下来看一眼
先说个背景。Kotlin 生态里,服务端框架一直有个尴尬的局面:JVM 上的 Ktor 和 Spring Boot 很成熟,但一旦你动了“用 Kotlin/Native 写服务端”的念头,基本就掉进了无人区。不是没有尝试,而是没有一个框架能同时满足三个硬需求:
- 真·跨平台:同一套业务代码,编译成 JVM 字节码跑在 Linux 服务器上,也能编译成原生二进制跑在嵌入式设备或 macOS 上,而不是靠 Docker 包一层 JVM 来假装“原生”。
- 异步非阻塞:不能是 JDK 19 的虚拟线程那种“伪异步”,必须是事件循环 + 协程的原生支持,因为 K/N 上根本没有 JVM 的线程模型。
- 零反射、零动态代理: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)
// 等等,每个路径都是精确匹配
}
}
这带来两个极致优化:
- 匹配是 O(1) 的。没有正则回溯,没有前缀树遍历,就是哈希查找。压测数据后面给。
- 路径参数是编译期提取的。
/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 的两倍多,但内存只有其八分之一。原因很明显:
- 没有 GC 停顿:K/N 的 GC 是引用计数 + 周期性的 tracing,但对象分配远少于 JVM 框架(因为无反射,无临时对象)。
- 事件循环零拷贝:请求处理路径上,Neton 直接操作字节缓冲区,不像 Netty 那样有复杂的
ChannelPipeline对象链。 - 路由哈希查找:前面说的
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 团队的路标里,我比较期待三个东西:
- 协程调度器的多线程模式:目前单线程事件循环虽然简单,但 CPU 密集任务会阻塞所有请求。他们计划在 1.0 正式版里支持多 worker 线程,每个 worker 一个事件循环,通过消息传递通信,而不是共享内存。
- gRPC 支持:原生目标的 gRPC 目前是空白,Neton 打算用编译期代码生成,而不是运行时反射。
- 迁移工具:从 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 上养成的“反射习惯”都要改掉,遇到问题先问自己:这个能不能在编译期解决?
以上。有问题欢迎在评论区交流,我会尽量回复。