在 uTools 中一键切换 Windows 的 JAVA_HOME 环境变量。
v2.0.0 为完全重写版本。 不再需要预先手工配置
JAVA8_HOME/JAVA11_HOME之类的中间变量,插件会自行扫描本机已安装的 JDK。
- 自动识别本机 JDK,数据来源按优先级合并去重:
- 手动添加过的目录(会被记住)
- 常见厂商安装目录:
Program Files\Java、Eclipse Adoptium、Microsoft、Zulu、Amazon Corretto、BellSoft、Semeru、SapMachine、JetBrains等,覆盖所有盘符,以及~/.jdks、%LOCALAPPDATA%\Programs\... - 注册表
HKLM\SOFTWARE\JavaSoft\JDK\<版本>\JavaHome(安装器写入的权威记录) - 已存在的
JAVA*_HOME环境变量 PATH中形如xxx\bin的条目反推
- 版本信息来自 JDK 自带的
release文件,不启动java进程,速度快且无副作用。 - 列表展示版本 / 厂商 / 路径,当前生效的一项打勾并标注。
- 支持搜索过滤:输入
17、oracle、adoptium或路径片段均可。 - 写入范围可选:
- 用户级(默认):免管理员,只对当前用户生效,改完立即可用;
- 系统级:弹一次 UAC,对全部用户生效。
- 自动修复 PATH 分两步:切换时只修当前作用域(用户级免 UAC,所以常规切换永远瞬时完成、不会被打断);真正卡住
java的抢占项若在系统 PATH,则以红字项单独提示,点它才弹一次 UAC。刻意不把提权捆绑进切换 —— 否则你只要在 UAC 上点「否」,连JAVA_HOME都写不进去(详见下文)。 - 诊断项(按需出现):PATH 被其他 java 目录抢先(含在另一个作用域里抢先的情形)、PATH 缺少
%JAVA_HOME%\bin、两个作用域中互相冲突的JAVA_HOME、当前JAVA_HOME是无效的间接引用。 - 写入后自检:每次写入都「写 → 回读比对 → 实跑一次
java -version」,把真实版本号显示在结果提示框里;若实测版本与所选 JDK 对不上,会直接点出来,不再有「不知道到底成没成功」。 - 执行结果留在列表里:上一次操作的结果固定显示在列表顶部,失败时标红并写明原因(回车可关闭)。失败时插件不会退出,让你能直接读到问题所在。
- 切换 / 修复完成后弹出结果提示框:独立小窗口居中显示本次结果(成功绿色 / 失败红色),10 秒自动关闭,点「知道了」或按任意键立即关闭,跟随系统深浅色。插件退出后弹窗仍然存活;旧版 uTools 无
createBrowserWindowAPI 时自动回退为系统通知。 - 修复类条目红色高亮:凡是「点一下就能修好」的诊断项,标题与说明会显示为红色并带左侧红条,与普通选项一眼区分(实现原理见下文)。
- 直接输入版本号即可切换:在 uTools 搜索框输入
8/11/17/21等,选择「按版本号切换 Java」即完成,无需进入列表。
方式一(本地开发)
- 安装 uTools 与 uTools 开发者工具;
- 打开开发者工具 → 「选择 plugin.json」→ 指向本目录的
plugin.json; - 直接运行调试。
方式二(打包安装)
在开发者工具中打包为 .upx,双击安装。打包内容只需:plugin.json、preload.js、result.html、logo.png(result.html 是切换/修复后的结果提示框页面,缺失时弹窗功能回退为系统通知)。
在 uTools 搜索框输入 switchjava(也支持 java / javahome / 切换java),进入列表:
✖ 上次操作失败(回车可关闭此提示) ← 上一次执行的结果,失败时标红;成功后显示 ✔
✓ JDK 21.0.6 Oracle · C:\Program Files\Java\jdk-21
JDK 17.0.3.1 Oracle · C:\Program Files\Java\jdk-17.0.3.1 · 当前 JAVA_HOME
⚙ 写入范围:用户级 点击切换为「系统级」:需 UAC,对全部用户生效
+ 手动添加 JDK 目录 扫描不到时手动指定 JDK 根目录
⟳ 重新扫描本机 JDK 忽略缓存立即重新扫描
☑ 切换时自动修复 PATH 只在抢占确实存在时修复对应作用域
⚠ 修复 PATH 抢占(改系统级) ← 修复类条目:红色标题 + 左侧红条
选中某个版本后按回车即可完成切换。子输入框中可输入版本号或厂商名进行筛选。
切换成功后会自动退出插件(uTools 的常规行为);失败时不会退出,而是把原因留在列表顶部,方便你直接看到问题出在哪。
先说结论:不能在 title 里写 HTML 来变色。
uTools 模板列表的 title / description 是按纯文本渲染的。uTools 7.8.0 客户端(resources/app.asar 内的列表组件)渲染代码为:
createElement('div', { className: 'list-item' + (isSelected ? ' list-item-selected' : '') }, …)
createElement('div', { className: 'list-item-title' }, item.title)
createElement('div', { className: 'list-item-description' }, item.description)title 是作为 React 的文本子节点插入的,标签会被转义;官方也没有开放 item 的样式字段。写 <span style="color:red"> 只会把标签原样显示出来。
可行的做法是利用一个事实:模板插件的 preload 与列表界面处于同一个渲染进程、同一个 DOM 中(列表组件直接调用 window.exports[code].args.enter / search)。因此本插件:
- 往
document.head注入一段样式(#switchjava-fix-style,含深/浅色两套配色); - 刷新列表后,给「修复类」条目的
.list-item节点补上switchjava-fix类 —— 呈现红色标题 + 红色说明 + 左侧红色竖条,选中时底色也变红; - 列表节点由 React 管理,鼠标移动导致选中态变化时 React 会重写
className抹掉这个类,所以用一个幂等的MutationObserver把标记补回来; - 离开插件时通过
utools.onPluginOut撤销全部标记,不在其他插件的列表里残留样式。
全部只是配色:注入失败、DOM 拿不到、uTools 改了内部结构,都只影响颜色,不影响切换功能——条目标题仍保留 ⚠ 前缀作为兜底标识。
这是原版本最容易踩的坑:即使 JAVA_HOME 已正确切换,java -version 仍可能显示旧版本。
原因是 Oracle 安装器会在 PATH 中写入 C:\Program Files\Common Files\Oracle\Java\javapath,该目录下的 java.exe 是一个启动器,它读取注册表里「最后一次安装的 JRE」来决定运行哪个 Java,完全不看 JAVA_HOME。只要它排在 %JAVA_HOME%\bin 之前,就会被优先命中。
插件不会在每次切换时都去动系统 PATH —— 那样每次切换都要弹 UAC,而一旦你在 UAC 上点「否」,连 JAVA_HOME 都写不进去。因此拆成两件独立的事:
一、切换版本(只写当前作用域,用户级免 UAC)
- 若当前作用域的 PATH 确实需要修复,先把
%JAVA_HOME%\bin前置(用户级 PATH 不需要管理员权限); - 写入
JAVA_HOME; - 读取 PATH 的原始未展开值并以
REG_EXPAND_SZ类型写回,避免%VAR%被写死、PATH 损坏。
二、修复系统 PATH 的抢占项(显式操作,弹一次 UAC)
真正卡住 java 的往往是系统 PATH 里的 javapath。这种情况会以红字项「⚠ 修复 PATH 抢占(改系统级)」出现在列表里,点它并同意 UAC 即可;插件会自行判断该修哪个作用域。
两处操作都是幂等的:重复执行不改变已有内容,也不会移除 PATH 中的其他条目(javapath 只是被排到后面,不会被删掉)。
Windows 合并 PATH 的顺序是 系统 PATH 在前 + 用户 PATH 追加在后。本机用 CreateEnvironmentBlock 取到的合并结果:
[ 3] C:\Program Files\Common Files\Oracle\Java\javapath ← 抢占项,在系统 PATH 里
[11] C:\Program Files\Git\cmd ← 机器独有项
[12] C:\Program Files\Java\jdk-17.0.3.1\bin ← 用户 PATH 的 %JAVA_HOME%\bin 展开而来
[13] C:\Program Files (x86)\pcsuite\ ← 用户独有项
机器独有项(索引 11)排在用户独有项(索引 13)之前 —— 这就是「系统 PATH 在前」的直接证据。
所以当 javapath 位于系统 PATH 时,只在用户 PATH 里前置 %JAVA_HOME%\bin 是徒劳的;插件会把修复目标自动定为系统 PATH(这也解释了此前的「点了修复却没变化」,其根因还有一处:提权脚本误用了不存在的 HKLM\Environment 键,已修正为 HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment)。
修复一次之后就不用再管:系统 PATH 里的 %JAVA_HOME% 同样会用合并后的 JAVA_HOME 展开(用户级覆盖系统级),所以之后无论写用户级还是系统级,java 都会跟着变。
这里踩过一个很隐蔽的坑,值得记下来。
提权需要先起一个非管理员进程,再由它用 Start-Process -Verb RunAs 拉起管理员进程。最初为了"杜绝引号注入",代码把逻辑脚本用 base64 编码后塞进 -EncodedCommand,而外层脚本本身也走 -EncodedCommand —— 于是整个脚本被 base64 了两层:
逻辑脚本 8250 字符
→ base64(UTF-16LE) ≈ 22000 字符(塞进外层脚本)
→ 外层脚本 22132 字符
→ base64(UTF-16LE) ≈ 59020 字符 ← 这才是真正传给 CreateProcess 的命令行
而 Windows 命令行上限是 32767 字符。超限后 CreateProcess 直接失败,PowerShell 子进程根本不会被创建。现象就是:点「修复 PATH」毫无反应、再打开列表修复项还在、连切换版本也一起失败(因为当时切换捆绑了系统级修复,所以每次切换都在走这条注定失败的路)。
修正方式是把逻辑脚本落成临时 .ps1,外层只负责用 -File 拉起它:
Start-Process -FilePath <powershell.exe> -Verb RunAs -WindowStyle Hidden -PassThru -Wait `
-ArgumentList '-NoProfile','-NonInteractive','-WindowStyle','Hidden','-ExecutionPolicy','Bypass','-File',<临时脚本>命令行长度因此从约 59000 降到约 1000,且与脚本规模彻底解耦;引号注入面依然是零(路径全部由插件生成、经 ' → '' 转义)。两个容易忽略的配套细节:
- 临时
.ps1必须写成 UTF-8 with BOM。PowerShell 5.1 看不到 BOM 会按 ANSI 解码,脚本里的中文通知全部乱码。 - 取消 UAC 时外层以退出码
1223(ERROR_CANCELLED)回传,插件据此给出「已取消管理员授权,未做任何修改」,而不是含糊的"执行失败"。
test/exec-check.js 把这套约束固化成了断言(含"提权命令行必须小于 32767"和"外层必须用 -File"),防止回归。
每次写入都会「写 → 回读比对 → 实跑一次 java -version」,结果显示在弹出的提示框里,例如:
JAVA_HOME → JDK 21.0.6(用户级);新进程实测 java -version → openjdk version "21.0.6" 2025-01-21 LTS
如果 java -version 仍与所选版本不符,消息里会带上实际命中的 java.exe 路径,便于定位。
执行环境变量写入要拉起 PowerShell 子进程,默认会在任务栏和屏幕上闪出一个控制台窗口。插件做了双保险:
- Node 侧
execFileSync(..., { windowsHide: true }); - PowerShell 参数与
Start-Process都带-WindowStyle Hidden—— 提权场景尤其需要后者:UAC 授权后新创建的进程只有收到-WindowStyle Hidden才会把自己的控制台窗口藏起来,只靠 Node 侧参数压不住。
切换成功后插件会调用 utools.outPlugin() 退出,但提示框还在。依据(读本机 uTools 7.8.0 客户端源码确认):outPlugin 的实现只处理调用方自己的窗口,不会遍历销毁该插件用 createBrowserWindow 创建的独立窗口。提示框是插件目录内的 result.html,结果数据经 URL 查询参数传入(页面用 textContent 以纯文本注入,不解析 HTML);页面内 10 秒倒计时结束、点「知道了」或按任意键都会调用 window.close() 自行关闭。若当前 uTools 版本没有 createBrowserWindow,则回退为 showNotification。
Windows 的进程环境块是进程启动时复制的一份快照。修改注册表后,已经运行的终端 / IDE / 编辑器不会自动更新。
插件在写入后会显式广播 WM_SETTINGCHANGE(lParam = "Environment"),让资源管理器等已运行的进程重新读取环境块。这一步是必要的:直接写注册表(.NET RegistryKey.SetValue、[Environment]::SetEnvironmentVariable)不会自动广播,只有 setx.exe 会。少了它,「新开的窗口仍是旧值」就会频频出现。
即便如此,已经打开的终端窗口永远不会刷新(它继承的是自己启动时的环境块),在旧终端里再敲 cmd 也只是继承同一个旧块。所以请新开一个终端窗口(从开始菜单 / 桌面 / 任务栏)。
或者直接看 uTools 通知末尾的 java -version 实测值 —— 插件是在独立进程里按最新注册表重新拼出合并 PATH 后再运行的,不依赖任何窗口刷新,因此它就是「新进程会看到什么」的权威答案。
| v1 | v2 | |
|---|---|---|
| 前置条件 | 必须手工新建 JAVA8_HOME ~ JAVA21_HOME |
无需任何前置配置 |
| 支持的版本 | 固定 8 / 11 / 17 / 18 / 21 | 扫描到什么就支持什么 |
JAVA_HOME 的值 |
%JAVA8_HOME%(变量的变量) |
JDK 真实绝对路径 |
| 临时文件 | 写在 uTools 程序目录 | 不产生持久文件 |
| 切换脚本 | 运行时生成 .vbs 并执行 |
直接由 PowerShell 完成 |
关于 %JAVA8_HOME% 这种间接引用:Windows 只在 REG_EXPAND_SZ 类型的值上做展开,而多数工具(IDEA、Maven、Gradle、各类构建脚本)直接读取 JAVA_HOME 的字符串,不会二次展开。因此 v1 写入的 JAVA_HOME=%JAVA8_HOME% 在这些工具眼中就是一个无效路径。v2 写入真实路径。插件也不会删除你原有的 JAVAxx_HOME 变量,只是把它们当作「JDK 位置提示」读取。
插件逻辑全部在 preload.js(CommonJS,无构建步骤、无依赖)。它遵循 uTools 的「模板插件应用」规范:plugin.json 不含 main 字段,通过 window.exports 声明列表模式与无 UI 模式。
不安装 uTools 也能验证核心逻辑:
node test/local-check.js # 用本机真实环境跑一遍扫描与列表生成
node test/exec-check.js # 执行层约束:不提权 / 命令行上限 / BOM / 结果可见(无依赖)
node test/list-style-check.js # 在 jsdom 里复刻客户端列表 DOM,验证红色高亮(需 jsdom)test/local-check.js 会 mock 掉 utools 对象,直接调起 enter / search 回调并打印生成的列表,用于确认 JDK 扫描与过滤是否正常。
test/exec-check.js 是最重要的一道防线,因为它补上了此前的验证盲区:旧测试只检查"生成的脚本文本长什么样",从未检查它能否被启动,于是漏掉了那个致命的两层 base64 缺陷。它拦截 execFileSync 拿到真实的启动参数,断言:用户级切换不提权、提权命令行小于 32767、提权用 -File 而非再套一层 base64、临时脚本带 UTF-8 BOM、机器作用域的注册表键写对、以及操作结果确实留在列表里且失败时可关闭。
test/list-style-check.js 需要 jsdom(npm i -D jsdom,或设置 NODE_PATH 指向已安装目录)。它把 reg.exe 的查询结果替换成脚本自己构造的桩,因此环境变量场景完全可控,断言与本机装了什么无关。覆盖:标记与 _fix 标志一一对应、React 重写 className 后标记自动补回、列表切换不残留标记、onPluginOut 撤销标记、title 内嵌 HTML 不被解析、跨作用域冲突的判定方向、以及修复目标是否落在真正抢先的那个作用域(场景 A 即复刻「抢占项在系统 PATH」这一真实情形)。
- 仅支持 Windows(
plugin.json已声明platform: ["win32"],运行时也会二次校验)。 - 写入系统级环境变量必须获得管理员权限。取消 UAC 不会造成静默失败:用户级下的切换(写
JAVA_HOME)已经完成,取消的只是系统 PATH 修复,插件会明确区分这两件事。 - UAC 弹窗与 HKLM 实际写入无法离线自动化测试(
test/exec-check.js只能验证到"启动参数正确、脚本能解析并执行"这一层),首次使用请人工确认一次。 JAVA_HOME的作用域优先级:它是普通变量,同名的用户级变量会覆盖系统级变量。所以「写系统级 + 用户级已有值」会让本次设置失效,插件会红字提示并支持一键清除;反过来「写用户级」是能盖住系统级的,不算冲突,不会报警。- PATH 是拼接而非覆盖,且系统 PATH 在前:该顺序由插件自动识别,并把
%JAVA_HOME%\bin前置到真正卡住的那个作用域(可能是系统 PATH,需 UAC)。列表里的红字项会写明修复目标。 - 修复只做「重排」,不会删除
javapath等条目。若你想彻底移除它,请在「系统变量」里自行删除。 - 红色高亮依赖 uTools 列表组件的内部 DOM 结构(类名
list-item/list-item-title)。若未来版本改结构,最坏情况只是高亮失效,条目仍带有⚠前缀。
见 LICENSE。