作为一个天天和 Java 打交道的开发,我最大的感受就是:Windows 上管理多个 JDK 版本,比写业务代码还折磨人。项目 A 必须用 JDK 8,项目 B 得切到 JDK 17,新项目又想尝尝 JDK 21 的特性。每次切换都要打开系统属性,改 JAVA_HOME,改 Path,还要担心新开的命令行窗口到底有没有读到新环境变量。装了多个 JDK 之后,安装包自带的“配置环境变量”选项还会把系统 PATH 搞得一团糟,最后java -version输出的永远不是你想要的那个版本。
所以我自己整理了一套 Windows 下的 JDK 多版本管理方案,核心就两个动作:用 PowerShell 脚本一键安装指定版本的 JDK,然后一条命令秒切版本。这篇文章会把这套方案的完整思路、完整脚本、实测过程和避坑经验都写清楚,适合所有在 Windows 上做 Java 开发、被 JDK 版本切换折磨过的人。
1. 为什么 Windows 下的 JDK 多版本管理这么麻烦
1.1 不同项目对 JDK 版本的硬性要求
Java 生态有个比较特殊的地方:不同框架和工具链对 JDK 版本的要求相当严格。很多遗留系统跑在 JDK 8 上,因为它们用了 javax 包、老的第三方库,换到 JDK 17 很可能直接编译不过。而 Spring Boot 3 又强制要求 JDK 17 起步,部分新组件甚至鼓励用 JDK 21。Gradle、Maven 插件也存在版本兼容矩阵,我见过不少同事在切换项目后第一件事就是重新配置 IDE 里的 JDK。
问题在于,你会同时维护多个项目。今天修一个老年人项目的 bug,明天开发新服务,后天可能还要跑一个临时脚本。每个项目都指定自己的 JDK 版本,指望着“装一个最新版搞定所有事”基本不可能。装上多个版本之后,又得想办法让系统能区分、能快速切换。
1.2 手动改环境变量的三大痛点
最传统的做法就是手动改环境变量,把 JAVA_HOME 指到目标 JDK 目录,再把 Path 里的%JAVA_HOME%\bin提前。听起来简单,实际用起来有不少坑。
第一个坑是容易改错。JDK 的安装目录通常带版本号,比如C:\Program Files\Java\jdk-17.0.12,手滑写错一个字符,环境变量立刻失效。而且某些安装包会在 Path 里同时塞入C:\Program Files\Common Files\Oracle\Java\javapath这种系统路径,它经常排在%JAVA_HOME%\bin前面,导致你用java -version查到的版本根本不是自己设置的。
第二个坑是生效延迟。Windows 环境变量修改后,已经打开的命令行窗口不会自动感知。哪怕你用了setx,新开的 cmd 窗口也未必能拿到最新值,经常要重启终端甚至注销登录。为了切个版本反复重启 IDE、重启终端,非常浪费时间。
第三个坑是环境不一致。你在 IDE 里能选 SDK,在命令行里又依赖 JAVA_HOME,两个地方可能配置成不同版本。于是经常出现“IDE 里编译正常,命令行 mvn 打包却失败”的诡异现象,排查起来相当恼火。
1.3 现有方案和工具的局限
很多人会想到用工具来解决,但 Windows 上的选择确实不多。Linux 下的 SDKMAN 很好用,Windows 却不支持。Scoop 可以安装 JDK,但它本身也是一套包管理生态,还要额外学习和维护。Chocolatey 能装软件,但大多数安装包默认使用 MSI 安装器,依然会写系统环境变量。IDE 里的 SDK 切换只对 IDE 自身有效,出不了 IDE 就管不着。
用 Docker 容器跑 Java 也不是不行,但本地开发改代码、跑测试、看日志都要经过容器,中间多一层转换,调试体验并不好。我更倾向于拿回“管理 JDK 本身”的控制权:把 JDK 当作普通的绿色软件,用脚本统一管理,想切就切。
2. 方案设计思路:为什么选 PowerShell + 本地目录部署
2.1 核心思路:目录化 JDK 存储 + 用户级环境变量动态切换
我这套方案的核心思想很简单:把不同版本的 JDK 全部解压到同一个目录下,统一命名,然后通过修改用户级环境变量来实现版本切换。不需要管理员权限,不碰系统变量,也不写注册表。
具体来说,所有 JDK 都放在%USERPROFILE%\jdks目录下,每个版本一个子目录,比如jdk-8、jdk-17、jdk-21。切换版本时,只需要把用户级 JAVA_HOME 指到对应子目录,再把 Path 中的%JAVA_HOME%\bin调整到最前面,新开的终端窗口就会读到新的 JDK。所有操作都封装成一个 PowerShell 函数,一条命令完成。
这样设计的好处非常明显。第一,所有 JDK 都在一个目录里,删一个版本就是删一个文件夹,干干净净。第二,切换只改环境变量,不卸载不重装,速度快。第三,因为改的是用户变量,不需要管理员权限,在公司电脑上也能用。第四,目录本身就是可以复制的,换电脑时直接把jdks文件夹拷走,环境就能快速恢复。
2.2 为什么不用安装包而用 zip 包
JDK 官方网站一般提供安装包和解压包两种形态。安装包最大的问题:它会在系统里创建卸载记录、写注册表、设置系统 Path,甚至会偷偷加入自动更新任务。对于本地开发机来说,这些“附加功能”不仅没必要,还会干扰多版本管理。
而 zip 包是纯绿色形态,解压后就是一个完整的 JDK 目录。我平时下载用的主要是 Adoptium 发行的 Temurin JDK,它提供 Windows zip 包,无登录、无广告,下载地址还是固定 API,非常适合脚本调用。Temurin 也是目前社区很常用的 OpenJDK 发行版,兼容性不错。
有的版本解压后目录名带版本号和构建号,例如jdk-17.0.12+7,这不利于统一管理。所以脚本里会在安装完成后自动重命名成jdk-17这种稳定名称,切换命令只认稳定名称。这也是不用安装包的一个重要原因:安装包的安装路径不可控,而 zip 包的目录名我们自己说了算。
2.3 为什么用 PowerShell 而不是 .bat / CMD
Windows 下做环境变量管理,PowerShell 比传统批处理顺手太多了。批处理处理字符串很强,但涉及到下载、解压、操作注册表级别的用户环境变量时就比较别扭。PowerShell 能直接调用 .NET API,比如[Environment]::SetEnvironmentVariable,可以明确指定写入 User 级别还是 Machine 级别,非常可靠。
另一个原因是 PowerShell 天生适合做“命令行工具”。我可以定义函数jdk install 17、jdk use 17,让用户像用 Linux 包管理器一样操作。凡是会写一点 PowerShell 的人,都能轻松读懂和修改脚本。而且 PowerShell 是 Windows 自带的,不用额外安装运行时,默认 5.1 版本就够用。
还有一点,PowerShell 函数具有当前会话上下文。我可以在脚本中刷新当前进程的$env:Path,让切换后立刻在当前窗口运行java -version验证结果。批处理很难做到这种“即时体验”。
3. 完整脚本:一键安装 + 秒切版本
3.1 脚本功能一览
整个脚本封装了三个核心操作,全部通过jdk命令入口调用,功能清单如下:
| 命令 | 功能 | 示例 |
|---|---|---|
jdk list | 列出%USERPROFILE%\jdks下已安装的 JDK 版本,并显示版本信息 | jdk list |
jdk install <版本号> | 从 Adoptium 下载指定版本的 JDK,自动解压并统一目录名 | jdk install 17 |
jdk use <版本号> | 将用户级 JAVA_HOME 和 Path 切换到指定版本,并刷新当前终端环境变量 | jdk use 17 |
jdk <版本号> | 直接切换的快捷方式 | jdk 8 |
安装命令默认安装该大版本下最新的 GA 版本,例如jdk install 17会安装最新的 JDK 17 更新。因为是从 Adoptium API 下载,所以不需要自己去找下载链接。
3.2 完整 PowerShell 脚本代码
直接复制以下内容,保存为jdk-switch.ps1:
# jdk-switch.ps1 # 用法:先执行 . .\jdk-switch.ps1 加载函数,再执行 jdk 命令 $JdkRoot = Join-Path $env:USERPROFILE "jdks" function Get-JdkInstalled { if (-not (Test-Path $JdkRoot)) { Write-Host "还没有安装任何 JDK,先运行 jdk install <版本号>" return } $list = Get-ChildItem -Path $JdkRoot -Directory | Where-Object { $_.Name -match '^jdk-\d+' } | ForEach-Object { $javaBin = Join-Path $_.FullName "bin\java.exe" $versionText = if (Test-Path $javaBin) { (& $javaBin -version 2>&1 | Select-Object -First 1).ToString().Trim() } else { "无效安装" } [PSCustomObject]@{ 目录 = $_.Name; 版本信息 = $versionText } } if ($list) { $list | Format-Table -AutoSize } else { Write-Host "JDK 目录下没有可用版本,先运行 jdk install <版本号>" } } function Install-Jdk { param([int]$Version) if ($Version -lt 8 -or $Version -gt 24) { Write-Host "暂不支持这个版本,建议使用 8 到 24 之间的 LTS 版本。" return } $JdkHome = Join-Path $JdkRoot "jdk-$Version" if (Test-Path $JdkHome) { if (Test-Path (Join-Path $JdkHome "bin\java.exe")) { Write-Host "JDK $Version 已安装,目录:$JdkHome" } else { Write-Warning "检测到目录 $JdkHome 但其中没有 bin\java.exe,脚本不会覆盖。请手动删除后再安装。" } return } $existingDirs = Get-ChildItem -Path $JdkRoot -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -match "jdk.*$Version" } if ($existingDirs) { $existing = $existingDirs | Select-Object -First 1 $target = Join-Path $JdkRoot $existing.Name if ($target -ne $JdkHome) { Rename-Item $existing.FullName $JdkHome -Force } Write-Host "检测到已下载的目录,已重命名为 $JdkHome" return } New-Item -ItemType Directory -Force -Path $JdkRoot | Out-Null $tempZip = Join-Path $env:TEMP "jdk-$Version.zip" $apiUrl = "https://api.adoptium.net/v3/binary/latest/$Version/ga/windows/x64/jdk/hotspot/normal/eclipse" Write-Host "正在从 Adoptium 下载 JDK $Version,请稍候..." try { Invoke-WebRequest -Uri $apiUrl -OutFile $tempZip -UseBasicParsing -MaximumRedirection 5 } catch { Write-Error "下载失败:$($_.Exception.Message)" Remove-Item $tempZip -Force -ErrorAction SilentlyContinue return } Write-Host "下载完成,开始解压到 $JdkRoot ..." Expand-Archive -Path $tempZip -DestinationPath $JdkRoot -Force Remove-Item $tempZip -Force $expanded = Get-ChildItem -Path $JdkRoot -Directory | Where-Object { $_.Name -match "jdk.*$Version" -and (Test-Path (Join-Path $_.FullName "bin\java.exe")) } | Select-Object -First 1 if ($expanded -and $expanded.FullName -ne $JdkHome) { Rename-Item $expanded.FullName -NewName "jdk-$Version" -Force } if (Test-Path (Join-Path $JdkHome "bin\java.exe")) { Write-Host "JDK $Version 安装成功,目录:$JdkHome" } else { Write-Error "安装失败:解压后找不到 java.exe,请检查网络源或手动下载 zip 包放到 $JdkRoot 下。" } } function Set-Jdk { param([int]$Version) $JdkHome = Join-Path $JdkRoot "jdk-$Version" if (-not (Test-Path (Join-Path $JdkHome "bin\java.exe"))) { Write-Error "没有检测到 JDK $Version,先执行:jdk install $Version" return } [Environment]::SetEnvironmentVariable("JAVA_HOME", $JdkHome, "User") $userPath = [Environment]::GetEnvironmentVariable("Path", "User") if ($null -eq $userPath) { $userPath = "" } $paths = $userPath -split ';' | Where-Object { $item = $_.Trim() if ([string]::IsNullOrEmpty($item)) { return $false } if ($item -like "*$JdkRoot*") { return $false } if ($item -like "*%JAVA_HOME%*") { return $false } return $true } $newUserPath = ($paths -join ';').TrimEnd(';') + ";%JAVA_HOME%\bin" [Environment]::SetEnvironmentVariable("Path", $newUserPath, "User") $env:JAVA_HOME = $JdkHome $machinePath = [Environment]::GetEnvironmentVariable("Path", "Machine") if (-not $machinePath) { $machinePath =