Claude CodeとOpenRewriteでJava 21へ上げる — mvnの残りエラーを直す
Java 21への移行を、OpenRewriteのレシピとClaude Codeで分担する手順です。mvn rewrite:runをpermissionsで通し、機械変換で残ったエラーをClaudeに直させる切り分けです。
Java 21への移行は、OpenRewriteのUpgradeToJava21レシピをmvn rewrite:runで先に回し、機械変換されなかった差だけをClaude Codeに直させる二段にすると、見通しが良くなります。レシピが書き換えられる範囲は決まっています。そこをClaudeに手で書かせると、差分が大きくなり、レビューも重くなります。
この記事では、permissionsで通すコマンドを3つに絞る設定から、残ったエラーをClaudeに渡す順番までを扱います。移行前に何を整えておくかは着手前の6ステップ、移行計画の立て方はレガシーコード移行の進め方にあります。ここではMavenプロジェクトを前提に、実行段階の分担だけを書きます。
先にOpenRewriteを回す理由は、変換の範囲が決まっているから
UpgradeToJava21は、Java 21への移行で「よく必要になる変更」を適用するレシピです。非推奨APIは、置き換え先がはっきりしているものだけ差し替えます。ビルドファイルのtarget/sourceも21に上げ、プラグインはJava 21に対応する版へ上げます。
定義には、次のような個別レシピが並んでいます。
| 分類 | 個別レシピの例 |
|---|---|
| 前段の移行 | 個別レシピの例Java 17への移行(未到達なら8から11、17へと進む) |
| ビルド | 個別レシピの例Java 21へのビルド更新、actions/setup-javaのJavaバージョン更新、プラグインの対応版への更新 |
| API置換 | 個別レシピの例new URL(String)をURI.create(String).toURL()へ、new Locale(..)をLocale.of(..)へ、Runtime#exec()の非推奨メソッドの置換 |
| コレクション | 個別レシピの例SequencedCollectionの採用 |
| 構文 | 個別レシピの例if-else-ifのswitch化、switchの式化、switchのパターンマッチング(JEP 441)の採用 |
| 削除 | 個別レシピの例余分なセミコロン、空のfinalize() |
公式のガイドには、変換前後のコードが載っています。たとえばentityList.get(0)がgetFirst()に、foo.get(foo.size() - 1)がgetLast()に変わります。
ここで押さえたいのは、レシピの守備範囲が「置換先が一意に決まる変更」であることです。公式のガイドも、実行後はgit diffで結果を確かめ、自動で移行できなかった箇所は手で直し、コミットする流れを案内しています。この「手で直す」部分が、Claude Codeに任せる範囲です。
機械変換と残差の分担
OpenRewrite
pomのバージョン、非推奨APIの置換、switchの書き換えなど、規則で決まる変更を一括で適用します。
Claude Code
レシピが触らなかった削除済みAPI、モジュール境界、リフレクションの失敗など、ビルド出力から原因を読む変更を直します。
レシピをpomに宣言し、実行コマンドを固定する
公式のMaven用設定は、rewrite-maven-pluginにactiveRecipesと、レシピを含むモジュールrewrite-migrate-javaを依存として書く形です。ガイドの例は、プラグイン6.51.0とレシピ集3.47.0を指定しています。
<plugin>
<groupId>org.openrewrite.maven</groupId>
<artifactId>rewrite-maven-plugin</artifactId>
<version>6.51.0</version>
<configuration>
<activeRecipes>
<recipe>org.openrewrite.java.migrate.UpgradeToJava21</recipe>
</activeRecipes>
</configuration>
<dependencies>
<dependency>
<groupId>org.openrewrite.recipe</groupId>
<artifactId>rewrite-migrate-java</artifactId>
<version>3.47.0</version>
</dependency>
</dependencies>
</plugin>版番号はガイドに載っている例の値です。実際に使うときは、公式のレシピ頁で最新の値を確かめてください。
pomにレシピを書いておく利点は、実行コマンドが短く固定できることです。activeRecipesは-Drewrite.activeRecipes=...でコマンドラインから渡すこともできます。ただし渡し方が自由だと、Claudeが別のレシピを混ぜて回せてしまいます。pomに宣言しておけば、mvn rewrite:runと打つだけで、回るレシピがpomの内容に決まります。
permissionsで通すコマンドは3つに絞る
Bashの許可ルールは、*が無ければコマンド全体の完全一致です。Bash(npm run build)はnpm run buildに合い、npm run build --watchには合いません。この性質を使って、移行で必要な3コマンドだけを固定文字列で通します。
{
"permissions": {
"allow": [
"Bash(mvn rewrite:dryRun)",
"Bash(mvn rewrite:run)",
"Bash(mvn -q -B -ntp verify)"
],
"deny": [
"Bash(git push *)",
"Read(~/.m2/settings.xml)"
]
}
}この設定でClaudeに任せられるのは、変更を試算するdryRun、変更を書き込むrun、全体を確かめるverifyです。-q -B -ntpの意味はMavenビルドの出力の絞り方に書きました。
ルールの効き方で、先に知っておく点が3つあります。
mvn rewrite:run -Drewrite.activeRecipes=...のように引数が増えると、完全一致のルールには合いません。許可プロンプトが出るので、別レシピを回すときは人が見て承認できますmvn rewrite:run && mvn verifyのような複合コマンドは、サブコマンドごとに別々にルールと照合されます。両方がallowに入っていれば通ります./mvnwで回すプロジェクトでは、コマンド文字列が変わります。Bash(./mvnw rewrite:run)のように、実際に使う形でルールを書き直します
~/.m2/settings.xmlのdenyは、資格情報の置き場を守るためです。理由は、認証トークンの節に書きます。許可ルールの書き方全般は/permissionsコマンドの解説も参照してください。
実行の順序は、dryRun、run、コミット、verify
手順は4段です。それぞれ別のコミット境界を置くと、Claudeが直した分と機械変換の分が混ざりません。
移行の実行順
- 1
作業ブランチを切り、dryRunを回す
作業ツリーが綺麗な状態で
mvn rewrite:dryRunを実行します。ソースは変わらず、変更予定の一覧がビルドログに出ます。 - 2
rewrite.patchを読む
dryRunは、適用した場合の差分を
target/site/rewrite/rewrite.patchに書き出します。Claudeに読ませて、意味が変わる変更がないか先に洗います。 - 3
runを回して、そのままコミットする
mvn rewrite:runでソースを書き換え、git diffで確かめて、機械変換だけのコミットにします。 - 4
verifyの出力をClaudeに渡す
mvn -q -B -ntp verifyを回し、失敗したエラーだけを直させます。直しは別コミットに分けます。
dryRunとrunが別のゴールなのは、作業の確認ができるからです。公式の説明では、rewrite:runは変更をディスクに書き込み、ビルドログには変更した箇所の警告を出します。rewrite:dryRunはソースを一切変えません。
機械変換のコミットを分けるのは、後でClaudeの差分だけをレビューできるようにするためです。直しの途中で方針が崩れたら、機械変換のコミットまで戻れば済みます。
意味が変わる変換は、patchの段階で見ておく
レシピの定義には、Thread.resume()、Thread.stop()、Thread.suspend()をthrow new UnsupportedOperationException()に置き換えるものがあります。コンパイルは通りますが、呼び出し側は実行時に例外を受けます。ビルドが緑になっても動作は変わっているので、patchの段階でこうした置換を拾っておくと、Claudeに「この呼び出しの代替を設計して」と依頼しやすくなります。
残った差は、ビルド出力の種類で振り分ける
verifyが落ちたら、出力をClaudeに渡します。ただし渡す前に、エラーの種類を見分けておくと、無駄な往復が減ります。Oracleの移行ガイドが挙げているJDK 17以降の変更から、残りやすいものを並べます。
| 残りやすい差 | 何が起きるか | Claudeに渡す材料 |
|---|---|---|
| JDK内部要素への深いリフレクション | 何が起きるかJDK 17からJDK内部の要素は既定で強くカプセル化され、JDK 9〜16のように単一のオプションで緩められない | Claudeに渡す材料失敗したテストのスタックトレースと、対象のライブラリ名 |
| 既定の文字コード | 何が起きるかJDK 18でJava SE APIの既定がUTF-8に変わった(JEP 400) | Claudeに渡す材料文字化けしたテストの期待値と実際の値 |
| エージェントの動的ロード | 何が起きるかJDK 21で、実行中のJVMへ動的にエージェントをロードすると警告が出る(JEP 451) | Claudeに渡す材料警告を出しているテストライブラリ |
| ファイナライズ | 何が起きるか非推奨で、将来の削除対象になっている | Claudeに渡す材料finalize()を持つクラスの一覧 |
| Security Manager | 何が起きるかJEP 411で、将来の削除対象として非推奨 | Claudeに渡す材料呼び出し箇所のgrep結果 |
| 依存ライブラリの版 | 何が起きるかレシピの説明が挙げるのはプラグインの更新で、アプリの依存ライブラリは別に確かめる | Claudeに渡す材料mvn dependency:treeの該当部分 |
文字コードの差は、コンパイルエラーになりません。テストの失敗や出力の食い違いとして現れるので、verifyのログに文字化けがないかを先に見ます。
OpenRewriteのレシピ一覧には、Mockito用のJavaエージェントをSurefireに追加するレシピや、不正なリフレクションの警告を抑えるmaven-jar-pluginの設定を足すレシピがあります。エージェントの警告やリフレクションの警告が出たら、Claudeに手で書かせる前に、該当する追加レシピをactiveRecipesに足して回す手もあります。ただし、どのレシピが自分のプロジェクトに当たるかは、mvn rewrite:discoverの出力で確かめます。
Claudeへの依頼は、1種類のエラーごとに区切る
Claudeにまとめて「ビルドを通して」と頼むと、pom.xmlの編集と、ソースの修正と、テストの無効化が一度に起きかねません。エラーの種類ごとに依頼を切ります。
mvn -q -B -ntp verify が失敗します。出力の最初のエラー1種類だけを直してください。
- 機械変換のコミット(OpenRewrite適用分)には手を入れないでください
- テストの削除、@Disabled、assertの緩和はしないでください
- 直したら同じコマンドを再実行し、次のエラーの種類を報告してくださいこの依頼の要点は、禁止事項を先に書くことです。移行作業では、テストを無効にすれば緑になります。CLAUDE.mdに同じ禁止を書いておくと、毎回の依頼に書かずに済みます。
## Java 21 移行中のルール
- 検証は mvn -q -B -ntp verify の終了コード 0 で判断する
- OpenRewrite が書き換えた箇所は、ビルドが失敗していない限り触らない
- テストの無効化・削除・期待値の緩和は、人の承認なしにしない認証トークンの扱いと、ネットワーク制限の落とし穴
OpenRewriteの公式頁には、運用に直結する注意が書かれています。rewrite-migrate-javaを含むOpenRewriteの成果物は、Code Genome Projectのリポジトリ(artifacts.codegenomeproject.org)から配布され、認証が必要です。サインインしてダウンロード用のトークンを作り、ビルドにはサインインに使ったメールまたはユーザー名と、そのトークンをパスワードとして渡します。
Mavenでは、~/.m2/settings.xmlの<server>にcodegenomeというIDで資格情報を書き、pomに同じIDのリポジトリとプラグインリポジトリを足します。pomを触りたくなければ、ガイドは-Drewrite.recipeArtifactCoordinates=...を使うコマンドライン形式も案内しています。
トークンがsettings.xmlにあるなら、前の設定で入れたRead(~/.m2/settings.xml)のdenyが効きます。このルールはClaudeのファイルツールを止めるもので、Mavenがファイルを読むのは妨げません。ただし公式は、Bashから呼ばれるスクリプトなどが間接的にファイルを読む場合は、ルールの対象外だと説明しています。パスへのアクセスを全プロセスで止めたいときは、サンドボックスを有効にします。
サンドボックスを使っている場合は、もう1つ確認が必要です。サンドボックス内のネットワークは、許可したドメインの一覧に載ったホストにしか出られず、その一覧は最初は空です。取得先のartifacts.codegenomeproject.orgと、Mavenの依存解決先をnetwork.allowedDomainsに足さないと、rewrite:runが依存を取りに行けません。症状と切り分けはsandboxから繋がらないときの原因にまとめています。
ライセンスの扱いも見ておきます。レシピはModerne Source Available Licenseのもとで提供され、事前にビルドされた成果物をダウンロードできるのはModerneの顧客です。非商用の利用では、成果物をソースから自分でビルドできると公式頁に書かれています。社内で使うときは、利用条件をライセンスの全文で確かめてください。
移行の判断を切り分けるときの目安
この分担は、いつでも最短とは限りません。判断の目安を3点挙げます。
- Java 8や11から上げる場合:
UpgradeToJava21は17への移行も含みます。ただし公式ガイドは、8から17までの変更は別の17向けガイドの領分だとしています。差分が大きくなるので、段階ごとにコミットを分けます - Gradleのプロジェクト: OpenRewriteの公式頁には
gradle rewriteRunの形があります。permissionsの考え方は同じで、許可するコマンドをgradle rewriteRunと検証用のタスクに絞ります - CIでの再発防止:
failOnDryRunResultsをtrueにすると、dryRunが変更を検出したときにビルドを失敗させられます。移行後の取りこぼしを、CIで機械的に止める用途に使えます
破壊的変更を1種類ずつ潰す進め方は、Jest 30への移行と同じ型です。
まとめ
機械変換できる範囲はOpenRewriteに任せ、Claudeはビルド出力から原因を読む残差に使います。permissionsは完全一致の3コマンドに絞り、コミットを分けておけば、Claudeの直しは差分として独立して読めます。
最初に見るのは、rewrite.patchで意味が変わる置換が無いかと、verifyの出力にある文字コードや動的エージェントの警告です。この2点を人が先に押さえておくと、Claudeへの依頼は1種類ずつのエラーに絞れます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。
- code.claude.com/docs/en/permissions
- code.claude.com/docs/en/sandboxing
- docs.openrewrite.org/recipes/java/migrate/upgradetojava21
- docs.openrewrite.org/running-recipes/popular-recipe-guides/migrate-to-java-21
- docs.openrewrite.org/reference/rewrite-maven-plugin
- docs.oracle.com/en/java/javase/21/migrate/significant-changes-jdk-release.…