Claude Media
Claude CodeとOpenRewriteでJava 21へ上げる — mvnの残りエラーを直す

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. 1

    作業ブランチを切り、dryRunを回す

    作業ツリーが綺麗な状態でmvn rewrite:dryRunを実行します。ソースは変わらず、変更予定の一覧がビルドログに出ます。

  2. 2

    rewrite.patchを読む

    dryRunは、適用した場合の差分をtarget/site/rewrite/rewrite.patchに書き出します。Claudeに読ませて、意味が変わる変更がないか先に洗います。

  3. 3

    runを回して、そのままコミットする

    mvn rewrite:runでソースを書き換え、git diffで確かめて、機械変換だけのコミットにします。

  4. 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種類ずつのエラーに絞れます。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/permissions
  2. code.claude.com/docs/en/sandboxing
  3. docs.openrewrite.org/recipes/java/migrate/upgradetojava21
  4. docs.openrewrite.org/running-recipes/popular-recipe-guides/migrate-to-java-21
  5. docs.openrewrite.org/reference/rewrite-maven-plugin
  6. docs.oracle.com/en/java/javase/21/migrate/significant-changes-jdk-release.…
この記事を共有:XはてブLinkedIn