皆さんこんにちは。システム推進の佐藤太一です。
このエントリでは、Windows上にインストールした複数のJVMを上手く切り替える方法について説明します。
- はじめに
- JBangのインストール
- JBangを使ったJavaのインストール
- デフォルトのJavaを設定する
- JBangを使ってJavaを切り替える
- PowerShellの補助関数を使って切り替える
- PowerShellのプロファイルを作りこんで自動で切り替える
- 類似するツール
- まとめ
はじめに
Javaでアプリケーション開発をしていると、気が付いたらたくさんのJVMをインストールしていませんか?
私はCorretto やAdoptOpenJDKなど様々なディストリビューションのJava 8、Java 17、Java 21、Java 23をそれぞれインストールしています。
eclipseやIntelliJといったIDE上でのビルドでは、IDEがランタイムの選択をサポートしてくれます。
Gradle や Maven にはtoolchainの仕組みがありますので、一旦ビルドが始まってしまえば、適切なランタイムが選択されるでしょう。
しかし、Windowsではそこに至るまでに動作するJVMを上手く管理する方法が少ないです。
このエントリでは、JBang を使って複数のバージョンのJavaをインストールして管理する方法を説明します。
最後はMacやLinuxでは日常的に行われているバージョンの自動切り替えについても、WindowsのPowerShellを使って実現する方法をご紹介します。
今回の説明で使うPowerShellは7.4系です。事前にPowerShellとscoopをインストールしておいてください。
JBangのインストール
まずは、JBangをインストールしましょう。非常に丁寧に書かれたインストールマニュアルがあるので、それほど迷うことはないはずです。 cf. Installation
今回はscoopを使ってインストールしますので以下のコマンドを実行します。
scoop bucket add jbangdev https://github.com/jbangdev/scoop-bucket scoop install jbang
JBang用のバケットを追加した上で、そのバケットからインストールしていますね。
JBangを使ったJavaのインストール
JBangをインストールしたら、次はJavaをインストールします。
JBangで簡易的にインストールできるディストリビューションはAdoptOpenJDKのみです。
それ以外のディストリビューションをインストールしたいのであれば、別途インストールした上でJBangの管理下に置く必要があります。今回は簡易的なインストールだけを説明します。
例えば、Java 8 をインストールするなら以下のコマンドを実行します。
jbang jdk install 8
次は、Java 17と21をインストールしてみましょう。
jbang jdk install 17 jbang jdk install 21
ディストリビューションが固定されているので、コマンドが非常に簡潔ですね。
インストール済みのJavaを一覧にしてみましょう。以下のコマンドを実行します。
jbang jdk list
私の環境では以下のように出力されます。
Installed JDKs (<=default): 8 (1.8.0_412) 17 (17.0.12+7) 21 (21.0.1+12-29) <
21 の右端に < が付いているのは、これをデフォルトのJavaに設定しているからです。
デフォルトのJavaを設定する
では、JBangを使ってデフォルトのJavaを設定しましょう。
jbang jdk default 21
これで、デフォルトのJavaが21になりました。ついでに環境変数も追加しておきましょう。
JBangによってインストールしたJavaのフルパスを確認するには、以下のコマンドを実行します。
jbang jdk java-env
これを実行すると、以下のように出力されます。
$env:PATH="C:\Users\taichi\.m2\jdks\jdk-21.0.1\bin;$env:PATH" $env:JAVA_HOME="C:\Users\taichi\.m2\jdks\jdk-21.0.1" # Run this command to configure your environment: # jbang jdk java-env | iex
ユーザのホームディレクトリ以下にファイルが作成されている様子を確認できますね。
JBangは、jdk java-env
コマンドの実行結果をiexつまり、Invoke-Expressionすることで実行環境を切り替えていくツールというわけです。
何もしていない時に動作するJavaを指定するために、Windowsの環境変数を設定するツールでPATH環境変数とJAVA_HOME環境変数を設定しておきましょう。
JBangを使ってJavaを切り替える
次は、PowerShell上で利用するJavaを切り替えてみましょう。
まず、PowerShellのターミナルを開いて現在のJavaを確認します。
C:\Users\taichi> java -version openjdk version "21.0.1" 2023-10-17 OpenJDK Runtime Environment (build 21.0.1+12-29) OpenJDK 64-Bit Server VM (build 21.0.1+12-29, mixed mode, sharing)
PATH環境変数にJava 21が設定されているので、このように表示されます。
これを、Java 17に切り替えてみましょう。以下のコマンドを実行します。
jbang jdk java-env 17 | iex
これで切り替わりましたので、確認してみましょう。
C:\Users\taichi> java -version openjdk version "17.0.12" 2024-07-16 OpenJDK Runtime Environment Temurin-17.0.12+7 (build 17.0.12+7) OpenJDK 64-Bit Server VM Temurin-17.0.12+7 (build 17.0.12+7, mixed mode, sharing)
切り替わっていますね。MavenやGradleが参照するJAVA_HOME環境変数も確認してみましょう。
C:\Users\taichi> $env:JAVA_HOME C:\Users\taichi\.jbang\cache\jdks\17
正しく切り替わっています。
PowerShellの補助関数を使って切り替える
JBangを使った切り替えはコマンドが少し長いので、覚えていられないという問題があります。
そこで、PowerShellのProfileという機能を使って切り替え用のコマンドを実装してみましょう。
プロファイルの詳細については、以下のドキュメントを参照してください。
要は$PROFILE
でパスを確認できるPowerShellスクリプトにコードを書いておくとPowerShellが起動されるたびに、それが自動的に動く仕組みです。
プロファイルに以下のような関数を定義します。
function global:switchJava([string]$version) { jbang jdk java-env $version | iex java -version }
以下のコマンドを実行してプロファイルをリロードします。
. $PROFILE
補助関数を実行して利用するJavaのバージョンを変更してみましょう。新しいターミナルを起動して、以下のコマンドを実行します。
switchJava 17
以下のように出力されてJavaのバージョンが切り替わります。
C:\Users\taichi> switchJava 17 openjdk version "17.0.12" 2024-07-16 OpenJDK Runtime Environment Temurin-17.0.12+7 (build 17.0.12+7) OpenJDK 64-Bit Server VM Temurin-17.0.12+7 (build 17.0.12+7, mixed mode, sharing)
PowerShellのプロファイルを作りこんで自動で切り替える
補助関数でのバージョン切り替えは便利ですが、ターミナルを起動するたびに利用するべきJavaのバージョンを確認して切り替えていくのは面倒です。
現在のディレクトリ内にある設定ファイルを見て利用するJavaを自動的に切り替えてほしいですよね。
Windows以外の環境で動作するbashやzshといったシェルでは、例えば.java-version
のようなファイルの存在確認をしてその中身によってバージョンを切り替えるという事が一般的に行われています。
Windowsではあまり広く行われていませんが、PowerShellを使えば実装可能であることを紹介します。
まずは、プロファイルに以下のコードをそのまま追加します。
ただし、既にプロファイルをカスタマイズしている人は global:prompt
の編集だけ注意してください。
要は、prompt関数の好きな場所に autoJava
を追加すれば動作します。
<# Copyright 2024 DENTSU SOKEN INC. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. #> function global:prompt { $origLastExitCode = $LASTEXITCODE autoJava $LASTEXITCODE = $origLastExitCode } function switchJava([string]$version) { $SB = { param([Parameter(Mandatory = $true)] $version) jbang jdk java-env $version if ($LASTEXITCODE -ne 0) { throw "jbang jdk java-env command failed with exit code $LASTEXITCODE" } } $output = Start-ThreadJob -ScriptBlock $SB -ArgumentList $version | Receive-Job -Wait $output -join "`n" | iex java -version } $global:lastKnownLocation = "" function autoJava { $p = $PWD.Path if ($p -ne $global:lastKnownLocation) { guessJava $global:lastKnownLocation = $p } } function guessJava { $versionFile = Join-Path -Path $PWD -ChildPath ".java-version" if (Test-Path $versionFile) { $content = Get-Content $versionFile $version = $content.Trim() if (findJava $version -eq $true) { $res = switchJava $version } else { Write-Host ".java-versionファイルでは $version が指定されていますが、そのバージョンのJavaはインストールされていません。" Write-Host "jbang jdk install $version を実行するとインストールできるかもしれません。" } } else { useDefaultJava } } function useDefaultJava { foreach ($tup in listJava) { if ($tup.IsDefault -eq $true) { switchJava $tup.Label return } } } function findJava([string]$version) { foreach ($tup in listJava) { if ($tup.Label -eq $version) { return $true } if ($tup.FullVersion -eq $version) { return $true } if ($tup.FullVersion.StartsWith($version)) { return $true } } return $false } function listJava { $SB = { jbang jdk list if ($LASTEXITCODE -ne 0) { throw "jbang jdk list command failed with exit code $LASTEXITCODE" } } $output = Start-ThreadJob -ScriptBlock $SB | Receive-Job -Wait return $output | Select-Object -Skip 1 | ForEach-Object { if ($_ -match '^\s*(\d+)\s+\(([\d\._\+\-]+)\)\s*(<)?\s*') { [PSCustomObject]@{ Label = $Matches[1] FullVersion = $Matches[2] IsDefault = $Matches[3] -eq "<" } } } }
コードの細かい処理内容については説明しませんが、いくつかある奇妙な部分について説明します。
Start-ThreadJob を使う理由
jbangコマンドがPowerShellスクリプトだからです。
prompt関数からPowerShellスクリプトが推移的に呼び出されると、呼び出されたスクリプトの終了時点で解釈が終了するのです。
私の場合は、posh-git などを使ってプロンプトを装飾しています。
prompt関数の戻り値がないと、デフォルトのプロンプトになってしまうので困ります。
というわけで、jbangコマンドをジョブとして実行してその結果の標準出力だけを使っているのです。
バージョンの評価ロジックについて
findJava関数に実装してあるバージョンの評価ロジックは非常に単純なものです。
- JBangでインストールされているバイナリのメジャーバージョン番号との完全一致
- JBangでインストールされているバイナリの詳細なバージョン番号との完全一致
- JBangでインストールされているバイナリの詳細なバージョン番号との前方一致
.java-versionに記載されているバージョン番号は多くの場合に、メジャーバージョン番号のみです。
Javaは非常に高度な互換性を保ちながら開発されているので、日常的な開発において細かいリビジョン番号やパッチ番号を気にする必要はないと考えています。
もし細かい処理が必要な方はこのコードを是非改善して、その内容をブログに書いて貰えるとありがたいですね。
グローバル変数を使った処理回数の低減
prompt関数は、PowerShellのプロンプトが表示されるたびに実行される関数なので非常に実行回数が多いものです。
なので、カレントディレクトリが変更されるまではJavaのバージョンを確認する処理をしないようにしてあります。
そのために使っている変数が lastKnownLocation
です。
類似するツール
まとめ
このエントリでは、JBangとそれを使ったPowerShellスクリプトを紹介しました。
このスクリプトは少し応用すれば、nodeやPythonといった他の言語でも利用できるものです。
このエントリによって、Windows環境で快適に開発できる技術者が少しでも増えることを願っています。
執筆:@sato.taichi、レビュー:@yamashita.tsuyoshi
(Shodoで執筆されました)