Neton 1.0 实战:Kotlin/Native 装配 AI 原生 API 服务
Neton 1.0.0 已发布到 Maven Central,用 Kotlin/Native 可以编出不带 JVM 的原生 HTTP 服务;真正要发布到 Central 的是共享核心库,服务本体以二进制分发,这样拆分才可行。
- Neton 交付的是原生的可执行文件,并非 jar;冷启动没有 JVM 预热。
- 在 Maven Central 上应该发布 core 共享库;服务端二进制不要发到 Central。
- Kotlin/Native 无法跨操作系统交叉编译;linuxX64 必须在 Linux 上构建。
- 发布失败最常见的原因是 GPG 签名缺失和 sources 产物缺失。
Neton 1.0.0 已经在 Maven Central 发布,用 Kotlin/Native 能编出不带 JVM 的原生 HTTP API 服务。我的判断是:它适合做“小、稳、秒起”的边缘 API 层,不适合拿来承接复杂业务域。
2026 年 10 月 29 日这个版本是 Neton 的第一个稳定版。官方给的定位很明确:以应用装配、依赖注入、HTTP API、数据库、认证授权、缓存、任务调度、对象存储、WebSocket 为一套完整体系,对标 Spring Boot、Laravel 这一层,不跟底层网络库抢位置。本文不复述功能清单,只讲一件事——怎么把它配起来、跑通、并且正确地接进 Maven Central 的依赖链。
Neton 1.0 和 Spring Boot 有什么本质区别?先看分发单元#
很多人把两者放在一起比注解风格,比错了地方。最大的差别不在代码怎么写,而在你最终交给运维的是什么。
Spring Boot 交的是 jar,Neton 交的是一个可执行文件。这一条决定了后面所有工程动作:CI 怎么出包、镜像怎么分层、依赖怎么发布、线上怎么排障。
| 对比项 | Spring Boot 3.x | Neton 1.0.0(Kotlin/Native) |
|---|---|---|
| 交付物 | 可执行 fat jar / war | 单一原生可执行文件 |
| 运行时依赖 | 需要目标机器或镜像内有 JRE | 不需要,可以直接 exec |
| 启动特征 | JVM 预热 + 类加载 + Spring 上下文刷新 | 进程直接进 main,没有预热阶段 |
| 交叉编译 | 字节码跨平台,一次构建处处跑 | 不能跨操作系统,linuxX64 必须在 Linux 上编 |
| 库覆盖 | spring-boot-starter-* 覆盖面极大 | 集中式一套,缺的轮子要自己写 |
| 依赖分发 | Maven 仓库存脚本 | Maven 仓库存库,服务本体走二进制分发 |
| 排障工具 | jstack / jmap / Arthas 一整套 | 原生调试器,工具面窄得多 |
我在一个内部项目上做过小范围对照:同一套 DTO、三个接口,分别用 Spring Boot 3.x 和 Neton 写一遍。镜像构建时前者必须为基础镜像准备 JDK,后者 FROM scratch 也能跑,前提是把 CA 证书和时区文件塞进去。镜像体积的差距取决于你选的 JDK 版本,别信二手数字,自己 docker image ls 量一次更靠谱。
真正让我在意的是工具链落差。JVM 那套 jstack、heap dump、在线诊断在原生进程上是断档的,进程一旦卡在某个系统调用,你手上只有 gdb、strace 和日志。这是选型时必须提前接受的成本,不是能不能解决的问题。
如何用 Gradle 装配一个 Neton 1.0 服务?#
按顺序走四步,每一步都有可验证的中间结果。
- 在 Maven Central 上核实真实坐标,别抄博客里的二手 artifactId;
- 写
build.gradle.kts,把 linuxX64 可执行目标配出来; - 写入口,绑定端口和监听地址;
- 本机跑起来,用 API.PLUS 的真实接口做一次出网验证。
第一步:核实坐标
Neton 刚发 1.0.0,网上流传的坐标未必和 Central 一致。用官方搜索接口问一次最快:
示例说明:以下代码仅用于逻辑验证,生产环境需补充异常处理、超时与重试、日志与监控告警及降级策略。
curl -sS 'https://search.maven.org/solrsearch/select?q=neton&rows=20&wt=json' \
| python3 -c 'import sys,json; d=json.load(sys.stdin); [print(x["id"], x.get("latestVersion")) for x in d["response"]["docs"]]'
q=neton 是关键字检索,rows=20 限制返回条数,wt=json 指定响应格式。预期输出是一行行 groupId:artifactId 加最新版本号,例如 xxx:neton-core 1.0.0。把打印出来的那一行原样抄进 Gradle,不要自己拼。
我见过最常见的低级事故,就是照抄某篇教程里的 artifactId,结果那个构件根本没上传,Gradle 报 Could not find 之后有人去改仓库地址,越改越远。
第二步:构建脚本
// build.gradle.kts —— 服务模块,目标是 linuxX64 可执行文件
plugins {
kotlin("multiplatform") version "2.2.20" // 版本按你仓库现有工具链锁定
kotlin("plugin.serialization") version "2.2.20"
}
kotlin {
linuxX64 {
binaries {
executable {
entryPoint = "plus.api.neton.main"
baseName = "api-plus-neton-service"
}
}
}
sourceSets {
val linuxX64Main by getting {
dependencies {
// 坐标以第一步查到的实际 artifact 为准
implementation("xxx:neton-core:1.0.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
}
}
}
}
entryPoint 写全限定名,不带 .kt;baseName 决定产出的文件名,部署脚本里会用到。构建机上补一条 gradle.properties:
org.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=1g
org.gradle.parallel=true
# 容器里编译器缓存经常失效,直接关掉,构建变慢但不会莫名链接失败
kotlin.native.cacheKind.linuxX64=none
Kotlin/Native 的链接阶段是内存大户。4C8G 的构建机上,全量链接把 Gradle 堆推到 6GB 以上是常态,默认堆大小会直接 OOM。这个坑我在两台 CI runner 上都踩过。
第三步:入口
// src/linuxX64Main/kotlin/plus/api/neton/Main.kt
package plus.api.neton
fun main() {
// 装配顺序:服务参数 -> 路由 -> 中间件 -> 启动
// 具体 DSL 函数名以 1.0.0 源码为准,端口/绑定地址/代理头这三项是不变的
startServer(
port = 8080,
host = "0.0.0.0",
trustProxyHeaders = true, // 在 Nginx 后面必须开,否则拿不到真实来源 IP
) {
route("/health") { """{"status":"ok","build":"1.0.0"}""" }
route("/v1/ipinfo") { ipLookup() } // 内部转发到 API.PLUS
route("/v1/complete") { aiComplete() } // 对接大模型补全
}
}
trustProxyHeaders 是我最想强调的一个参数。默认关闭时,服务看到的所有请求来源都是 Nginx 的 127.0.0.1,你的限流、审计、风控全部失效。上线前先确认这一项。
第四步:出网验证
服务起来之后,让它调一次外部接口,确认容器里的 DNS、CA 证书、出网策略都是通的。拿 API.PLUS 的 GET 示例接口 做验证最直接:
curl -sS -G "https://www.api.plus/api/get" \
--data-urlencode "apikey=YOUR_KEY" \
--data-urlencode "ip=8.8.8.8" \
-H "Accept: application/json" \
-w '\nHTTP=%{http_code} time=%{time_total}s\n'
-G 让后面的 --data-urlencode 以查询串形式发出去,而不是走 POST body;--data-urlencode 会做 URL 编码,避免 key 里有特殊字符时被截断;-w 打印状态码和总耗时,方便判断是不是网络层慢。返回形如:
{"code":200,"msg":"ok","data":{"ip":"8.8.8.8","country":"美国","city":"","isp":"Google LLC"}}
字段名以实际返回为准。time 这一项如果稳定超过 1s,先查容器 DNS,再查是否走了代理。
图片类接口用 随机动漫壁纸接口 验证更省事,它不需要 key,可以直接压测连通性。Python 侧调用示例:
# Python 3.9+
import json
import urllib.request
import urllib.error
URL = "https://www.api.plus/api/dongman?type=json"
req = urllib.request.Request(
URL,
headers={
"Accept": "application/json",
# 带上服务名,出问题时对端日志里能认出是谁在调
"User-Agent": "api-plus-neton-service/1.0.0",
},
)
try:
with urllib.request.urlopen(req, timeout=5) as resp:
data = json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as e:
print("upstream 4xx/5xx:", e.code) # 对端拒绝,别重试
raise
except urllib.error.URLError as e:
print("network error:", e.reason) # DNS/连接超时,可以退避重试
raise
print(data.get("data", data))
timeout=5 是必须的,原生服务里没有 JVM 那种默认超时兜底,不设置会一直挂着。type=json 决定返回结构化结果而非 302 跳转,服务端转发场景一定要用 json 形态。业务接口需要 POST 时参考 POST 示例接口,注意表单和 JSON 两种 Content-Type 的响应契约不同。
Maven Central 上到底该发什么?拆包策略决定成败#
这是整篇文章里我最想纠正的一个认知偏差。标题里说“部署到 Maven Central 依赖链”,很多人第一反应是把整个服务打成构件推上去。这个做法是错的。
Kotlin/Native 编出来的是特定 OS + 架构的机器码,不是跨平台字节码。你发一个 linuxX64 可执行文件到 Central,别人 implementation 进来什么也做不了。Central 上有意义的产物只有两类:多平台共享库(DTO、序列化、客户端 SDK),以及给其他 Kotlin 项目用的模块。
所以我推荐的目录结构是这样:
.
├── core/ # multiplatform 共享库,发布到 Maven Central
│ └── src/commonMain/kotlin/plus/api/neton/core/
├── service/ # linuxX64 可执行,只产出二进制,不发 Central
│ └── src/linuxX64Main/kotlin/plus/api/neton/
└── settings.gradle.kts
core 里放什么:请求/响应的数据类、@Serializable 标注、错误码枚举、对 API.PLUS 的客户端封装。这些东西被三个以上服务复用,发布才有价值。service 里放什么:路由、中间件、数据库连接、启动参数——这些是部署形态,不该进仓库。
发布配置与签名
// core/build.gradle.kts
plugins {
kotlin("multiplatform")
`maven-publish`
signing
}
publishing {
publications {
create<MavenPublication>("maven") {
groupId = "plus.api.neton"
artifactId = "neton-ai-core"
version = "1.0.0"
pom {
name.set("api-plus-neton-core")
description.set("Shared DTO and client for API.PLUS Neton services")
url.set("https://www.api.plus")
licenses {
license { name.set("Apache-2.0"); url.set("https://www.apache.org/licenses/LICENSE-2.0") }
}
scm {
connection.set("scm:git:https://www.api.plus")
url.set("https://www.api.plus")
}
}
}
}
repositories {
maven {
name = "central"
url = uri("https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/")
credentials {
username = providers.environmentVariable("MAVEN_USERNAME").getOrNull()
password = providers.environmentVariable("MAVEN_PASSWORD").getOrNull()
}
}
}
}
signing {
val key = providers.environmentVariable("SIGNING_KEY").getOrNull()
val pwd = providers.environmentVariable("SIGNING_PASSWORD").getOrNull()
if (key != null && pwd != null) {
useInMemoryPgpKeys(key, pwd)
sign(publishing.publications["maven"])
}
}
发布前把公钥传到 keys.openpgp.org,密钥过期是 Central 拒收的头号原因。执行命令:
gpg --armor --export-secret-keys <KEY_ID> > signing-key.asc
export SIGNING_KEY="$(cat signing-key.asc)"
export SIGNING_PASSWORD='***'
export MAVEN_USERNAME='***'
export MAVEN_PASSWORD='***'
./gradlew :core:publishAllPublicationsToCentralRepository --no-daemon
--no-daemon 在 CI 里能避免守护进程复用导致的密钥读取问题。发布完之后验证是否真的可见:
curl -sS "https://repo1.maven.org/maven2/plus/api/neton/neton-ai-core/maven-metadata.xml"
能返回 <version>1.0.0</version> 才算成功。从上传完成到 repo1 可查通常以分钟计,不要一提交就立刻断言失败。
三个必踩的坑
| 现象 | 原因 | 处理 |
|---|---|---|
| Central 驳回,提示缺少 sources | multiplatform 模块不会自动生成 sources 产物 | 给每个 target 挂 withSourcesJar(),或引入 Dokka 出 javadoc |
签名报 no signature for artifact | 只签了主 jar,没签 metadata 变体 | sign(publishing.publications["maven"]) 整体签,别逐个签 |
| macOS 上编 linuxX64 直接失败 | Kotlin/Native 不支持跨操作系统交叉编译 | CI 用 Linux runner,或本地起 gradle:jdk21 容器 |
上线后盯哪几个数字?#
原生服务的观感和 JVM 完全不同:没有 heap 曲线,没有 GC 日志,很多熟悉的仪表盘直接失效。下面这几项是我实际会看的。
- 从进程启动到监听端口的耗时。应用启动时打一行带时间戳的日志,和 systemd 的
ActiveEnterTimestamp相减。这个数决定你的滚动发布窗口能开多小。 - RSS 常驻内存。读
/proc/<pid>/status里的VmRSS。Kotlin/Native 有追踪式 GC,RSS 会随负载爬升后趋于平稳,看的是稳态值而不是瞬时值。 - 二进制体积。
ls -lh一下就行。每加一个依赖都值得记录一次,CI 里做成趋势图。 - 构建耗时与 Gradle 堆峰值。Kotlin/Native 链接阶段是瓶颈,堆不够表现为 OOM 而不是慢。
- Maven Central 传播延迟。从
publishAllPublications成功到 repo1 可查的时间差,发布窗口里要预留。
失败案例也说一个。我们在灰度环境遇到过一次服务启动成功但外部请求全部超时,查了两小时才发现是 trustProxyHeaders 没开,Nginx 传的 X-Forwarded-For 被忽略,应用侧按 127.0.0.1 做限流,第一秒就把配额打满。日志里看不到任何异常,因为请求确实“正常处理”了,只是被自己的限流拦了。这类问题在 JVM 上同样存在,但原生服务缺少那些能一眼看出“来源 IP 全一样”的现成面板。
监控侧可以配合 沐小悠统计 做站点维度的流量与访客分析,它接入成本低,适合验证前端流量和后端日志对不对得上。如果服务需要用户体系,沐小悠聚合登录 提供社会化账号聚合登录,省掉自建 OAuth 回调的那一圈活。更多接入细节可以翻 API 文章中心,遇到具体问题走 技术支持。
总结:适用边界比框架能力更重要#
Neton 1.0.0 值得试,但别指望它把 Spring Boot 那套东西平移过来。我给的边界是:
- 适合:边缘 API 层、需要快速冷启动的 Serverless 型服务、对镜像体积和依赖数量敏感的场景、AI 网关这类“协议转换 + 转发”为主的薄服务。
- 不适合:需要大量第三方集成、需要成熟可观测方案、团队里没有 Kotlin 经验、业务域复杂到需要多个 starter 拼装的场景。
落地姿势上,我的建议是拆成两段:core 放共享模型和客户端,发到 Maven Central 被别人 implementation;service 编成 linuxX64 二进制,走镜像或 systemd 分发。这两件事不要混在一起做,混了就是两头都不顺。
验证阶段需要外部接口练手的话,可以在 API.PLUS 注册拿一个免费 API Key,用 GET 示例接口 打通链路,再用 随机动漫壁纸接口 做并发压测——它不需要鉴权,最适合拿来测连接池和超时配置。跑通了再往上叠业务逻辑,比一上来就搭一整套要快得多。