はじめに
前回の記事でSpring BootからQuarkusへの移行を、IBM Bobとマイグレーションスキルで達成しました。
ただいくつか反省点もありました。そこでこの記事では、前回の知見を再利用しつつ「ユーザーの介入とBobコイン消費を抑えてマイグレーションを達成する」という狙いで再度挑戦してみました。
前回の記事を理解されていることを前提に記載しています。
また、今回の記事は「いかにAIを賢く使うか」に焦点をあてています。あまりマイグレーションそのものには言及していません![]()
TL;DR
AIへの指示を工夫することで、コスト・処理時間・移行精度の各観点で前回より良好な結果を得ることができました。
この記事で実装すること
- Windows環境で、WSL2によるBob Shellサンドボックス環境の構築
- マイグレーションスキルのバッチ実行
- バッチ実行とマイグレーション知見の追記によるプロセス改善
前回記事からのアップデート方針
次の2点が改善方針です
- 実行中は手を介さずに、安全に実行できる
- 移行判断を蓄積して育てられる
実現したいこと
前回よりも高い精度で、かつユーザーが途中で承認作業や移行判断を行わなくても移行タスクを完遂できることを狙います。
実運用を想定すると、企業内の複数のアプリケーションが移行対象になるでしょう。アーキテクチャ標準が浸透している場合はアプリ構造も近いものとなり、マイグレーションのポイントも共通性が高いと思われます。
そのため、先行するアプリのマイグレーションで得られた知見を後続の移行作業に活かせることも狙います。
留意事項
広範囲に自動承認を許可すると、AIの選択がコンピューターに悪影響を与える可能性があります。そのため、自動承認を許容しつつコンピューターへの影響を最小限にする必要があります。
上記を踏まえた今回の構成
AIのスキルを活用してマイグレーションを行う点は前回と同じですが、次の点を追加・変更しています。
-
移行の知見をファイル化する
移行の知見を記載するテキストファイルを準備し、マイグレーション実行時にそのテキストを参照して知見を反映させる仕組みとします。このテキストは、移行対応後に修正・追記を行うことで次回以降のマイグレーションを円滑に精度よく実行できることを企図しています。 -
隔離したBobの実行環境としてWSL2を選択する
対象リソースおよびBobをWSL2上に配置します。処理をWSL2内部で行わせることで、コンピューターへのリスクを低減します。
WSL2はデフォルトではWindowsホスト側へのアクセスを許容しているため、これらを設定により停止します。
より完全な隔離が必要な場合は、専用VMの利用なども検討してみてください
-
BobShellの非インタラクティブモードを使用する
WSL2上でのBobの実行に適したBob Shellを採用します。ユーザーの介入を行わない前提として非インタラクティブモードで実行します。
WSL2環境の追加
WSL2環境はマイグレーション専用に作成します
マイグレーション専用の環境を追加します。
WSL2ではUbuntu利用が第一選択に挙がることが多いですが、別のディストリビューションを選択することもできます。詳細は次の記事にまとめています。
この記事ではCentOS Stream 10を選択します。ダウンロードサイトからイメージを取得した後、次のコマンドで環境の追加を行います(フォルダ名などはご自身の環境に合わせて変更してください)。
wsl --import BobSandbox C:\apps\wslDistro\BobSandbox C:\temp\CentOS-Stream-Image-WSL-Base.x86_64-10-202604020202.wsl
WSL2環境のスナップショット作成/削除
もし反復利用などでWSL2環境のスナップショットが必要な場合、適切なタイミングで取得してください。エクスポートを行う際はターミナル接続が切れる点にはご注意ください。
# エクスポート
wsl --export BobSandbox C:\temp\BobSandbox1.tar
# 環境削除と再インポート
wsl --unregister BobSandbox
wsl --import BobSandbox C:\apps\wslDistro\BobSandbox C:\temp\BobSandbox1.tar
WSL2環境上のBob実行環境構築
WSL2環境にBobとその他必要なライブラリをインストールします
Bobを動かすために、次の設定を行います。
BobのAPIキーを取得する → 各種インストール → ユーザー追加
BobのAPIキー取得
IDE版のBobではブラウザ経由で対話的にサインインすることが多いと思いますが、APIキーでの運用も可能です。
この手順はBobのWebサイトから実施します。
スコープはInferenceを指定します。
最終的に、APIキー文字列が取得できます。APIキーはこのタイミングでしか出力されないため、忘れずに控えておくようにします。
インストール
Bob、Gitのインストールを行います。Bobの前提としてnodejsが必要になるため、こちらもインストールします。
この記事を作成したタイミングでのBobのバージョンは 2.0.0 です。
# Bobインストール
dnf install -y nodejs
curl -fsSL https://bob.ibm.com/download/bobshell.sh | bash
# Bobインストール確認
bob --version
# Gitインストール
dnf install -y git-core
OSの追加設定
ユーザー追加
マイグレーション処理で使用するユーザーを追加します。
useradd -m -s /bin/bash usr1
passwd usr1
※ この後パスワードを入力
# 権限追加
usermod -aG wheel usr1
WSL2の設定変更
wsl.confを修正します。
vi /etc/wsl.conf
[boot]
systemd=true
[interop]
enabled=false # Windows バイナリの実行を禁止
appendWindowsPath=false
[automount]
enabled=false # /mnt/c を作らない
[user]
default=usr1 # 追加ユーザーを使用する
再起動
上記までの設定が終了したら、別ウィンドウのPowerShellから、次のコマンドでWSL2環境を停止します。
wsl --terminate BobSandbox
再度ターミナルでアクセスを試みると、停止状態のWSL2は自動的に起動します。このタイミングでwsl.confの内容が反映され、usr1でログインしている状態となります。
JDKインストール
ユーザーが切り替わった後、JDKのインストールを行います。
# SDKMAN(JDKのパッケージマネージャ)インストール
curl -s "https://get.sdkman.io" | bash
# JDK(GraalVM CE 21)インストール
source "$HOME/.sdkman/bin/sdkman-init.sh"
sdk install java 21.0.2-graalce
# JDKインストール確認
java --version
この記事ではJDK21で取得できる最新バージョンの21.0.2を指定しています。SDKMANからのインストール最新情報は、こちらを参照ください。
プロジェクト設定
移行プロジェクトとマイグレーションスキル、プロンプトを準備します
WSL2環境上に、移行対象のプロジェクト配置、およびマイグレーションスキルを実行するための各種準備を行います。
最終的な配置はこのようになります。
~/projects/realworld/
├─ .bob/
│ └─ skills/
│ └─ migrate-spring-to-quarkus/ ← マイグレーションスキル
│
├─ harness/
│ ├─ prompt.txt
│ ├─ questions.md
│ ├─ findings.md
│ ├─ .gitignore
│ └─ logs/
│ └─ .gitkeep
│
└─ (プロジェクト本体)
環境変数へのAPIキーの追加
BobがAPIキーを認識できるよう、環境変数として追加します。APIキーは実際の値に置き換えます。
echo 'export BOBSHELL_API_KEY="(APIキー)"' >> ~/.bashrc
APIキーは ~/.bashrc に平文で保存されるため、共有端末やバックアップ対象の環境では注意が必要です。
ディレクトリ作成とプロジェクト配置
projectsディレクトリを作成し、その下にrealworldという名前でクローンを取得します。
mkdir -p ~/projects && cd ~/projects
git clone https://github.com/gothinkster/spring-boot-realworld-example-app.git realworld
cd realworld/
マイグレーションスキル追加
続いて、前回記事で使用したスキルを登録します。
npx skills add quarkusio/skills --skill migrate-spring-to-quarkus
前回記事からの留意点は次の2つです。
| 留意点 | 説明 |
|---|---|
| 選択するエージェントはIBM Bobのみとします、他は外します | スキルのインストールディレクトリがBob標準の.bobになります |
| find-skillsの導入は行いません | その他スキルを使用しないため、導入しません |
マイグレーション実行用のプロンプト追加
実行に使用する各テキストファイルを配置します。
prompt.txt
スキル実行のためのプロンプトを登録します。Bobに対する直接的な指示になります。
mkdir -p harness
vi harness/prompt.txt
prompt.txt(長いため折り畳み)
# Spring Boot → Quarkus 移行タスク
あなたはこのプロジェクトをQuarkusに移行するエージェントです。以下の指示に従って作業してください。
---
## 0. ファイル構成について
このharnessは3つのファイルで構成されています。役割を混同しないでください。
- `harness/prompt.txt`(本ファイル): 実行時にBobへ渡す指示そのもの。「何をすべきか」の要約ルール。
- `harness/questions.md`: 実行中にBobが疑問点・軽微な判断を記録するログ。Bobが書き込む。
- `harness/findings.md`: 過去の実行から得られた知見の蓄積。「なぜそうすべきか」の経緯・背景を含む。
**Bobはこのファイルを編集しません。** 人間が実行後にレビューして書きます。
再利用価値が固まった項目は、本ファイルの「3. 実装方針」「4. 既知の落とし穴」に転記されます。
---
## 1. 実行環境について
このセッションは非対話モード(bob run)で実行されています。
ツール呼び出しはすべて事前承認済みです。承認を求める必要はありません。
一方で、あなたの質問に即座に答える人間はこの場にいません。
---
## 2. 疑問が生じた場合の手順(重要・必須)
疑問が生じたら、まず以下の基準で「停止すべき疑問」か「軽微な疑問」かを
判定してください。
### 2.1 停止すべき疑問(作業を中断する)
次のいずれかに該当する場合は、これ以上ツールを呼ばずに作業を中断してください。
- 本ファイルの「3. 実装方針」「4. 既知の落とし穴」「6. 実行パラメータ」の
いずれにも判断基準が見当たらない
- かつ、誤ると後戻りが困難な変更を伴う
(アーキテクチャ選択、複数ファイルに影響する変更、データの扱い、
削除を伴う変更 など)
対応:
1. `harness/questions.md` の末尾に、以下の形式で追記する(既存内容は残す)。
Q<番号> [PENDING] <ISO8601タイムスタンプ>
- 対象: <ファイル/クラス名>
- 疑問点: <具体的な内容>
- 影響範囲: <関連するファイルや機能>
- Bobの暫定案(あれば): <内容>
2. 最終応答の本文に、必ず文字列 `AWAITING_USER_INPUT` を含めて終了する
(検知用の固定マーカー。省略しないこと)。
### 2.2 軽微な疑問(作業を止めずに進める)
上記に該当しない疑問(命名規則、テストの軽微な調整、下記「4. 既知の落とし穴」
に近いが完全一致しない程度の差異 など)については、自己判断で継続してください。
判断内容は `harness/questions.md` に以下の形式で記録してください(作業は止めない)。
N<番号> [NOTE] <ISO8601タイムスタンプ>
- 対象: <ファイル/クラス名>
- 判断内容: <何をしたか>
- 根拠: <本ファイルの規定 / Quarkusの標準的な作法 / その他 のいずれか>
### 2.3 resume時の手順
このプロンプトが再度渡された場合(`--resume latest` での再開)は、
作業開始前に必ず `harness/questions.md` を確認してください。
[PENDING] のうち「- 回答:」が追記されている項目があれば、
その項目を [ANSWERED] に書き換えてから、回答の内容に従って
作業を再開してください。回答が追記されていない [PENDING] 項目が
残っている場合は、それらについては引き続き停止した状態を保ってください。
> 注記: このresumeフローは、直近の実行では [PENDING] が0件のまま完了したため、
> 実際に [PENDING] が発生した状態からの再開動作は未検証です。次回 [PENDING] が
> 発生した際は、resume後の挙動を重点的に確認してください。
---
## 3. 実装方針(固定ルール)
1. 移行戦略は「6. 実行パラメータ」で `full`(QuarkusネイティブAPI)に固定指定済みのため、
ヒアリングは発生しない。Spring互換APIは記述は似せられるが完全な互換動作を保証しないため
採用しない(原因特定が困難なバグを生みやすい)。
2. コード変換フェーズ(`@Service` / `@Repository` 等の変換)の時点で、
テストでモック対象となり得るBeanは無条件で `@ApplicationScoped` にする。
Spring互換モードを使う場合でも `@ApplicationScoped` は問題なく使用できるため、後回しにしない。
3. テスト移行フェーズに着手する前に、必ず `harness/findings.md` の
L-02・L-03(テストアーキテクチャの制約 / テスト分離)を読み込み、`@QuarkusTest` /
`@InjectMock` / `@TestProfile` の制約を確認したうえで実装方針を決める。
4. MyBatisマッパーのXML配置を試行錯誤しない。`harness/findings.md` の
L-04に記載の正解パターン(`@Mapper` インターフェースと同一パッケージへの配置)
を最初から採用する。
5. 設定ファイル(`application.properties` 等)を変更した後にビルド・テストの挙動が
変わらないと感じた場合、真の原因調査に入る前に `harness/findings.md` の
L-05に記載のキャッシュ確認手順を先に実行する。
6. 各フェーズで上記いずれかのルールに抵触する事象、または下記「4. 既知の落とし穴」に
該当しない新規の判断が発生した場合、`harness/questions.md` に [NOTE] 形式で記録する
(2.2節の手順に従う)。`harness/findings.md` への転記・更新は人間が実行後にレビューして
行うため、Bobはこのファイルを編集しない。
7. REST層は Quarkus REST(RESTEasy Reactive)のネイティブAPIを使用する。
Spring MVC由来のアノテーション(@RestController、@RequestMapping、
@GetMapping 等)をそのまま残さず、jakarta.ws.rs のネイティブアノテーション
(@Path、@GET 等)に置き換える。Spring Compatibility APIによる
互換シムは使用しない(移行戦略: full のため)。
---
## 4. 既知の落とし穴
### 1. テストアーキテクチャの制約
#### @QuarkusTest
- 抽象クラスから継承する形では機能しない。テストクラスに直接アノテーションを付与すること。
#### @InjectMock
- CDIの normal scope(`@ApplicationScoped` 等)のBeanにのみ使用可能。
- `@Singleton`(Spring `@Service` の変換先としてよく使われがちだが)には使用できない。
→ コード変換フェーズで対象Beanを `@ApplicationScoped` にしておくことが前提条件になる。
#### テストコンテキストの分離
- `@InjectMock` を持つテストと、実DBを使うテストは別のQuarkusコンテキストを起動するため、
同時に実行すると競合しうる。
- 分離が必要な場合は `@TestProfile` を使って明示的にプロファイルを切り分ける。
#### テスト間のデータ干渉
- Spring Bootの `@Transactional` + `@Rollback` によるテスト後の自動ロールバックは、
Quarkusには存在しない。
- 同じメールアドレス等の一意制約に依存するテストが並ぶ場合、UNIQUE制約違反で
互いに干渉することがある。
- 対策:`DbTestBase.cleanDb()` のような明示的クリーンアップ処理を、
テスト移行フェーズの**最初**に設計・実装する(後付けにしない)。
#### 事前確認
着手前に [Quarkus Testing Guide](https://quarkus.io/guides/getting-started-testing) の
以下の項目を確認する:
- `@QuarkusTest` のコンテキスト共有ルール
- `@InjectMock` のCDIスコープ要件(normal scopeのみ)
- `@TestProfile` による分離の必要条件
---
### 2. MyBatis (Quarkiverse MyBatis 2.x) XML解決
#### 試行不要(前回すべて失敗した方法)
以下は前回試して機能しなかったため、再試行する必要はない。
- `mapper-locations=mapper/*.xml` — globパターンに対応していない
- `classpath:mapper/*.xml` — 動作しない
- `xmlconfig` モード — 別コンテキスト問題が発生する
#### 正解パターン
- XMLマッパーファイルは、対応する `@Mapper` インターフェースと**同一パッケージ**に配置する。
これにより自動検出される(quarkus-mybatis の `MyBatisProcessor` の仕様)。
- 詳細は quarkus-mybatis の公式ドキュメントおよびソースコードを参照:
https://github.com/quarkiverse/quarkus-mybatis
#### 見落としやすいポイント
- 複数マッパーで共有される ResultMap(例:`TransferData.xml` のような共有定義ファイル)の
存在を、初期のコード解析フェーズで洗い出しておくこと。
この存在を見落とすと、XML移設後に解決エラーが後工程で発覚する。
- namespaceが有効なJavaクラス名でない共有XMLは、MyBatisProcessorが読み込めない。
空の `@Mapper` インターフェースを新設し、XML側のnamespaceをそのFQNに変更したうえで
関連するresultMap参照も更新する。
---
### 3. 移行戦略の固定方針とビルドキャッシュ対応
#### 移行戦略はQuarkusネイティブAPIを既定とする
- Spring互換APIは記述の互換性のみを保証し、Authorizationヘッダー処理やJSONパースなど
細かな挙動差異がある。この差異は原因特定が難しく、デバッグコストが大きい。
- 特別な理由(短期間での暫定移行など)がない限り、初回のヒアリングでQuarkusネイティブAPIを選択する。
#### ビルドキャッシュによる誤診を避ける
- `application.properties` などの設定ファイルを変更した直後、Gradleが `UP-TO-DATE` 判定で
古いビルド結果を使い続け、「変更が反映されていない」と誤診することがある。
- 設定変更を行った後の検証では、以下のいずれかを必ず実施する:
- `./gradlew clean` を実行してから `test` を実行する
- `--rerun-tasks` フラグを付けて実行する
- 「修正したのに現象が変わらない」と判断する前に、上記のクリーンビルドを先に試すこと。
---
### 4. 設定ファイルの無言差し替え禁止
- Spring固有の設定キーで、Quarkusに1:1で対応する設定が存在しない場合は、
**黙って別の意味の設定に置き換えない。**
(例:`spring.jackson.deserialization.UNWRAP_ROOT_VALUE=true` を、意味の異なる
`quarkus.jackson.fail-on-unknown-properties` 等に無言で差し替えることは禁止。)
- 該当する挙動を再現するコード(例: `ObjectMapperCustomizer` の実装)を書くか、
対応するAPIが見つからない場合は `harness/questions.md` に記録して停止する。
---
### 5. 機械的な一括置換の取りこぼし
- `sed 's/^@Service$/@ApplicationScoped/'` のような行全体一致の置換は、
`@Service("customName")` のような引数付きアノテーションや、同一行への複数アノテーション
併記、インターフェースへの付与などを取りこぼす。
- アノテーション変換に一括置換コマンドを使う場合は、実行後に必ず
`grep -rn "@Service\|@Repository\|@Component" src/` 等で取りこぼしがないか確認する。
- `@RestController` / `@RequestMapping` からJAX-RSへの変換は**一括置換では行わない**
(クラス階層のパス結合、メソッド単位のHTTP動詞、`ResponseEntity`→`Response.ok(...).build()`、
`@AuthenticationPrincipal`→`SecurityIdentity`/JWTの書き換えが必要なため、個別に読んで書き換える)。
---
### 6. テストを@Disabledで回避することの禁止
- 移行が難しいテストに対して `@Disabled`(または `@Ignore`)を付与してスキップし、
失敗を回避することを**禁止する**。これは失敗の隠蔽であり、移行漏れが後工程まで
発覚しないまま残ることになる。
- 移行できないテストに遭遇した場合は、「2. 疑問が生じた場合の手順」に従い、
`harness/questions.md` に記録して停止する。安易な回避より、正確な報告を優先する。
---
### 7. JDK/ベースイメージの一致確認
- 作業開始前に、実際のJDKバージョン・ディストリビューション(`java -version`)が
本ファイルおよび環境構築手順で前提としているものと一致するか確認し、最終応答に
明記する。
- 食い違いがあれば、それ自体を `harness/questions.md` に記録して停止する
(後工程のトラブル切り分けを困難にするため、この確認を後回しにしない)。
---
## 5. Git運用
- コミット前に `git status --porcelain` で作業ツリーの状態を確認すること。
- pull request は作成しない。
- コミットメッセージは移行内容を要約したものとし、1コミットにまとめる
(下記「6. 実行パラメータ」のブランチ運用指示に従う)。
---
## 6. 実行パラメータ
- 移行戦略: full(Spring依存を全て排除し、Quarkusネイティブなアーキテクチャに置き換える。
Spring Compatibility APIによる互換シムは使用しない)
→ 「3. 実装方針」の判断で停止して質問しないこと。この戦略で進めること。
- Git ブランチ運用: 現在チェックアウトされているブランチ(main)を直接使用する。
新しいブランチは作成しない。作業完了後、mainに対して単一コミットを作成する。
→ 停止して質問しないこと。この運用は承認済み。pull request は作成しない。
questions.md
これはBobが処理中に不明点などがあった場合にユーザーとのやりとりを記録するファイルです。
Bobが処理を終了した際に[PENDING]の項目があったら、必要な指示をユーザーが追記します。
vi harness/questions.md
# Bob Shell 疑問点ログ
この実行1回分の生ログです。[PENDING] は人間の回答待ち、
[ANSWERED] は回答済みで再開後に反映済み、[NOTE] は自己判断で
継続した軽微な記録です。
人間が回答する場合は、対象の [PENDING] 項目の直後に
`- 回答: <指示内容>` を追記してください。
<!-- 以下、実行ごとに追記されます -->
findings.md
これは知見を蓄積するユーザーが追記していくファイルです。Bobは参照のみ行います。
次回以降のマイグレーションを行う際に従うべき移行判断を追記していきます。初期状態では、前回の記事で得られた知見を記載しています。
vi harness/findings.md
findings.md(長いため折り畳み)
# findings.md(migrate-spring-to-quarkus / spring-boot-realworld-example-app)
このファイルは**人間が実行後にレビューして書く、知見の蓄積**です。Bobは編集しません
(Bobが実行中に生成する疑問点は `harness/questions.md` に別途記録されます)。
再利用価値が固まった項目は `harness/prompt.txt` の「3. 実装方針」「4. 既知の落とし穴」に
転記し、このファイルには「反映済み」として残します。
## 運用ルール
- 実行のたびに、新しく判明したことを末尾のテンプレートに従って追記する
- **誤りだった項目は削除せず、`状態: superseded` に変更して理由を残す**
(削除すると「なぜその選択をやめたか」が失われ、次回同じ道を試してしまう)
- `強制度: MUST` の項目は、エージェントの既定の判断より優先される
- `適用範囲: 汎用` の項目は、同じアーキテクチャの別アプリを移行する際にそのまま
持ち越せる。新しいアプリ用のファイルを作るときは、これらをコピーして起点にする
- `prompt.txt反映` が `済み` の項目は、本文(3節または4節)に要約が転記済み。
経緯・背景の詳細を確認したい場合のみこのファイルを参照すればよい
## メタデータの意味
| 項目 | 値 |
|---|---|
| 適用フェーズ | `build` / `code` / `frontend` / `testing` / `cleanup` / `all` |
| 適用範囲 | `汎用`(他アプリに持ち越せる) / `このアプリ固有` |
| 強制度 | `MUST`(既定判断を上書き) / `SHOULD`(推奨) / `INFO`(参考) |
| 状態 | `confirmed`(実測確認済み) / `unverified`(未検証の推測) / `superseded`(無効化。理由必須) |
| prompt.txt反映 | `済み` / `未` |
| 初回記録 | 判明した実行の識別子 |
---
## L-01 CDIスコープはcodeモジュールで確定させる
- 適用フェーズ: `code`
- 適用範囲: `汎用`
- 強制度: **MUST**
- 状態: `confirmed`
- prompt.txt反映: `済み`(3節2番)
- 初回記録: run-01
### 指示
Springの`@Service` / `@Repository` / `@Component`は、すべて`@ApplicationScoped`に変換する。
Spring互換アノテーションのまま残さない。`@Singleton`にマップしない。
### 背景
`@InjectMock`は`@Singleton`スコープのBeanに使えない(normal scopeが必要)。
`@Singleton`のまま進めると`invalid bean scope: Singleton`が発生し、testingフェーズで
大きく手戻りする。`@ApplicationScoped`は移行戦略(full / Spring互換)に関係なく使用可能。
---
## L-02 Quarkusのテストアーキテクチャを先に確認する
- 適用フェーズ: `testing`
- 適用範囲: `汎用`
- 強制度: **MUST**
- 状態: `confirmed`
- prompt.txt反映: `済み`(3節3番、4節1)
- 初回記録: run-01
### 指示
testingモジュール開始前に https://quarkus.io/guides/getting-started-testing を読み、
以下を確認してからテストを書き始める。
- `@QuarkusTest`のコンテキスト共有ルール
- `@InjectMock`のCDIスコープ要件(normal scopeのみ)
- `@TestProfile`による分離が必要になる条件
確定している事実:
- `@QuarkusTest`はabstractクラスから継承しても機能しない。**具象テストクラスに直接付与する**
- `@InjectMock`を使うテストとDBを触るテストは別Quarkusコンテキストを起動し、競合する。
テストを書き始める前に分割方針を決める(クラスを分けて`@TestProfile`で明示分離する、
あるいは実DBを使う箇所では`@InjectMock`を使わない)
---
## L-03 テスト分離は設計段階で組み込む
- 適用フェーズ: `testing`
- 適用範囲: `汎用`
- 強制度: **MUST**
- 状態: `confirmed`
- prompt.txt反映: `済み`(3節3番、4節1)
- 初回記録: run-01
### 指示
テストを書き始める前に、明示的なDBクリーンアップ基底クラス
(例: `DbTestBase`の`cleanDb()`を`@BeforeEach`から呼ぶ)を作り、
DBに触る全テストで使用する。
### 背景
Spring Bootの`@Transactional` + `@Rollback`によるテスト後自動ロールバックは
**Quarkusには存在しない**。同じメールアドレスなど同一の一意値を使うテスト同士が
UNIQUE制約違反で干渉する。実行するまで気づけない類の失敗。
---
## L-04 MyBatisのXMLマッパー解決方法(答えは確定済み)
- 適用フェーズ: `code`
- 適用範囲: `汎用`
- 強制度: **MUST**
- 状態: `confirmed`
- prompt.txt反映: `済み`(3節4番、4節2)
- 初回記録: run-01
### 指示
Quarkiverseの`quarkus-mybatis`エクステンションを使用する。
参照: https://github.com/quarkiverse/quarkus-mybatis
**各マッパーXMLを、対応する`@Mapper`インターフェースと同一パッケージに配置する。**
エクステンションがその場所を自動検出する。これが動作する構成。
以下は試行済みで動作しない。**再試行しない。**
| 試したこと | 結果 |
|---|---|
| `mapper-locations=mapper/*.xml` | glob未対応 |
| `classpath:mapper/*.xml` | 動作しない |
| `xmlconfig`モード | 別コンテキストが立ち上がる問題 |
(以下はこのアプリ固有)`TransferData.xml`に共有ResultMapがある。複数のマッパーから
参照されているため、XMLの再配置時に参照解決が壊れないよう扱う。namespaceが有効な
Javaクラス名でないため、空の`@Mapper`インターフェースを新設し、XML側のnamespaceを
そのFQNに変更したうえで関連するresultMap参照も更新する対応で解決済み(run-02)。
- 適用範囲: `このアプリ固有`(TransferData.xmlの部分のみ)
---
## L-05 Gradleのビルドキャッシュによる誤診を避ける
- 適用フェーズ: `all`
- 適用範囲: `汎用`
- 強制度: **MUST**
- 状態: `confirmed`
- prompt.txt反映: `済み`(3節5番、4節3)
- 初回記録: run-01
### 指示
`application.properties`やリソースを変更した後は、必ず`./gradlew clean`を実行し、
または`--rerun-tasks`を付与してから再実行する。この手順を踏まずに
「変更が効いていない」と結論づけない。
### 背景
Gradleは変更を検知できず`UP-TO-DATE`と判定して古いビルドを使い回すことがあり、
「変更したのに反映されない」という誤診断につながる。
---
## L-06 EBQ × Gradleでのnetty consistent resolution衝突
- 適用フェーズ: `build`
- 適用範囲: `汎用`(EBQをGradleで使う場合に発生。Mavenでは基本的に起きない)
- 強制度: **MUST**
- 状態: `confirmed`(現象・原因) / `unverified`(推奨される対処)
- prompt.txt反映: `未`(対処が未確定のため転記見送り)
- 初回記録: run-02
### 現象
Could not resolve io.netty:netty-transport-native-unix-common:{strictly 4.1.135.Final}
### 原因(確認済み)
quarkus-bom(enforcedPlatform)が`4.1.135.Final`と`4.1.135.Final-redhat-00001`の
**両方の制約を持ち込み**、さらにQuarkus Gradleプラグインのconsistent resolutionが
deployment側クラスパスに`strictly 4.1.135.Final`を強制するため、両立できずに失敗する。
### 試したこと
| 対処 | 結果 |
|---|---|
| `dependencyResolutionManagement`にIBMリポジトリ追加 | 変化なし。**再試行不要** |
| `configurations.all { resolutionStrategy { eachDependency { ... } } }`でio.nettyの全依存に機械的に`-redhat-00001`を付与 | 解決は通ったが、下記の理由で推奨しない |
### 注意(推奨対処が未確定)
上記の全依存一括置換は次の理由で脆い。**そのまま踏襲しないこと。**
- ビルド番号`00001`のハードコード(artifactごとに番号が違えば破綻する)
- 全configuration・全netty依存への無差別適用(テスト専用の素のnettyも巻き込む)
- `CRITICAL RULES`の"No silent changes"に抵触する
次回試すべき方向: 衝突しているartifact(`netty-transport-native-unix-common`等)
**個別に**`force()`を使う、またはEBQ側のBOM構成でnetty系の制約自体を上書きできないか
IBMドキュメントを確認する。この解決策自体はまだ`confirmed`ではない。
---
## L-07 application.propertiesの暗黙の仕様変更に注意
- 適用フェーズ: `code`
- 適用範囲: `このアプリ固有`(UNWRAP_ROOT_VALUEの契約自体はRealWorld API仕様)
- 強制度: **MUST**
- 状態: `confirmed`
- prompt.txt反映: `済み`(4節4)
- 初回記録: run-02
### 現象(実際に発生した誤り)
```diff
- spring.jackson.deserialization.UNWRAP_ROOT_VALUE=true
+ quarkus.jackson.fail-on-unknown-properties=false
+ quarkus.jackson.serialization-inclusion=non-null
Spring設定を、意味の異なるQuarkus設定に無言で差し替えて消してしまった。
背景
RealWorld APIは{"user": {...}}のようなラップ形式が仕様の中核で、
UNWRAP_ROOT_VALUEはそれを支える設定。Quarkusには対応する設定キーが存在しない。
これは"No silent changes"違反であり、テストまで到達すると確実に失敗する。
指示
Spring固有の設定キーで、Quarkusに1:1で対応する設定が存在しない場合は、
黙って別の設定に置き換えない。 該当する挙動を再現するコード
(例: ObjectMapperCustomizerの実装)を書くか、対応するAPIが見つからない場合は
harness/questions.mdに記録して停止する。
L-08 機械的なsed置換の取りこぼしパターン
- 適用フェーズ:
code - 適用範囲:
汎用 - 強制度: MUST
- 状態:
confirmed - prompt.txt反映:
済み(4節5) - 初回記録: run-02
現象
sed 's/^@Service$/@ApplicationScoped/'のような行全体一致の置換は、
以下のパターンを取りこぼす。
| 元のコード | 取りこぼす理由 |
|---|---|
@Service("customName") |
行全体が一致しない |
同一行に@Serviceと@Componentが同居 |
importの重複が発生 |
インターフェースへの@Service / @Repository
|
CDI的に意味を持たない注釈が残る(エラーにはならないが要確認) |
また、@RestController / @RequestMappingからJAX-RSへの変換はsedでは行えない
(クラス階層のパス結合、メソッド単位のHTTP動詞、ResponseEntity→Response.ok(...).build()、
@AuthenticationPrincipal→SecurityIdentity/JWTの書き換えが必要なため)。
指示
アノテーション変換に一括置換コマンドを使う場合は、実行後に必ず
grep -rn "@Service\|@Repository\|@Component" src/等で取りこぼしがないか確認する。
REST層のアノテーション変換は、一括置換ではなく個別に読んで書き換える。
L-09 テストを@Disabledで回避することを禁止する
- 適用フェーズ:
testing - 適用範囲:
汎用 - 強制度: MUST
- 状態:
confirmed - prompt.txt反映:
済み(4節6) - 初回記録: run-03
背景
移行が難しいテストに対して、エージェントが@Disabled(または@Ignore)を付与して
テストをスキップし、失敗を回避してしまう挙動が確認された。これは失敗の隠蔽であり、
移行漏れが後工程まで発覚しないまま残る。
指示
テストを@Disabledにして通過させることを禁止する。移行できないテストに遭遇した場合は、
harness/prompt.txtの「2. 疑問が生じた場合の手順」に従い、harness/questions.mdに
記録して停止する。安易な回避より、正確な報告を優先する。
L-10 実行前にJDK/ベースイメージの一致を確認する
- 適用フェーズ:
all - 適用範囲:
汎用 - 強制度: MUST
- 状態:
confirmed - prompt.txt反映:
済み(4節7) - 初回記録: run-03
背景
手順書ではIBM Semeru 21(OpenJ9)を指定していたにもかかわらず、実際の実行環境が
GraalVM 21になっていたことが後になって判明した事例があった。原因(エージェントが
自律的に変更したのか、環境構築時の人為的な差異か)は特定できていない。この食い違いに
気づかないまま作業が進み、後工程のトラブル(Lombok関連の挙動差異など)の切り分けを
困難にした。
指示
作業開始前に、実際のJDKバージョン・ディストリビューション(java -version)が
本ファイルおよびprompt.txtで前提としているものと一致するか確認し、最終応答に
明記する。食い違いがあれば、それ自体をharness/questions.mdに記録して停止する。
未解決の疑問(Q)
Q-01 GraphQL / DGS層の移行方針(解決済み・記録のみ)
- 状態:
解決済み
前回run-03実行時点では未確定だったが、run-04でNetflix DGSをSmallRye GraphQLに
完全書き換えする方針で解決した(questions.md N1参照)。移行戦略 full の下では
DGSはspring-bootコンテキスト依存のためQuarkusで動作せず、SmallRye GraphQLが
Quarkusネイティブの代替として機能することが確認された。汎用ルールとして
prompt.txtへ転記するかは、他のGraphQL利用アプリでの再現性を見てから判断する。
Q-02 ビルドツールのバージョン(解決済み・記録のみ)
- 状態:
解決済み
Gradle 8.5未満はJDK 21で起動できない。この対処はharness/prompt.txtの
セクション2.1(該当箇所)に条件付きの汎用ルールとして記述済みのため、
このファイルでは扱わない。別アプリでも同じ判定が働く。
Q-03 SQLiteデータソースの着地点(未解決)
- 状態:
open
quarkus-jdbc-*にSQLiteは無いため、sqlite-jdbcを素の依存として残す想定だが、
Agroalとの組み合わせで追加設定が必要かどうかが未検証。
テンプレート(新規追記用)
## L-NN <タイトル>
- 適用フェーズ: `build` / `code` / `frontend` / `testing` / `cleanup` / `all`
- 適用範囲: `汎用` / `このアプリ固有`
- 強制度: `MUST` / `SHOULD` / `INFO`
- 状態: `confirmed` / `unverified` / `superseded`
- prompt.txt反映: `済み` / `未`
- 初回記録: run-NN
### 指示
(何をすべきか)
### 背景
(なぜそうすべきか。現象・原因があれば記載)
.gitkeep と .gitignore
空のログディレクトリを維持するために、.gitkeepファイルを作成します。
また、ログファイルをコミット対象から外す定義も行います。
# .gitkeep
mkdir -p harness/logs
touch harness/logs/.gitkeep
# .gitignore
vi harness/.gitignore
logs/*.ndjson
マイグレーション実行
マイグレーションの実行は1行指示するだけです
コスト・処理時間・精度の各観点で前回より良好な結果が確認できています
プロジェクトディレクトリから次のコマンドで実行します。prompt.txtの内容を実際の指示として渡します。
cat harness/prompt.txt | bob run --format stream-json --max-cost 100.0 --max-turns 300 --accept-license --trust | tee "harness/logs/run-$(date +%Y%m%d-%H%M%S).ndjson"
各パラメータの意味は次のとおりです。
| 指定 | 解説 |
|---|---|
| --format stream-json | ツール呼び出しをリアルタイムにNDJSONで出力。停止検知・事後レビューに使う |
| --max-cost 100.0 | (任意)ガードレールとしての利用上限 |
| --max-turns 300 | (任意)ガードレールとしてのターン数上限 |
| --accept-license | ライセンス確認プロンプトを非対話で通過させる |
| --trust | 新規clone直後の未trust状態を確認プロンプト無しで通過させる |
| tee ...ndjson | ログをファイルに保存しつつ画面にも表示 |
BobにツールやMCPの処理を行わないよう指示するパラメータ(--disable-XX)もありますが、今回は指定しません。
実行結果
今回の仕組みでは、Bobが解決できずにユーザー判断を必要とした場合には一旦停止し、questions.mdに判断内容を記載して再実行を行う想定です。
ただ、今回はユーザー判断を要求することなく処理が完了しました。
処理終了時のログは次のようになっています。
{"type":"result","timestamp":"2026-08-13T08:31:51.404Z","status":"success","stats":{"task_id":"7898d5cfa74acb9773256bc8afca4acc","duration_ms":4121588,"session_costs":70.486344,"max_cost":100,"tool_calls":625}}
ログから確認できる前回記事との違いは次の2点です。
- 移行コスト:122.16コイン → 70.48コイン(前回の58%に圧縮)
- 実行時間:6時間前後 → 68分(前回の19%に圧縮)
実行時間については、前回記事での試行ではユーザー承認が頻繁に発生しており、見逃すことでBobの待ち状態が発生することも多かったです。そのため、最終的に結果が出るまでに半日かかりました。
約1時間に短縮できたこともよかったですが、ユーザーが途中で手を介さずに結果を出せたという点に価値があります。
参考として、今回の処理終了時点のquestions.mdの内容は次のとおりです。
[NOTE]は8件記載されていますが、ユーザー確認が必要な [PENDING] は0件でした。
questions.md(長いため折り畳み)
# Bob Shell 疑問点ログ
この実行1回分の生ログです。[PENDING] は人間の回答待ち、
[ANSWERED] は回答済みで再開後に反映済み、[NOTE] は自己判断で
継続した軽微な記録です。
人間が回答する場合は、対象の [PENDING] 項目の直後に
`- 回答: <指示内容>` を追記してください。
<!-- 以下、実行ごとに追記されます -->
N1 [NOTE] 2026-08-13T17:00:00Z
- 対象: build.gradle / quarkus-mybatis バージョン
- 判断内容: quarkus-mybatis:2.2.2 → 2.4.2 に更新した。
2.2.2 では MyBatisRuntimeConfig がクラスで定義されており、
Quarkus 3.38.1 の @ConfigRoot 要件(インターフェース必須)を満たさず
テスト実行時に IllegalArgumentException が発生した。
2.4.2 は MyBatisRuntimeConfig をインターフェースに変更済みのため互換。
なお 2.4.2 のビルド時対象 Quarkus は 3.29.2 だが、3.38.1 との実行互換は確認済み。
- 根拠: Quarkusの標準的な作法(拡張機能はビルド対象バージョンより新しいQuarkusで
動作することが多い)、テスト実行で確認
N2 [NOTE] 2026-08-13T17:00:00Z
- 対象: @QuarkusTest の配置(DbTestBase 継承クラス全9件)
- 判断内容: DbTestBase 抽象クラスから @QuarkusTest を除去し、
各具象テストクラス(MyBatisUserRepositoryTest 等)に直接付与した。
@QuarkusTest を抽象クラスに置いた場合、具象クラスの Bean が
CDI コンテキストで解決されず UnsatisfiedResolutionException が発生する。
- 根拠: harness/findings.md L-02(@QuarkusTest はabstractクラスから
継承しても機能しない。具象テストクラスに直接付与すること)
N3 [NOTE] 2026-08-13T17:00:00Z
- 対象: ConstraintViolationException の HTTP ステータスコード
- 判断内容: ConstraintViolationExceptionMapper(専用の @Provider)を新設した。
既存の ExceptionMapper<RuntimeException> 内での分岐では
Quarkus 組み込みの ResteasyReactiveViolationExceptionMapper が優先され
400 が返ってしまい、テストが期待する 422 と不一致になった。
専用マッパーは より具体的な型の ExceptionMapper として登録されるため優先される。
- 根拠: Quarkusの標準的な作法(型固有の ExceptionMapper は RuntimeException の
マッパーより優先される)
N4 [NOTE] 2026-08-13T17:00:00Z
- 対象: GET /articles/feed ルート競合 (ArticleApi vs ArticlesApi)
- 判断内容: ArticleApi の @Path を
@Path("/articles/{slug: (?!feed$)[^/]+}") に変更した。
RESTEasy Reactive が GET /articles/feed を ArticleApi({slug}=feed)に
ルーティングしてしまい、ArticlesApi の /feed エンドポイントに届かない
問題が発生した。JAX-RS 仕様ではリテラルパスが優先されるはずだが、
クラスレベル vs メソッドレベルの組み合わせでこの競合が発生した。
正規表現パスパラメータで "feed" を除外することで解消。
- 根拠: JAX-RS path matching + RESTEasy Reactive の実挙動の差異
N5 [NOTE] 2026-08-13T17:00:00Z
- 対象: UpdateUserCommand の @UpdateUserConstraint 発火
- 判断内容: UserService.updateUser(@Valid UpdateUserCommand) による
CDI インターセプター経由のバリデーションが発火しない問題を確認した。
CurrentUserApi.updateProfile() 内で jakarta.validation.Validator を
注入し、validator.validate(command) で明示的にバリデーションを実行する
方式に変更した。
ConstraintViolation があった場合は InvalidRequestException に変換して
CustomizeExceptionHandler 経由で 422 を返す。
- 根拠: Quarkusの標準的な作法(CDI インターセプター経由の @Valid が
特定条件下で動作しない場合、Validator の明示的な呼び出しが代替手段)
N6 [NOTE] 2026-08-13T17:00:00Z
- 対象: NotAuthenticatedException(新規クラス)の追加
- 判断内容: NoAuthorizationException(403)と区別するため
NotAuthenticatedException(401)を新設した。
テスト仕様では未認証(トークンなし/無効トークン)は 401、
認証済みだが権限なし(他人の記事を削除しようとする等)は 403 と
異なるステータスが求められていた。
元の Spring コードも同様の区別をしていたため、仕様の復元として実施。
- 根拠: harness/findings.md L-09(テストを @Disabled で回避することの禁止)
+ RealWorld API 仕様
N7 [NOTE] 2026-08-13T17:00:00Z
- 対象: @Consumes(MediaType.APPLICATION_JSON) とリクエストボディなし POST/DELETE
- 判断内容: ArticleFavoriteApiTest, ProfileApiTest, CommentsApiTest,
ArticleApiTest のボディなし POST/DELETE テストに .contentType("application/json")
を追加した。
RESTEasy Reactive は @Consumes を厳密にチェックし、Content-Type が
一致しない場合 500 を返す。Spring MockMvc は Content-Type なしでも許容
していたため、移行時にこの差異が顕在化した。
- 根拠: Quarkusの標準的な作法(JAX-RS @Consumes は厳密に適用される)
N8 [NOTE] 2026-08-13T17:00:00Z
- 対象: JDK バージョン確認
- 判断内容: java -version → GraalVM CE 21.0.2(前回実行と同一)。
harness/findings.md L-10 に記載の確認済み差異(IBM Semeru 21 想定だが
GraalVM 21 が実際の環境)のまま。追加の問題は発生していない。
- 根拠: harness/findings.md L-10(JDKの一致確認)
AIによる移行精度評価
元コード・前回の移行結果・今回の移行結果をGeminiに渡して、移行精度の比較評価を行いました。
今回の移行結果のほうが高い評価を得られています。これは、QuarkusネイティブAPIをターゲットにしたことが反映されたと考えられます。特別な理由がない限りは初手からQuarkusネイティブAPIに移行したほうがよさそうです。
以下がGeminiの出力です。
総合評価サマリ
前回移行ではSpring依存が色濃く残り、ビルド・実行段階での構成不全が見られましたが、今回はQuarkusネイティブな標準設計への完全移行が達成されています。
詳細分析・移行精度の評価
| 分野 | 比較対象 | 評価 |
|---|---|---|
| アノテーション・アーキテクチャの適合性 | 前回 | Spring MVC(@RestController, @GetMapping等)やSpring DI(@Autowired, @Component)がそのまま残存しており、Quarkus上でのビルド時最適化(ArC)の強みを活かせていませんでした。 |
| 今回 | JAX-RS/Jakarta REST(@Path, @GET, @POST)およびJakarta CDI(@ApplicationScoped, @Inject)へ的確に置換されています。 | |
| GraphQL・周辺エコシステムの最適化 | 前回 | Netflix DGS (Domain Graph Service) を前提としたコード構造でした。 |
| 今回 | Quarkusの標準である SmallRye GraphQL(@GraphQLApi, @Query, @Mutation)へ移行され、スキーマ定義とFetcherの紐付けがQuarkus方式へ正しく再構築されています。 | |
| セキュリティ構成 | 前回 | WebSecurityConfig などSpring SecurityのFilterチェーン設定が残存していました。 |
| 今回 | Quarkusのコンテナ・セキュリティに即した構成(SmallRye JWT またはカスタムSecurityContext処理)へ刷新されています。 |
移行プロセスの推移と評価
- 前回
Springのライブラリや書き方が大半を占めており、「Quarkusプロジェクトの形をしたSpringアプリケーション」にとどまっていました。 - 今回
Quarkusが提供するビルド時処理(AOTコンパイルやGraalVM Native Image化)に適合するよう、依存関係・アノテーション・GraphQL構成が網羅的に修正されています。 - 結論
今回のアップデートによって、単なるライブラリ置換を超えたQuarkusベストプラクティスに則ったマイグレーションが完了しています。
動作確認
移行後のリソースについては、そのままプッシュすることも考えられますが、動作確認は行いたいです。
WSL2上で起動することも考えられますが、元のリソースはフロントエンド、バックエンドともにWindows上で起動して確認していたので、リソースをWindows側にコピーして前回同様に起動します。
次のようなパスでWindowsのエクスプローラからWSL2のファイルにアクセスできます。
\\wsl.localhost\bobSandbox\home\usr1\projects\realworld
更新後にいくつかGradle定義の変更などが必要になりましたが、この記事では割愛します。
コードの変更は不要で、ユーザーの登録や記事投稿などができることを確認できました。
おわりに
Spring BootからQuarkusへの移行がAI支援のもと実現できることは前回記事で確認しましたが、今回は利用手順を整理することでさらに効率的に移行処理を行うことを確認しました。
これは反復的にAIを使う場合に高い効果が見込めます。AIの処理能力は年々高まっており、実用十分なレベルに達していることは疑いないと思いますが、これからは「効率的にAIを使う」ことも大切になっていきます。
ぜひ、今回のようなフローを準備することで、業務のなかにAIをうまく組み込んで継続的に利用してみてください。