java-migration について
Java プロジェクトをアップグレードするための段階的なガイドです。現状確認からビルド設定の変更、テストまでを扱い、Java と Spring Boot の互換性変更や確認項目を示します。
主な機能
- 01Java バージョンの移行経路
- 02段階的な移行ワークフロー
- 03バージョン変更と削除 API の説明
- 04Spring Boot 2.x から 3.x への指針
- 05移行チェックリストとコマンド
活用シーン
- 01既存プロジェクトを新しい Java LTS に更新する
- 02更新後のコンパイルや実行時の問題を調べる
- 03Spring Boot と Jakarta パッケージを移行する
- 04移行前後の確認項目を整理する
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ decebals/ claude-code-java --skill java-migrationインストール後、Claude Code で/java-migrationを入力して読み込まれたか確認します。
同梱ファイルREADME.md
詳細
Java 移行スキル
Java プロジェクトをメジャーバージョン間でアップグレードするためのステップごとのガイドです。
使用するタイミング
- ユーザーが「Java 25 にアップグレードして」「Java 8 から移行して」「Java のバージョンを更新して」と言った場合
- レガシープロジェクトのモダナイズ
- Spring Boot 2.x → 3.x → 4.x の移行
- LTS バージョンの採用準備
移行パス
Java 8 (LTS) → Java 11 (LTS) → Java 17 (LTS) → Java 21 (LTS) → Java 25 (LTS)
│ │ │ │ │
└──────────────┴───────────────┴──────────────┴───────────────┘
Always migrate LTS → LTS
クイックリファレンス:変更が必要な点
| From → To | 主な破壊的変更 |
|---|---|
| 8 → 11 | javax.xml.bind の削除、モジュールシステム、内部 API |
| 11 → 17 | シールドクラス(プレビューから正式版へ)、強力なカプセル化 |
| 17 → 21 | パターンマッチングの変更、finalize() の削除に向けた非推奨化 |
| 21 → 25 | Security Manager の削除、Unsafe メソッドの削除、32 ビットのサポート終了 |
移行ワークフロー
ステップ 1:現在の状態を評価する
# Check current Java version
java -version
# Check compiler target in Maven
grep -r "maven.compiler" pom.xml
# Find usage of removed APIs
grep -r "sun\." --include="*.java" src/
grep -r "javax\.xml\.bind" --include="*.java" src/
ステップ 2:ビルド設定を更新する
Maven:
<properties>
<java.version>21</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
</properties>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.12.1</version>
<configuration>
<release>21</release>
</configuration>
</plugin>
Gradle:
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
ステップ 3:コンパイルエラーを修正する
コンパイルを実行し、エラーを繰り返し修正します:
mvn clean compile 2>&1 | head -50
ステップ 4:テストを実行する
mvn test
ステップ 5:実行時の警告を確認する
# Run with illegal-access warnings
java --illegal-access=warn -jar app.jar
Java 8 → 11 の移行
削除された API
| 削除されたもの | 代替手段 |
|---|---|
javax.xml.bind (JAXB) | 依存関係を追加:jakarta.xml.bind-api + jaxb-runtime |
javax.activation | 依存関係を追加:jakarta.activation-api |
javax.annotation | 依存関係を追加:jakarta.annotation-api |
java.corba | 代替手段なし(ほとんど使用されません) |
java.transaction | 依存関係を追加:jakarta.transaction-api |
sun.misc.Base64* | java.util.Base64 を使用 |
sun.misc.Unsafe(一部) | 可能な場合は VarHandle を使用 |
不足している依存関係を追加する(Maven)
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>4.0.4</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>2.1.1</version>
</dependency>
モジュールシステムの問題
JDK 内部へのリフレクションを使用している場合は、JVM フラグを追加します:
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.util=ALL-UNNAMED
Maven Surefire:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>
--add-opens java.base/java.lang=ALL-UNNAMED
</argLine>
</configuration>
</plugin>
導入できる新機能
// var (local variable type inference)
var list = new ArrayList<String>(); // instead of ArrayList<String> list = ...
// String methods
" hello ".isBlank(); // true for whitespace-only
" hello ".strip(); // better trim() (Unicode-aware)
"line1\nline2".lines(); // Stream<String>
"ha".repeat(3); // "hahaha"
// Collection factory methods (Java 9+)
List.of("a", "b", "c"); // immutable list
Set.of(1, 2, 3); // immutable set
Map.of("k1", "v1"); // immutable map
// Optional improvements
optional.ifPresentOrElse(
value -> process(value),
() -> handleEmpty()
);
// HTTP Client (replaces HttpURLConnection)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com"))
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
Java 11 → 17 の移行
破壊的変更
| 変更 | 影響 |
|---|---|
| 強力なカプセル化 | --illegal-access は機能しなくなります。明示的な --add-opens を使用する必要があります |
| シールドクラス(正式版) | プレビュー機能を使用していた場合 |
instanceof のパターンマッチング | プレビュー版から正式版への構文変更 |
導入できる新機能
// Records (immutable data classes)
public record User(String name, String email) {}
// Auto-generates: constructor, getters, equals, hashCode, toString
// Sealed classes
public sealed class Shape permits Circle, Rectangle {}
public final class Circle extends Shape {}
public final class Rectangle extends Shape {}
// Pattern matching for instanceof
if (obj instanceof String s) {
System.out.println(s.length()); // s already cast
}
// Switch expressions
String result = switch (day) {
case MONDAY, FRIDAY -> "Work";
case SATURDAY, SUNDAY -> "Rest";
default -> "Midweek";
};
// Text blocks
String json = """
{
"name": "John",
"age": 30
}
""";
// Helpful NullPointerException messages
// a.b.c.d() → tells exactly which part was null
Java 17 → 21 の移行
破壊的変更
| 変更 | 影響 |
|---|---|
| パターンマッチング switch(正式版) | プレビュー版との小さな構文上の違い |
finalize() の削除に向けた非推奨化 | Cleaner または try-with-resources に置き換えます |
| UTF-8 がデフォルト | プラットフォームのエンコーディングを前提としている場合、ファイル読み込みに影響する可能性があります |
導入できる新機能
// Virtual Threads (Project Loom) - MAJOR
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> handleRequest());
}
// Or simply:
Thread.startVirtualThread(() -> doWork());
// Pattern matching in switch
String formatted = switch (obj) {
case Integer i -> "int: " + i;
case String s -> "string: " + s;
case null -> "null value";
default -> "unknown";
};
// Record patterns
record Point(int x, int y) {}
if (obj instanceof Point(int x, int y)) {
System.out.println(x + ", " + y);
}
// Sequenced Collections
List<String> list = new ArrayList<>();
list.addFirst("first"); // new method
list.addLast("last"); // new method
list.reversed(); // reversed view
// String templates (preview in 21)
// May need --enable-preview
// Scoped Values (preview) - replace ThreadLocal
ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
ScopedValue.where(CURRENT_USER, user).run(() -> {
// CURRENT_USER.get() available here
});
Java 21 → 25 の移行
破壊的変更
| 変更 | 影響 |
|---|---|
| Security Manager の削除 | Security Manager に依存するアプリケーションには、別のセキュリティ対策が必要です |
sun.misc.Unsafe メソッドの削除 | 代わりに VarHandle または FFM API を使用します |
| 32 ビットプラットフォームのサポート終了 | x86-32 はサポートされなくなります |
| レコードパターン変数が final | switch 内でパターン変数を再代入できません |
ScopedValue.orElse(null) が禁止 | null 以外のデフォルト値を指定する必要があります |
| 動的エージェントの制限 | -XX:+EnableDynamicAgentLoading フラグが必要です |
Unsafe の使用状況を確認する
# Find sun.misc.Unsafe usage
grep -rn "sun\.misc\.Unsafe" --include="*.java" src/
# Find Security Manager usage
grep -rn "SecurityManager\|System\.getSecurityManager" --include="*.java" src/
導入できる新機能
// Scoped Values (FINAL in Java 25) - replaces ThreadLocal
private static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
public void handleRequest(User user) {
ScopedValue.where(CURRENT_USER, user).run(() -> {
processRequest(); // CURRENT_USER.get() available here and in child threads
});
}
// Structured Concurrency (Preview, redesigned API in 25)
try (StructuredTaskScope.ShutdownOnFailure scope = StructuredTaskScope.open()) {
Subtask<User> userTask = scope.fork(() -> fetchUser(id));
Subtask<Orders> ordersTask = scope.fork(() -> fetchOrders(id));
scope.join();
scope.throwIfFailed();
return new Profile(userTask.get(), ordersTask.get());
}
// Stable Values (Preview) - lazy initialization made easy
private static final StableValue<ExpensiveService> SERVICE =
StableValue.of(() -> new ExpensiveService());
public void useService() {
SERVICE.get().doWork(); // Initialized on first access, cached thereafter
}
// Compact Object Headers - automatic, no code changes
// Objects now use 64-bit headers instead of 128-bit (less memory)
// Primitive Patterns in instanceof (Preview)
if (obj instanceof int i) {
System.out.println("int value: " + i);
}
// Module Import Declarations (Preview)
import module java.sql; // Import all public types from module
パフォーマンスの改善(自動)
Java 25 には、いくつかの自動的なパフォーマンス改善が含まれています:
- コンパクトオブジェクトヘッダー:オブジェクトあたり 16 バイトから 8 バイトに削減
- String.hashCode() の定数畳み込み:String キーを使用する Map の検索が高速化
- AOT クラス読み込み:事前キャッシュにより起動が高速化
- 世代別 Shenandoah GC:スループットが向上し、一時停止時間が短縮
OpenRewrite による移行
# Automated Java 25 migration
mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
-Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-migrate-java:LATEST \
-Drewrite.activeRecipes=org.openrewrite.java.migrate.UpgradeToJava25
Spring Boot の移行
Spring Boot 2.x → 3.x
要件:
- Java 17+(必須)
- Jakarta EE 9+(javax.* → jakarta.*)
パッケージ名の変更:
// Before (Spring Boot 2.x)
import javax.persistence.*;
import javax.validation.*;
import javax.servlet.*;
// After (Spring Boot 3.x)
import jakarta.persistence.*;
import jakarta.validation.*;
import jakarta.servlet.*;
検索と置換:
# Find all javax imports that need migration
grep -r "import javax\." --include="*.java" src/ | grep -v "javax.crypto" | grep -v "javax.net"
自動移行:
# Use OpenRewrite
mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
-Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-spring:LATEST \
-Drewrite.activeRecipes=org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_0
依存関係の更新(Spring Boot 3.x)
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.2</version>
</parent>
Hibernate 5 → 6 の変更点
// ID generation strategy changed
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // preferred
private Long id;
// Query changes
// Before: createQuery returns raw type
// After: createQuery requires type parameter
// Before
Query query = session.createQuery("from User");
// After
TypedQuery<User> query = session.createQuery("from User", User.class);
よくある移行時の問題
問題:リフレクションへのアクセスが拒否される
症状:
java.lang.reflect.InaccessibleObjectException: Unable to make field accessible
修正方法:
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.lang.reflect=ALL-UNNAMED
問題:JAXB の ClassNotFoundException
症状:
java.lang.ClassNotFoundException: javax.xml.bind.JAXBContext
修正方法: JAXB の依存関係を追加します(Java 8→11 のセクションを参照)
問題:Lombok が動作しない
修正方法: Lombok を最新バージョンに更新します:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</dependency>
問題:Mockito でテストが失敗する
修正方法: Mockito を更新します:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.8.0</version>
<scope>test</scope>
</dependency>
移行チェックリスト
移行前
- 現在の Java バージョンを記録する
- すべての依存関係とそのバージョンを一覧にする
- 内部 API(
sun.*、com.sun.*)の使用箇所を特定する - フレームワークの互換性を確認する(Spring、Hibernate など)
- バックアップを作成する/ブランチを作成する
移行中
- ビルドツールの設定を更新する
- 不足している Jakarta の依存関係を追加する
-
javax.*→jakarta.*のインポートを修正する(Spring Boot 3 の場合) - 必要に応じて
--add-opensフラグを追加する - Lombok、Mockito、その他のツールを更新する
- コンパイルエラーを修正する
- テストを実行する
移行後
- 不要な
--add-opensフラグを削除する - 新しい言語機能(レコード、var など)を採用する
- CI/CD パイプラインを更新する
- 実施した変更を記録する
クイックコマンド
# Check Java version
java -version
# Find internal API usage
grep -rn "sun\.\|com\.sun\." --include="*.java" src/
# Find javax imports (for Jakarta migration)
grep -rn "import javax\." --include="*.java" src/
# Compile and show first errors
mvn clean compile 2>&1 | head -100
# Run with verbose module warnings
java --illegal-access=debug -jar app.jar
# OpenRewrite Spring Boot 3 migration
mvn org.openrewrite.maven:rewrite-maven-plugin:run \
-Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-spring:LATEST \
-Drewrite.activeRecipes=org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_0
バージョン互換性マトリックス
| フレームワーク | Java 8 | Java 11 | Java 17 | Java 21 | Java 25 |
|---|---|---|---|---|---|
| Spring Boot 2.7.x | ✅ | ✅ | ✅ | ⚠️ | ❌ |
| Spring Boot 3.2.x | ❌ | ❌ | ✅ | ✅ | ✅ |
| Spring Boot 3.4+ | ❌ | ❌ | ✅ | ✅ | ✅ |
| Hibernate 5.6 | ✅ | ✅ | ✅ | ⚠️ | ❌ |
| Hibernate 6.4+ | ❌ | ❌ | ✅ | ✅ | ✅ |
| JUnit 5.10+ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Mockito 5+ | ❌ | ✅ | ✅ | ✅ | ✅ |
| Lombok 1.18.34+ | ✅ | ✅ | ✅ | ✅ | ✅ |
LTS サポートのタイムライン:
- Java 21:Oracle の無料サポートは 2028 年 9 月まで
- Java 25:Oracle の無料サポートは 2033 年 9 月まで
コメント
Markdown 対応