# spring-boot-runner
**Repository Path**: boluoshu/spring-boot-runner
## Basic Information
- **Project Name**: spring-boot-runner
- **Description**: vscode spring boot启动管理插件
- **Primary Language**: Unknown
- **License**: MIT
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2026-09-17
- **Last Updated**: 2026-10-27
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# Spring Boot Runner(免插件)— VS Code 扩展
> 🌐 **语言 / Language:** **中文** | [English](#english)
一个在命令行直接运行**未打包**的 Spring Boot(Maven)项目的 VS Code 扩展:通过 `java -cp`
启动,既不需要 `spring-boot-maven-plugin`,也不依赖官方的 VS Code Spring Boot 插件。
它是 `scripts/run-spring-boot.mjs` 的一层薄 UI —— 一个核心脚本、一个入口:
- **VS Code 扩展** → 人在编辑器里点一下命令。
运行器是纯 Node(ESM)实现,因此**无需安装 Python** —— 只需要 PATH 上有 Maven 和一个 JDK。
## 源码仓库
Gitee:`https://gitee.com/boluoshu/spring-boot-runner`(分支 `master`)
```bash
git clone https://gitee.com/boluoshu/spring-boot-runner.git
```
## 为什么要做这个
Spring Boot Dashboard「运行」应用的方式是:解析出 `@SpringBootApplication` 主类 + 类路径,
然后调用 `java -cp `。本扩展用纯命令行工具复刻了这一过程
(用 `maven-dependency-plugin` 取代 IDE 的 JDT 类路径解析器),全程不需要
`spring-boot-maven-plugin`。超长的类路径写入临时目录下按项目区分的 `@argfile`,
启动命令因此保持很短,也绕开了 Windows `CreateProcess` 的命令行长度限制。
## 命令
| 命令 | 作用 |
| --- | --- |
| `Spring Boot Runner: 运行项目` | 用 `.vscode/launch.json` 里的一条运行配置启动。有多条时会询问用哪条;一条都没有则自动探测。 |
| `Spring Boot Runner: 运行测试…` | 跑测试并直接退出(不启动 java):`mvn test` / `gradle test`,可输入过滤串(如 `com.foo.*Test`),会记住上次输入。 |
| `Spring Boot Runner: 调试运行(JDWP)` | 同上,但以 JDWP 启动(`suspend` 按配置),并自动附加 VS Code 的 Java 调试器,断点立即可用。 |
| `Spring Boot Runner: 附加自定义参数运行` | 先提示输入额外参数(如 `--profiles dev --port 8080`),再运行。含空格的路径要加引号;这里的 `--java-home` 只对本次运行生效。 |
| `Spring Boot Runner: 设置 JDK(JAVA_HOME)…` | 提示输入 JDK 目录(会校验 `bin/java(.exe)`),存入选定的运行配置。输入为空即清除。 |
| `Spring Boot Runner: 停止` | 停止正在运行/调试的应用:分离调试器、通过 PID 文件杀掉整个 Java 进程树(`taskkill /PID … /T /F`),并兜底向终端发送 Ctrl-C。 |
输出在一个可复用的集成终端里,终端名为 **Spring Boot Runner**。
## 运行/调试配置 —— `.vscode/launch.json`
运行/调试配置保存在项目的 **`.vscode/launch.json`** 中,使用本扩展自己的调试器类型
`spring-boot-runner`。它们就是普通的调试配置,因此同一份文件同时驱动三个入口:
- **运行和调试**视图(侧边栏下拉框);
- **F5**(带调试运行)与 **Ctrl+F5**(不带调试运行);
- 本扩展贡献的 **运行/调试配置…** 面板(类 IDEA 的表单)。
`Ctrl+Shift+P → Spring Boot Runner: 运行/调试配置…` 打开面板:左侧是配置树,右侧是选中配置的
表单。运行参数没有单独的手写配置文件 —— 这个面板*就是* `launch.json` 的编辑器。
```jsonc
{
"version": "0.2.0",
"configurations": [
{
"type": "spring-boot-runner",
"request": "launch",
"name": "应用程序 1",
"module": "backend/bonlink-agent", // 留空 = 自动探测
"mainClass": "com.bonlink.PlatformApplication", // 留空 = 自动探测
"profiles": "dev",
"port": "8081",
"vmArgs": "-Xmx1g -Denv=dev",
"env": "SPRING_DATASOURCE_URL=jdbc:mysql://localhost/db",
"javaHome": "D:\\soft\\Java\\jdk-21.0.8+9",
"skipTests": true
},
{
"type": "spring-boot-runner",
"request": "attach", // 不构建、不启动,只附加
"name": "远程 JVM 调试 1",
"hostName": "10.0.0.5",
"port": 5005
}
]
}
```
`request: "launch"` 字段:
| 字段 | 默认值 | 含义 |
| --- | --- | --- |
| `module` | `""` | 构建模块目录名(Maven 的 pom.xml 或 Gradle 的 build.gradle(.kts),留空自动探测)。 |
| `mainClass` | `""` | 全限定主类名(留空自动探测 @SpringBootApplication;面板里是带候选的下拉框,可直接选)。 |
| `programArgs` | `""` | 程序实参,**每行一条**;不换行时仍按空格分隔(兼容旧写法)。 |
| `vmArgs` | `""` | 额外的 JVM 参数,**每行一条**;不换行时仍按空格分隔(兼容旧写法)。 |
| `extraClasspath` | `[]` | **额外的 classpath 条目,可以写多个。** JSON 数组(在 `launch.json` 里一行一条)或单个字符串、用换行/`;` 分隔。追加在 Maven 解析出的类路径**之后**,因此只做补充、不打乱既有顺序,可用于补上缺失的 jar 或另一个模块的 `target/classes`。相对路径按项目根解析,支持 `${workspaceFolder}` 等变量。不参与构建缓存 —— 改完下次运行即生效。 |
| `encodingArgs` | UTF-8 一组 | **控制台编码参数。** 默认就是一组 UTF-8(`file.encoding`、`sun.jnu.encoding`、`sun.stdout.encoding`、`sun.stderr.encoding`、`console.encoding`)—— 缺省即生效、不写进 `launch.json`;显式写空数组 `[]` 表示「不设置」。面板里是「控制台编码方式」下拉框(UTF-8 / GBK / 不设置 / 自定义),选中后启动时会先切换控制台代码页(Windows 上 `chcp 65001` / `936`)再注入参数。列表格式同 `extraClasspath`。参数同样追加到 Maven 构建的 `MAVEN_OPTS`;`vmArgs` 里手写的同名 `-D` 始终优先。JDK 19+ 的标准名(`stdout.encoding` / `stderr.encoding`)在缺省时自动补上。 |
| `profiles` | `""` | `spring.profiles.active`。 |
| `port` | `""` | 覆盖 `server.port`。 |
| `workingDir` | `""` | `java` 进程的工作目录(留空 = 模块目录)。 |
| `env` | `""` | 额外的 `KEY=VALUE` 环境变量,**每行一条**;不换行时仍按分号分隔(兼容旧写法)。 |
| `envFile` | `""` | 要加载的 `.env` 文件路径。 |
| `javaHome` | `""` | Maven 构建与应用启动**共用的 JDK 目录**(留空用 PATH 上的 `java`)。 |
| `debugPort` | `"5005"` | 调试时使用的 JDWP 端口。 |
| `suspend` | `true` | 调试时挂起 JVM,等待调试器附加。 |
| `jrebel` | `false` | 自动探测本机安装的 JRebel 扩展并注入 `-agentpath:…/jrebel64.dll`。 |
| `force` | `false` | 即使没有改动也重新构建(忽略构建缓存)。 |
| `clean` | `false` | 构建前先执行 `mvn clean`。 |
| `skipTests` | `true` | 构建时跳过测试。 |
| `offline` | `false` | Maven 离线模式(`-o`)。 |
### 额外 classpath(数量不限)
面板里填 **额外 classpath** 即可,一行一条:
```jsonc
"extraClasspath": [
"${workspaceFolder}/libs/fastjson.jar",
"${workspaceFolder}/other-module/target/classes"
]
```
每一条都会追加到运行器从 Maven 解析出的类路径(应用 classes + reactor 兄弟模块 + 已解析依赖)
之后。不存在的条目只在终端里给出警告,不会中断运行。
### Gradle 项目
构建系统自动判定:根目录(或子目录)有 `pom.xml` 走 Maven,只有 `build.gradle(.kts)`
走 Gradle —— 纯 Gradle 项目不会因为 PATH 上没有 `mvn` 而失败。
- **构建**:优先用项目自己的 `gradlew` wrapper(版本与项目绑定),其次 PATH 上的 `gradle`;
增量编译跑 `classes`(`clean` → `clean classes`,`force` → `--rerun-tasks`,
`offline` → `--offline`)。
- **类路径**:不改你的 build.gradle —— 运行器临时注入一个 init script,注册 `sbrClasspath`
任务打印 `sourceSets.main.runtimeClasspath`(IDEA 的做法类似,只是它走自己的模型),用完即删。
- **产物目录**:Gradle 的 class 落在 `build/classes/{java,kotlin,groovy}/main`,与 Maven 的
`target/classes` 不同;构建缓存与“跳过构建”的判定按对应目录找,所以增量体验一致。
- **编码参数**同时进 `GRADLE_OPTS`(Gradle 的守护进程与构建脚本读它),不只 `MAVEN_OPTS`。
### Kotlin / Groovy 与主类下拉
自动探测主类时扫描 `.java` / `.kt` / `.groovy` 三种扩展名,且覆盖
`src/main/{java,kotlin,groovy}` 三个源码目录 —— Kotlin Spring 项目也能自动找到主类。
面板里的主类输入框带 **datalist 下拉**:按当前选中的模块过滤候选,既能一键选、也能自由输入
(手输的值会保留在列表里)。主类写错只能到运行期才暴露,下拉把这类错误提前消掉。
### 运行测试
`Spring Boot Runner: 运行测试…` 会在选定的配置上跑测试然后**直接退出**:不组装类路径、
不写构建缓存、不启动 java。过滤串(`com.foo.BarTest`、`com.foo.*Test`)可留空跑全部,
输入过一次后会作为下次的默认值。
实现上是 `mvn test -Dtest=<过滤> -DfailIfNoTests=false` / `gradle test --tests <过滤>`;
`skipTests` 在测试模式下自动失效(否则一个测试都不会执行)。
### JDK 版本预检
启动前读一次 `pom.xml`(或 `build.gradle`)里的 `java.version` / `maven.compiler.release` /
`sourceCompatibility`,并与本次要用的 `java` 实际版本对比。**JVM 低于项目要求**时给一条
带操作入口的警告(“设置 JDK…” / “打开运行/调试配置”),而不是等你撞上
“不支持的发行版” / `UnsupportedClassVersionError` 再回头找原因。
读不到要求版本或测不到实际版本就**不提示** —— 宁可不说话,也不误报;警告与构建并行,
不阻塞启动。
### 控制台编码方式 —— 修复中文乱码
不注入任何参数时,Windows 上 JVM 按 `native.encoding`(中文系统 = GBK)编码 stdout,
而 VS Code 集成终端按 UTF-8 解码 —— 中文日志必然乱码(`鍵鐩兼枟` 那种)。
所以本扩展**默认就按 UTF-8 处理**,分两步:
1. 启动时先在终端执行 `chcp 65001`(GBK 档是 `chcp 936`),把 Windows 控制台的代码页切过去;
2. 再把一组编码参数注入应用 JVM。参数同时进 `MAVEN_OPTS`,Maven 自己的输出
(编译警告、依赖下载日志)也能正确解码。
面板里选的就是「控制台编码方式」:
- **UTF-8(推荐,默认)** —— 代码页 `65001` + 一组 `-D…=UTF-8`;
- **GBK(控制台切到 936)** —— 给必须跟系统代码页走的外部终端;
- **不设置** —— 既不切代码页也不注入参数(写进 `launch.json` 的是空数组,
不会被默认值重新填回来);
- **自定义** —— 切到逐项勾选:`file.encoding`、`sun.jnu.encoding`、`sun.stdout.encoding`、
`sun.stderr.encoding`、`console.encoding`(取当前值,默认 UTF-8)。
JDK 19 起 `sun.stdout.encoding` / `sun.stderr.encoding` 改名成了标准属性
`stdout.encoding` / `stderr.encoding`(JEP 400),运行器会在缺标准名时自动补一条 ——
JDK 8 ~ 25 一套配置通吃。若 `vmArgs` 已手写同名 `-Dkey`,以手写值为准,注入项被跳过
(终端里有日志)。
### 不写死路径的 JRebel(热部署)
在面板勾选 **自动检测 JRebel**(或在 `launch.json` 里写 `"jrebel": true`),扩展会在运行时
找到 native agent 并把 `-agentpath:<绝对路径>` 追加到 JVM 参数。这样就不用手写
```
-agentpath:C:\\Users\\me\\.vscode\\extensions\\jrebelbyperforcesoftware.jrebel-2026.3.2\\agent\\jrebel\\lib\\jrebel64.dll
```
——那种写法在 JRebel 升级、目录名里的版本号变化的瞬间就悄悄失效(JVM 会以
`Invalid -agentpath argument` 拒绝启动)。
查找顺序,命中即止:
1. `springBootRunner.jrebelAgentPath` 设置 —— 显式覆盖。
2. `vscode.extensions.all` —— 宿主自己的列表,拿到的是真实的 `extensionPath`。
3. 宿主的 `extensions.json` 清单 —— `identifier.id` + `version` + 位置的权威来源,
即使 JRebel 已安装但在本窗口被禁用也能生效。
4. 扩展目录名扫描 —— 最后的兜底。
来源 2 和 3 带版本号,因此装了多个 JRebel 构建时取最新的。目录名扫描不假设任何命名格式:
同一个 JRebel 在 `.vscode` 里叫 `jrebelbyperforcesoftware.jrebel-2026.3.2`,
在 CodeBuddy 里叫 `jrebelbyperforcesoftware.jrebel-2026.3.2-universal`。
注意:
- 选中的文件同时匹配当前平台**与 CPU 架构**(`jrebel64.dll` / `libjrebel64.so` /
`libjrebelaarch64.so` / `libjrebel64.dylib`);载入错误的那一个会让 JVM 报 bad-ELF /
cannot-load 失败。
- 若 `vmArgs` 里已经有 `-agentpath:…jrebel…` 或 `-Drebel.home=…`,注入会被**跳过**
——同一个 native agent 加载两次会让 JVM 中止。发生时你会收到提示。
- `rebel.xml` 仍由 JRebel 自己的向导生成,这里只负责挂载 agent。
- 启动配置时若什么都没找到,会给出带 **设置 agent 路径** 操作的警告,而不是静默失败。
## 面板里的运行 / 已停止状态
正在运行的配置名前会显示绿色 `●`,并把 ▶ / 🐞 换成红色**停止**按钮(■),无需悬停始终可见。
行尾还有一个 **⊗ 清空控制台** 按钮,一键清掉该配置自己的终端输出。点击停止只会杀掉该配置的
进程树 —— 其它配置继续运行。
点下运行 / 调试的那一刻就进入「运行中」:PID 文件是在构建**开始之前**写下的,所以构建还没
跑完时按钮已经是停止态,不会出现手快连点两下、同一终端里叠进两个构建的情况;这时点停止
会连带打断构建。
状态来自运行器写下的按配置区分的 PID 文件
(`%TEMP%/spring-boot-runner--.pid`)加存活检查,因此应用自己退出或被外部
强杀时,指示灯会自行熄灭。每 2 秒轮询一次 —— 进程死亡不像调试会话那样有可订阅的事件。
底部**状态栏**里还有一条同名指示:有服务运行时显示 `$(debug-alt) 配置名`(多条时显示数量)
并铺上橙色底,点击聚焦该配置的集成终端(多条时弹选择框);一个都没跑时整条隐藏,不占位置。
它同样按 PID 文件每 2 秒重算,因此侧边栏收起、或同时跑着好几条配置时也一眼可见。
### 打开面板时它扫什么、有多慢
配置列表(读 `launch.json`)是瞬时的,**先发**;「模块」下拉和「主类」datalist 需要扫描仓库
源码,是这里最慢的一步 —— 700+ 个 java 文件的仓库实测 0.5~1 秒,而旧实现用同步读文件,
还会阻塞扩展宿主(连累其它扩展一起卡),能拖到 4~20 秒。所以:
- 扫描结果**缓存**:收起再展开侧边栏、或 webview 被 VS Code 回收后重开,都直接复用;
- 只有 `pom.xml` / `build.gradle` / `settings.gradle` 出现变化、或切换工作区才重扫;
- 扫描本身已改为异步并发读取(每批 16 个文件、在 Buffer 里搜标记),不再阻塞 VS Code;
- 树工具栏的 **⟳ 重新扫描** 可随时手动丢掉缓存重扫(改完构建结构、pom 监视没触发时用),
状态行会给出开始 / 完成反馈。
`request: "attach"` 字段:`hostName`(默认 `localhost`)、`port`(默认 `5005`)、`timeout`。
attach 配置既不构建也不启动任何东西 —— 只附加调试器,也不会为本扩展创建额外的会话。
所有字符串字段都接受常见的 launch.json 变量 —— `${workspaceFolder}`、
`${workspaceFolderBasename}`、`${userHome}`、`${env:NAME}` —— 无论走 F5(由 VS Code 替换)
还是从运行/调试配置面板启动(由扩展自己替换)。
文件通过 jsonc 编辑,因此**其它调试器(`java`、`node`、`chrome` …)的条目和你自己写的注释都会
保留**。`type` 不是 `spring-boot-runner` 的条目绝不会被碰;无法解析的 `launch.json` 只会被报告,
不会被覆盖。
> 早期版本把配置存在 `.vscode/spring-boot-runner.json`、读平铺的 `springBootRunner.*` 设置。
> 两者**都不再读写** —— `.vscode/launch.json` 是唯一真源。
### `javaHome`(JDK 选择)
`javaHome` 会把 `java` 解析为 `/bin/java`,**并且**为每次 Maven 调用导出
`JAVA_HOME=`(同时把 `/bin` 前置到 PATH),于是 `mvn install` 与启动应用用
的是同一个 JDK —— `mvn.cmd` 正是通过 `JAVA_HOME` 选 java 的。当集成终端里的 `java` 版本不对时
很有用(比如项目要 JDK 21 而 PATH 上是 JDK 17)。
三种设置方式:
- **面板** —— `运行/调试配置…` → `JDK(JAVA_HOME,留空用 PATH 上的 java)` 字段。
- **命令** —— `Spring Boot Runner: 设置 JDK(JAVA_HOME)…` 提示输入路径并存入选定的运行配置。
- **launch.json** —— 直接写 `"javaHome": "C:\\Program Files\\Java\\jdk-21"`。
## Maven 页签:模块树与依赖图
- **模块树**按聚合 / 父子关系嵌套(`` 优先,其次 `` 的 `relativePath`,
再不行按坐标匹配):聚合模块默认展开、叶子默认折叠;工具栏的 `⊞ 展开` / `⊟ 折叠`
一键整树展开或压平。
- 每个模块下:生命周期阶段(hover 出现 ▶ 可直接在终端跑)、声明的插件、依赖项
(打开视图后在后台自动跑 `mvn dependency:tree` 解析,无需手动触发;pom 改动后自动重跑)。
- 树由 [`@vscode-elements/elements`](https://vscode-elements.github.io/) 的
`` 渲染:缩进引导线、展开/选中态与 VS Code 原生树一致,展开/折叠走
组件原生的 `expandAll()` / `collapseAll()`。
- 工具栏的 **⛓ 依赖图** 在**编辑器区**打开一个面板:左边是模块依赖图(节点 = reactor
模块,边 = 模块间依赖,含传递),右边搜索 `groupId:artifactId`(或关键字)会列出
「这个包是从哪条链被引进来的」,每条链按 A → B → C 画出来,`omitted` 的原因在悬停提示里。
- pom 扫描(`detectMavenWorkspace`)与 `dependency:tree` 结果都有缓存:按 pom 文件集合 +
mtime 判脏,没动 pom 就不会重扫;需要时用 `⟳` 手动强制重跑。
## 活动栏入口
扩展贡献了一个**活动栏**容器(叶子+播放图标,`media/activity-icon.svg`),名字叫
**Spring Boot Runner**。点开是一个带一键链接的视图:运行 / 调试 / 自定义参数 / 停止。
注意:VS Code 把活动栏图标渲染成**单色**(主题前景色)—— 彩色的 `images/icon.png` 只会在扩展
面板里显示,永远不会出现在活动栏。顶层 `icon` 字段与活动栏图标是两套独立机制。
## 设置
只剩三个安装级设置;一切描述*一次运行*的内容都在 `launch.json` 里。可通过设置界面或
`.vscode/settings.json` 配置:
| 键 | 默认值 | 含义 |
| --- | --- | --- |
| `springBootRunner.nodePath` | `""` | 运行 `run-spring-boot.mjs` 用的 Node.js 解释器(留空用 PATH 上的 `node`)。只有集成终端里找不到 `node` 时才需要填绝对路径。 |
| `springBootRunner.runnerScript` | `null` | `run-spring-boot.mjs` 的路径(默认用 `scripts/` 下随扩展打包的副本)。 |
| `springBootRunner.jrebelAgentPath` | `""` | JRebel native agent 路径的覆盖。**留空即可** —— 自动探测已覆盖常规情况;只有 JRebel 装在搜索够不到的地方才需要填。支持 `${userHome}`。 |
## 停止是怎么工作的
启动时 `run-spring-boot.mjs` 把自己的 PID 写入
`%TEMP%/spring-boot-runner-[-<配置 id>].pid` —— 后缀用配置的**稳定 id**:
`ConfigStore` 在读到缺少 id 的旧条目时一次性补写进 `launch.json`,F5 与面板拿到的是同一个值,
因此同名配置、改名、复制都不会互相覆盖 PID 与 `@argfile`(旧版本按配置名命名的 pid 文件
在停止时会顺带清一次)。
`停止` **先礼后兵**,给应用留出跑 shutdown hook 的机会:
1. 分离调试器;
2. 向**本扩展自己的**集成终端发 Ctrl-C —— shell 转给前台的 node + java,JVM 得以执行
`@PreDestroy`、关闭连接池、落盘日志(这正是直接 `taskkill /F` 会跳过的部分),
绝不碰你自己开的终端;
3. 轮询最多 8 秒;
4. 没退:Unix 给整个进程组发 SIGTERM(java 与 node 同组,JVM 仍能优雅退出),
Windows 试一次不带 `-F` 的 taskkill,再等 3 秒;
5. 还没退才强杀(`taskkill /PID /T /F` / `SIGKILL`),最后清理 PID 文件。
在 `spring-boot-runner` 调试会话里按 **Stop** 按钮也会触发同样的清理,因此 F5 启动的应用也能
从调试工具栏停止。
## 界面语言 / Language
- **扩展描述与全部字段提示**(命令面板、设置页、IntelliSense)走 VS Code 官方的
`package.nls.json`(English)/ `package.nls.zh-cn.json`(中文),按显示语言自动切换;
Marketplace 也会按访问者的语言显示 displayName 与 description。
- **配置面板与提示消息**由 `src/i18n.ts` 的双语表驱动,同样跟随 VS Code 显示语言。
- **本文档**单文件内双语:顶部 `中文 / English` 点击切换,两版章节一一对应。
- **Descriptions and every field hint** (command palette, settings, IntelliSense) use the official
`package.nls.json` (English) / `package.nls.zh-cn.json` (Chinese) and follow the display
language; the Marketplace shows displayName / description in the visitor's language too.
- **The configuration panel and host messages** are driven by the bilingual table in
`src/i18n.ts`, also following the VS Code display language.
- **This document** carries both languages in one file: use `中文 / English` at the top to switch;
the two sides have matching sections.
## 构建与安装(自己执行)
```bash
cd spring-boot-runner-vscode
npm install # 同时安装 jsonc-parser(用于安全编辑 launch.json)
npm run compile # 产出 dist/extension.js
# 生成可安装的 .vsix:
npx @vscode/vsce package # 需要 @vscode/vsce,会按需安装
```
然后在 VS Code 里:**扩展 → … → 从 VSIX 安装…**,选中生成的 `.vsix`。
本地开发时在该目录按 `F5` 即可启动「扩展开发宿主」。
### 发布到 Open VSX
Open VSX(open-vsx.org)是 VSCodium / Cursor / Windsurf 等非微软发行版使用的注册表。
```bash
# 令牌放在仓库根的 .ovsx-token(已在 .vscodeignore 里,不进 vsix、不入库)
npx ovsx publish spring-boot-runner-.vsix --pat "$(cat .ovsx-token)"
```
- 令牌在 生成(需先登录并 claim `boluoshu` 命名空间);
- **版本号必须递增** —— Open VSX 不允许覆盖已存在的版本;
- 发布后服务端要异步校验与索引,约 1 分钟后
`https://open-vsx.org/api/boluoshu/spring-boot-runner` 才会返回新版本(`ovsx` 当场就打印 Published)。
---
# Spring Boot Runner (plugin-free) — VS Code Extension
> 🌐 **Language:** [Chinese](#chinese) | **English**
A VS Code extension that runs an **unpackaged** Spring Boot (Maven) project straight from the
command line via `java -cp`, without the `spring-boot-maven-plugin` and without the official
VS Code Spring Boot plugin.
It is a thin UI over `scripts/run-spring-boot.mjs`. One core script, one entry point:
- **VS Code extension** → human clicks a command in the editor.
The runner is pure Node (ESM), so **no Python install is required** — only a Maven on PATH and a JDK.
## Source repository
Gitee: `https://gitee.com/boluoshu/spring-boot-runner` (branch `master`)
```bash
git clone https://gitee.com/boluoshu/spring-boot-runner.git
```
## Why
The Spring Boot Dashboard "runs" an app by resolving the `@SpringBootApplication` main class +
classpath, then calling `java -cp `. This extension reproduces that with
plain command-line tools (`maven-dependency-plugin` instead of the IDE's JDT classpath resolver)
and never needs `spring-boot-maven-plugin`. The long classpath is written to a per-project
`@argfile` in the temp dir, so the launched command stays tiny and avoids the Windows
`CreateProcess` command-line length limit.
## Commands
| Command | What it does |
| --- | --- |
| `Spring Boot Runner: Run Project` | Run with a run configuration from `.vscode/launch.json`. Asks which one when several exist; falls back to auto-detect when none does. |
| `Spring Boot Runner: Run Tests…` | Run tests and exit without launching java: `mvn test` / `gradle test`, with an optional filter (e.g. `com.foo.*Test`) that is remembered. |
| `Spring Boot Runner: Run with Debug (JDWP)` | Same, but launches with JDWP (`suspend` per configuration) and auto-attaches VS Code's Java debugger, so breakpoints work immediately. |
| `Spring Boot Runner: Run with Custom Args` | Prompt for extra args (e.g. `--profiles dev --port 8080`), then run. Quote paths with spaces; `--java-home` here overrides the JDK for this run only. |
| `Spring Boot Runner: Configure Java Home…` | Prompt for a JDK home (validated against `bin/java(.exe)`) and store it in the chosen run configuration. Empty input clears it. |
| `Spring Boot Runner: Stop` | Stop a running/debugging app: detaches the debugger, kills the Java process tree via the PID file (`taskkill /PID … /T /F`), and sends Ctrl-C to the terminal as a fallback. |
Output runs in a reusable integrated terminal named **Spring Boot Runner**.
## Run/debug configurations — `.vscode/launch.json`
Run/debug configurations live in the project's **`.vscode/launch.json`**, under this extension's
own debugger type `spring-boot-runner`. They are ordinary debug configurations, so the same file
drives three entry points:
- the **Run and Debug** view (dropdown in the sidebar),
- **F5** (run with debugger) and **Ctrl+F5** (run without debugger),
- the **Run/Debug Configurations…** panel contributed by this extension (an IDEA-style form).
`Ctrl+Shift+P → Spring Boot Runner: Run/Debug Configurations…` opens the panel: a tree of
configurations on the left, the selected configuration's form on the right. There is no settings
file to edit by hand for run parameters — the panel *is* an editor for `launch.json`.
```jsonc
{
"version": "0.2.0",
"configurations": [
{
"type": "spring-boot-runner",
"request": "launch",
"name": "App 1",
"module": "backend/bonlink-agent", // empty = auto-detect
"mainClass": "com.bonlink.PlatformApplication", // empty = auto-detect
"profiles": "dev",
"port": "8081",
"vmArgs": "-Xmx1g -Denv=dev",
"env": "SPRING_DATASOURCE_URL=jdbc:mysql://localhost/db",
"javaHome": "D:\\soft\\Java\\jdk-21.0.8+9",
"skipTests": true
},
{
"type": "spring-boot-runner",
"request": "attach", // runs nothing, only attaches
"name": "Remote JVM debug 1",
"hostName": "10.0.0.5",
"port": 5005
}
]
}
```
`request: "launch"` fields:
| Field | Default | Meaning |
| --- | --- | --- |
| `module` | `""` | Build module directory (Maven pom.xml or Gradle build.gradle(.kts); empty = auto-detect). |
| `mainClass` | `""` | Fully-qualified main class (empty = auto-detect from `@SpringBootApplication`; the panel offers a dropdown of candidates). |
| `programArgs` | `""` | Program arguments, **one per line**; a single line still splits on spaces (legacy form). |
| `vmArgs` | `""` | Extra JVM arguments, **one per line**; a single line still splits on spaces (legacy form). |
| `extraClasspath` | `[]` | **Extra classpath entries — as many as you like.** JSON array (one entry per line in `launch.json`) or a single string split on newlines/`;`. Appended **after** the Maven-resolved classpath, so missing jars / another module's `target/classes` can be added without disturbing existing order. Relative paths resolve against the project root; `${workspaceFolder}` etc. work. Not part of the build cache — changes apply on the next run. |
| `encodingArgs` | a UTF-8 set | **Console encoding args.** Defaults to a UTF-8 set (`file.encoding`, `sun.jnu.encoding`, `sun.stdout.encoding`, `sun.stderr.encoding`, `console.encoding`) — the default applies without writing anything to `launch.json`; an explicit empty array `[]` means "do not set". The panel exposes a *console encoding* dropdown (UTF-8 / GBK / none / custom) that also switches the Windows console code page (`chcp 65001` / `936`) before launching. Same list format as `extraClasspath`. Also appended to the Maven build's `MAVEN_OPTS`; a hand-written `-D` in `vmArgs` always wins. The JDK 19+ standard names (`stdout.encoding` / `stderr.encoding`) are filled in automatically when missing. |
| `profiles` | `""` | `spring.profiles.active`. |
| `port` | `""` | Override `server.port`. |
| `workingDir` | `""` | Working directory of the `java` process (empty = the module directory). |
| `env` | `""` | Extra `KEY=VALUE` env vars, **one per line**; a single line still splits on semicolons (legacy form). |
| `envFile` | `""` | Path to a `.env` file to load. |
| `javaHome` | `""` | **JDK home** for both the Maven build and the launched app (empty = `java` on PATH). |
| `debugPort` | `"5005"` | JDWP port used when debugging. |
| `suspend` | `true` | When debugging, suspend the JVM until the debugger attaches. |
| `jrebel` | `false` | Auto-detect the locally installed JRebel extension and inject `-agentpath:…/jrebel64.dll`. |
| `force` | `false` | Rebuild even when nothing changed (ignore the build cache). |
| `clean` | `false` | Run `mvn clean` before building. |
| `skipTests` | `true` | Skip tests during the build. |
| `offline` | `false` | Maven offline mode (`-o`). |
### Extra classpath (unlimited entries)
Tick nothing special — just fill **额外 classpath** in the panel, one entry per line:
```jsonc
"extraClasspath": [
"${workspaceFolder}/libs/fastjson.jar",
"${workspaceFolder}/other-module/target/classes"
]
```
Each entry is appended to the classpath the runner built from Maven (app classes + reactor
siblings + resolved dependencies). Entries that do not exist are reported as a warning in the
terminal but never abort the run.
### Gradle projects
The build system is detected automatically: a `pom.xml` (at the root or in a subdirectory) means
Maven, only `build.gradle(.kts)` means Gradle — a pure Gradle project no longer fails just because
`mvn` is not on PATH.
- **Build**: prefers the project's own `gradlew` wrapper (version matches the project), then a
`gradle` on PATH; incremental compilation runs `classes` (`clean` → `clean classes`, `force` →
`--rerun-tasks`, `offline` → `--offline`).
- **Classpath**: your build.gradle is never modified — the runner injects a throwaway init script
that registers an `sbrClasspath` task printing `sourceSets.main.runtimeClasspath` (same idea as
what IDEA does with its own model), then deletes it.
- **Output dirs**: Gradle classes live in `build/classes/{java,kotlin,groovy}/main`, not
`target/classes`; the build cache and skip-build checks look at the matching directory, so
incremental behaviour stays the same.
- **Encoding arguments** also go into `GRADLE_OPTS` (what the Gradle daemon and build scripts
read), not just `MAVEN_OPTS`.
### Kotlin / Groovy and the main-class dropdown
Auto-detection scans `.java`, `.kt` and `.groovy` across `src/main/{java,kotlin,groovy}` — Kotlin
Spring projects get a detected main class too.
The main-class field is a **datalist dropdown**: candidates are filtered by the currently selected
module, so you can pick with one click or type freely (typed values stay in the list). A wrong
FQCN used to surface only at run time; the dropdown moves that failure earlier.
### Running tests
`Spring Boot Runner: Run Tests…` runs the tests on the chosen configuration and **exits**: no
classpath assembly, no build-cache write, no java launch. The filter (`com.foo.BarTest`,
`com.foo.*Test`) may be empty to run everything, and the last value becomes the default next time.
Under the hood it is `mvn test -Dtest= -DfailIfNoTests=false` /
`gradle test --tests `; `skipTests` is ignored in test mode (otherwise no test would run
at all).
### JDK version preflight
Before launching, the runner reads `java.version` / `maven.compiler.release` /
`sourceCompatibility` from `pom.xml` (or `build.gradle`) and compares it with the actual `java`
about to be used. When the **JVM is older than the project requires** you get an actionable
warning (Set JDK… / Open Run/Debug Configurations) instead of hitting an unsupported release or
`UnsupportedClassVersionError` and only then hunting for the cause.
If the required version cannot be read, or the actual one cannot be measured, **nothing is
shown** — no warning beats a wrong one. The check runs in parallel with the build and never
blocks the launch.
### Console encoding — fixing garbled Chinese logs
With no arguments injected, a JVM on Windows encodes stdout using `native.encoding` (GBK on a
Chinese system) while the VS Code integrated terminal decodes as UTF-8 — Chinese log lines come
out as `鍵鐩兼枟`-style mojibake. So this extension defaults to UTF-8, in two steps:
1. On launch the terminal first runs `chcp 65001` (`chcp 936` for the GBK option) to switch the
Windows console code page;
2. then a set of encoding arguments is injected into the app JVM. The same arguments go to
`MAVEN_OPTS`, so Maven's own output (compile warnings, dependency downloads) decodes correctly too.
The panel dropdown is the **console encoding**:
- **UTF-8 (recommended, default)** — code page `65001` plus a set of `-D…=UTF-8`;
- **GBK (console switched to 936)** — for external terminals that must keep the system code page;
- **None** — neither switches the code page nor injects anything (written to `launch.json` as an
empty array, so the default does not creep back in);
- **Custom** — tick items individually: `file.encoding`, `sun.jnu.encoding`, `sun.stdout.encoding`,
`sun.stderr.encoding`, `console.encoding` (all take the current value, UTF-8 by default).
Since JDK 19, `sun.stdout.encoding` / `sun.stderr.encoding` were renamed to the standard properties
`stdout.encoding` / `stderr.encoding` (JEP 400); the runner adds the standard name automatically
when missing, so one configuration works across JDK 8 ~ 25. If `vmArgs` already sets the same
`-Dkey`, your hand-written value wins and the injected one is skipped (logged in the terminal).
### JRebel (hot reload) without hard-coding a path
Tick **自动检测 JRebel** in the panel (or write `"jrebel": true` in `launch.json`) and the
extension finds the native agent at run time and appends `-agentpath:` to the JVM
arguments. This replaces hand-writing
```
-agentpath:C:\\Users\\me\\.vscode\\extensions\\jrebelbyperforcesoftware.jrebel-2026.3.2\\agent\\jrebel\\lib\\jrebel64.dll
```
which silently breaks the moment JRebel updates and the version number in the directory name
changes (the JVM then refuses to start with `Invalid -agentpath argument`).
Search order, first hit wins:
1. `springBootRunner.jrebelAgentPath` setting — explicit override.
2. `vscode.extensions.all` — the host's own list, so the real `extensionPath` is used.
3. The host's `extensions.json` manifest — authoritative `identifier.id` + `version` + location,
and it works even when JRebel is installed but disabled in this window.
4. Directory-name scan of the extension folders — last-resort fallback.
Sources 2 and 3 carry a version, so when several JRebel builds are installed the newest wins.
The scan never assumes a directory-name shape: the same JRebel is
`jrebelbyperforcesoftware.jrebel-2026.3.2` in `.vscode` but
`jrebelbyperforcesoftware.jrebel-2026.3.2-universal` in CodeBuddy.
Notes:
- The file picked matches the current platform **and CPU arch** (`jrebel64.dll` /
`libjrebel64.so` / `libjrebelaarch64.so` / `libjrebel64.dylib`); loading the wrong one makes
the JVM fail with a bad-ELF / cannot-load error.
- If your `vmArgs` already contain `-agentpath:…jrebel…` or `-Drebel.home=…`, injection is
**skipped** — loading the same native agent twice aborts the JVM. You are told this happened.
- `rebel.xml` is still generated by JRebel's own wizard; this only mounts the agent.
- When the run configuration is started and nothing was found, you get a warning with a
**Set agent path** action rather than a silent no-op.
## Running / stopped state in the panel
A configuration that is currently running shows a green `●` before its name and replaces the
▶ / 🐞 pair with a red **stop** button (■), always visible without hovering. The row also carries a
**⊗ clear-console** button that wipes this extension's terminal output. Clicking stop kills only
that configuration's process tree — other configurations keep running.
The state comes from the per-configuration PID file the runner writes
(`%TEMP%/spring-boot-runner--.pid`) combined with a liveness check, so the
indicator clears itself when the app exits on its own or is killed externally. It is polled every
2 s because process death, unlike a debug session, emits no event we can subscribe to.
A status-bar entry mirrors that state: while anything is running it shows `$(debug-alt) `
(or a count) on an orange background and hides completely when nothing is up; clicking it focuses
the matching terminal (a picker appears when several are running).
### What the panel scans, and how slow it is
The configuration list (a `launch.json` read) is instant and is sent **first**; the *module*
dropdown and the *main class* datalist need a source scan, which is the slow part here — measured
at 0.5–1 s for a repo with 700+ java files, while the old synchronous file reads also blocked the
extension host (dragging other extensions down with it) and could take 4–20 s. Therefore:
- the scan result is **cached**: collapsing and re-opening the sidebar, or a webview recycled by
VS Code, reuses it instead of scanning again;
- only a change to `pom.xml` / `build.gradle` / `settings.gradle`, or a workspace switch,
invalidates the cache;
- the scan itself now reads files asynchronously in batches of 16 and searches raw Buffers, so it
no longer blocks VS Code;
- the **⟳ rescan** button in the tree toolbar drops the cache and scans again on demand (useful
after restructuring a build when the pom watcher did not fire), reporting start and finish in
the status line.
`request: "attach"` fields: `hostName` (default `localhost`), `port` (default `5005`), `timeout`.
An attach configuration neither builds nor starts anything — only the debugger is attached, and no
extra session is created for this extension.
All string fields accept the usual launch.json variables — `${workspaceFolder}`,
`${workspaceFolderBasename}`, `${userHome}`, `${env:NAME}` — both when started via F5 (VS Code
substitutes them) and when started from the Run/Debug Configurations panel (the extension
substitutes them itself).
The file is edited through jsonc edits, so **entries of other debuggers (`java`, `node`, `chrome`,
…) and any comments you wrote are preserved**. Entries whose `type` is not `spring-boot-runner` are
never touched, and a `launch.json` that does not parse is reported instead of being overwritten.
> Earlier versions stored configurations in `.vscode/spring-boot-runner.json` and read flat
> `springBootRunner.*` settings. Both are **no longer read or written** — `.vscode/launch.json` is
> the single source of truth.
### `javaHome` (JDK selection)
`javaHome` resolves `java` as `/bin/java` **and** exports `JAVA_HOME=` (plus
`/bin` first on PATH) for every Maven invocation, so `mvn install` compiles with the same
JDK that launches the app — `mvn.cmd` picks its java via `JAVA_HOME`. Useful when the integrated
terminal's `java` is the wrong version (e.g. the project needs JDK 21 but PATH has JDK 17).
Three ways to set it:
- **Panel** — `Run/Debug Configurations…` → the `JDK (JAVA_HOME)` field.
- **Command** — `Spring Boot Runner: Configure Java Home…` prompts for the path and stores it in a
chosen run configuration.
- **launch.json** — write `"javaHome": "C:\\Program Files\\Java\\jdk-21"` directly.
## Maven view: module tree and dependency graph
- The **module tree** nests by aggregation / inheritance (`` first, then the
`` `relativePath`, then coordinate matching): aggregators start expanded and leaves
start collapsed; the toolbar's `⊞ Expand` / `⊟ Collapse` opens or flattens the whole tree.
- Under each module: lifecycle phases (hover for a ▶ to run in the terminal), declared
plugins, and dependencies (resolved in the background from a real `mvn dependency:tree`
as soon as the view opens, and re-run automatically after a pom change).
- The tree is rendered with the `` component from
[`@vscode-elements/elements`](https://vscode-elements.github.io/), so indent guides and
expand/selection states match the native VS Code tree.
- The **⛓ Graph** button opens a panel **in the editor area**: on the left a module dependency
graph (nodes = reactor modules, edges = inter-module dependencies incl. transitive ones), and a
search box that lists how a package is pulled in — each hit is drawn as the A → B → C chain,
with `omitted` reasons in the tooltip.
- Both the pom scan (`detectMavenWorkspace`) and the `dependency:tree` results are cached against
the set of pom files plus their mtimes, so nothing is re-scanned until a pom actually changes;
the `⟳` buttons force a rebuild.
## Activity Bar entry
The extension contributes an **Activity Bar** container (leaf+play icon, `media/activity-icon.svg`)
named **Spring Boot Runner**. Clicking it opens a view with one-click links for
Run / Debug / Custom Args / Stop.
Note: VS Code renders Activity Bar icons **monochrome** (theme foreground color) — the colored
`images/icon.png` only shows in the Extensions panel, never in the Activity Bar. The top-level
`icon` field and the Activity Bar icon are two independent mechanisms.
## Settings
Only three installation-level settings remain; everything that describes a *run* lives in
`launch.json`. Set via the Settings UI or `.vscode/settings.json`:
| Key | Default | Meaning |
| --- | --- | --- |
| `springBootRunner.nodePath` | `""` | Node.js interpreter for `run-spring-boot.mjs` (empty = `node` on PATH). Set an absolute path only if `node` is not resolvable from the integrated terminal. |
| `springBootRunner.runnerScript` | `null` | Path to `run-spring-boot.mjs` (defaults to the bundled copy under `scripts/`). |
| `springBootRunner.jrebelAgentPath` | `""` | Override for the JRebel native agent path. **Leave empty** — auto-detection covers the normal cases; use this only when JRebel lives somewhere the search cannot reach. Supports `${userHome}`. |
## How Stop works
On start, `run-spring-boot.mjs` writes its PID to
`%TEMP%/spring-boot-runner-[-].pid` — the suffix is the configuration's
**stable id**: `ConfigStore` backfills it into `launch.json` once for older entries that lack one,
and F5 plus the panel resolve the same value, so same-named configurations, renames and
duplicates never overwrite each other's PID file or `@argfile` (pid files named after the
configulation name by older versions are cleaned up as well).
`Stop` **asks before it kills**, giving the app room to run its shutdown hooks:
1. detach the debugger;
2. send Ctrl-C to **this extension's own** integrated terminal — the shell forwards it to the
foreground node + java, so the JVM can run `@PreDestroy`, close connection pools and flush
logs (exactly what a bare `taskkill /F` skips). Terminals you opened yourself are never touched;
3. poll for up to 8 s;
4. still alive: SIGTERM to the whole process group on Unix (java shares it with node, so the JVM
still exits cleanly), or `taskkill` without `-F` on Windows, then wait 3 more seconds;
5. only then force-kill (`taskkill /PID /T /F` / `SIGKILL`) and remove the PID file.
The same cleanup runs when you press the **Stop** button of a `spring-boot-runner` debug session,
so F5-started apps are stopped from the debug toolbar too.
## Language
- **Descriptions and field hints** (command palette, settings, IntelliSense) use the official
`package.nls.json` (English) / `package.nls.zh-cn.json` (Chinese) and follow the display
language; the Marketplace shows displayName / description in the visitor's language as well.
- **The configuration panel and host messages** are driven by the bilingual table in
`src/i18n.ts`, following the VS Code display language.
- **This document** carries both languages in one file: use `中文 / English` at the top to switch;
the two sides have matching sections.
- **扩展描述与字段提示**走 `package.nls.*` 双份,按显示语言自动切换;Marketplace 按访问者语言
显示 displayName 与 description。
- **配置面板与提示消息**由 `src/i18n.ts` 双语表驱动,同样跟随 VS Code 显示语言。
- **本文档**单文件双语,顶部 `中文 / English` 点击切换,章节一一对应。
## Build & install (run these yourself)
```bash
cd spring-boot-runner-vscode
npm install # also installs jsonc-parser (used to edit launch.json safely)
npm run compile # emits dist/extension.js
# To produce a .vsix you can install in VS Code:
npx @vscode/vsce package # needs @vscode/vsce; installs on demand
```
Then in VS Code: **Extensions → … → Install from VSIX…**, pick the generated `.vsix`.
For local dev, press `F5` in this folder to launch an Extension Development Host.
### Publishing to Open VSX
Open VSX (open-vsx.org) is the registry used by non-Microsoft distributions such as
VSCodium, Cursor and Windsurf.
```bash
# the token lives in .ovsx-token at the repo root (listed in .vscodeignore: never packaged, never committed)
npx ovsx publish spring-boot-runner-.vsix --pat "$(cat .ovsx-token)"
```
- Create the token at (sign in and claim the
`boluoshu` namespace first);
- **the version must increase** — Open VSX refuses to overwrite an existing version;
- after publishing, the server validates and indexes asynchronously; expect about a minute before
`https://open-vsx.org/api/boluoshu/spring-boot-runner` reports the new version (`ovsx` itself
prints “Published” right away).