林子豪的 PKM
← 返回 Blog

nrepl-starter 与 Tenx:把 REPL 接进运行中的 Java JVM

nrepl-starter 在 Java 应用内部启动 Clojure nREPL,Tenx 再把 IDEA 里的方法调用转成 REPL 表达式,让复杂测试数据准备和运行时调试从半小时缩短到秒级。

4 min read系列 · nrepl-starter
clojurejavanreplintellijdeveloper-tools

在 Java 项目里,测试一个业务场景经常不是调用一个 HTTP 接口那么简单。

以仓储履约为例,要测试“拣货过程中客户取消订单,已拣货包裹进入归还流程”,可能要先创建订单、准备库存、推进订单状态,再卡在恰当的时间点触发取消。只从界面或公开接口构造,一次就可能花半小时。很多真正有用的操作——修改库存、推动状态机、调用某个内部服务——本来就不应该暴露成 HTTP API。

我写 nrepl-starter 的目的,就是把 Clojure 的交互式开发能力带进普通 Java 应用:让脚本直接在业务 JVM 内执行,复用已经加载的类、对象和依赖,而不是在进程外重新实现一套测试接口。

后来我又写了 IDEA 插件 Tenx,把“在 REPL 里组织表达式”进一步缩短成“在 Java 方法旁直接调用”。这套实践最终积累了约 3000 行脚本,覆盖从生成订单到发货、退货、订单超时、库存异常和包裹分流等 50 多个常用测试场景,把不少原来需要 30 分钟准备的数据降到了秒级。这段实践最早记录在 cider 2022 年报。

nrepl-starter 做了什么

nrepl 是一个网络 REPL 协议。IDE、编辑器或脚本可以通过它向运行中的 Clojure 进程发送代码,进程返回求值结果。

nrepl-starter 把这个方向反过来用在 Java 应用上:它把 Clojure、nREPL 和 cider-nrepl 嵌入目标 JVM,并在应用启动时打开一个 nREPL server。客户端连上以后,求值发生在业务进程内部,所以表达式可以直接 import 项目中的 Java 类、创建对象和调用方法。

Calva 和 Tenx 通过 nREPL 连接同一个运行中的 Java JVM

这和单独启动一个 Clojure 脚本进程不同。独立进程只能通过数据库、HTTP、RPC 等边界访问应用;嵌入式 REPL 与应用共享 classpath 和运行时,可以复用内部业务代码。它也和 debugger 不完全相同:debugger 适合暂停和观察一次执行,REPL 更适合反复组合操作、保存脚本,并把一次调试沉淀成下次可直接复用的工具。

当前版本推荐用 Java Agent 启动,不需要修改业务 main

mvn clean package

java \
  -javaagent:./target/nrepl-starter-2.0.0-SNAPSHOT-agent.jar \
  -Dnrepl.port=7888 \
  -Dnrepl.enabled=true \
  -jar your-app.jar

启动后,可以用 Calva 等 nREPL 客户端连接 7888,直接调用 Java:

(ns repl.core
  (:import [com.example App]))

(App/add 1 2)

(def app (App.))
(.subtract app 10 3)

项目也保留了在 main 中显式创建 repl.R 的启动方式。不过 Java Agent 还能取得 Instrumentation,因此是现在更完整的路径。

从测试脚本扩展到运行时工作台

nrepl-starter 2.0 不只负责调用已有方法,还在向 JVM 运行时工作台发展。

repl.tools 可以在目标 JVM 中编译并加载新的 Java class,也可以通过 Instrumentation.redefineClasses 替换已加载 class 的方法体:

(repl.tools/redefine-file!
  "com.example.App"
  "src/main/java/com/example/App.java")

JVM 标准 HotSwap 仍有边界:可以修改方法体和常量,但不能增加或删除字段、方法。需要改变 class schema 时,可以把新类交给 compile-and-load!,由新的 classloader 加载。

repl.inspect 则把内存、GC、线程、CPU、已加载 class、对象浅大小和 heap dump 等运行时信息暴露成 EDN 数据。JDK 17 及以上还可以启用只监听 loopback 的 Electric Clojure Dashboard,在浏览器里看实时指标、搜索 class 和执行表达式。

这些能力仍然围绕同一个原则:不要为每次诊断重新造一条临时接口;把运行中的 JVM 当作一个可以查询和操作的系统。

Tenx 如何把 IDEA 方法变成 REPL 调用

直接写 REPL 表达式很灵活,但调用一个普通 Java 方法时,开发者还要自己查类名、参数类型和 Spring Bean。Tenx 利用 IntelliJ PSI 已经掌握的源码信息,把这一步自动化。

在方法旁执行 Call this method 时,Tenx 会读取:

- 类名和方法名;

- 参数名与参数类型;

- 当前 IDEA 项目对应的 REPL client。

插件再连接默认的 7888 端口,生成并发送一段 Clojure 表达式。这段表达式通过 repl.R 找到 Spring Bean、转换参数并调用目标方法。Tenx 的 Gradle 配置并不直接依赖 nrepl-starter,但它在运行时依赖 nrepl-starter 提供的端口和 repl.R API,因此两者是 server 与 IDEA client 的关系。

Tenx 的公开仓库保留了这条原始实现,不过 README 和安装说明已经过时,当前不应再按其中的 Marketplace 名称安装。它更适合作为设计和实现的历史记录。

版本与安全边界

写本文时,GitHub main 上的 nrepl-starter 是 2.0.0-SNAPSHOT,而 Maven Central 最新正式版是 1.1.0。仓库 README 里的 Gradle 示例仍写着 1.0.9。因此,使用 2.0 的 Java Agent、Dashboard、repl.toolsrepl.inspect 前,应从源码构建,并以仓库当前 README 为准,不能把 SNAPSHOT 当成已经发布到 Central 的稳定版。

nREPL 能执行任意代码,这正是它有用的原因,也是它的安全边界。不要把端口暴露给不可信网络;线上使用时至少应通过 loopback、SSH tunnel、网络策略和主机权限限制访问,并把每一次 eval 当作直接取得目标 JVM 权限。它首先是开发、测试和受控诊断工具,不是应该公开暴露的业务 API。

相关笔记:clojure repl、clojure 连接 nrepl 服务器、创建 idea 插件。