5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AIエージェントと挑むレガシー刷新:Spring Boot 4.0移行を支える「調査・変換・検証」の三段構え

5
Posted at

はじめに

GMOコネクトの星です。

「このシステム、動いてるけど中身がブラックボックス……手を入れるのが怖い😇」という状況、ご経験はないでしょうか。

社内製フレームワーク(以下「独自FW」)で構築された管理系Webアプリを、Spring Boot 4.0 + Java 17環境へ移行しました。この独自FWはオンプレミスの専用サーバに強く依存していてクラウド環境では動きません。「そのまま移植」の選択肢はなく、完全な置き換えです。

頼りにしたのはAIエージェント(Claude Code)ですが、AIに丸投げすれば終わりとはいきません。うまく使うには「型」が必要で、試行錯誤の末に**「調査・変換・検証」の三段構えのプロセス**にたどり着きました。

移行中に踏んだ地雷も含めて、現場の実録として残します。

先にまとめ

三位一体プロセスの概要:

フェーズ やること AIの使い方
調査 pom.xml・設定ファイルから依存関係を把握 「何が変わるか」を分類させる
変換 独自フレームワーク→Spring標準のコード変換 パターンを適用させる(作らせない)
検証 手順書通りにゼロから再構築して差分確認 再現性を物理的に担保する

主な変換パターン(9種):

カテゴリ Before After
DB接続 domain.selectOne() (独自FW) repository.findByLoginIdAndDeletedAtIsNull() (JPA)
DI @ApplicationScoped + @Inject (CDI) @Service + @Autowired (Spring)
Webフレームワーク @Path + @GET + Viewable (JAX-RS) @Controller + @GetMapping + Model (Spring MVC)
フィルター登録 web.xml FilterRegistrationBean (SecurityConfig.java)
認証I/F validate(Credentials, WebContext) (pac4j 3.x) validate(Credentials, WebContext, SessionStore) (pac4j 5.x)
CSRF管理 CsrfTokenGeneratorAuthorizer (Authorizer) CsrfTokenGeneratorMatcher (Matcher)
セッション操作 J2EContextキャスト + HttpSession 直接操作 sessionStore.set(context, key, value)
Hex変換 DatatypeConverter.printHexBinary() (JAXB) HexFormat.of().formatHex() (Java 17標準)
サーブレットAPI javax.servlet.* jakarta.servlet.*

バージョン変更:

ライブラリ Before After
pac4j-core 3.5.0 5.7.2
pac4j-http 3.5.0 5.7.2
j2e-pac4j 4.1.0 jee-pac4j:6.0.0(廃止・後継へ)
javax.servlet-api 4.0.1 jakarta.servlet-api:6.0.0
commons-fileupload 1.4 削除(Spring自動解析で代替)

【調査】AIで5分でわかる依存関係の全貌

移行で最初に詰まるのは「何がどこに依存しているか」の把握です。社内製フレームワークは、その名前でGoogle検索しても何も出てきません。

ここでAIエージェントに投げた最初のプロンプトがこれです。

このプロジェクトのpom.xmlと主要なConfigファイルを読んで、
以下を整理してください:
1. 独自フレームワーク固有の依存(社内製FW等)
2. Spring Bootへの移行で必ず変更が必要になるライブラリ
3. そのまま使えるライブラリ
4. 変換パターンを「機械的な置換」と「ロジックの書き換え」に分類

10分後には次の分類が返ってきました。

機械的な置換(AIが一括処理できる):

  • javax.*jakarta.* のパッケージ名変更(全ファイル共通)
  • @Inject@Autowired@ApplicationScoped@Service

ロジックの書き換え(ファイルごとに判断が必要):

  • 独自FW Clientの呼び出し → Spring Data JPA Repository
  • JAX-RSのエンドポイント定義 → Spring MVCのController
  • pac4j 3.x → 5.x のAPIシグネチャ変更

この分類が重要です。「機械的な置換」を先に済ませてから「ロジックの書き換え」に集中する、という順序立てができます。規模感もつかめ、「全体で何日かかりそうか」の見通しを立てられました。

【変換1】DB接続:独自FW Client → Spring Data JPA

独自FWのDB接続は、Domainクラスを介した独自APIでした。

// Before: 独自FW Client
AppAccountDomain domain = new AppAccountDomain();
domain.setLoginId(credentials.getUsername());
Map<String, Object> map = domain.selectOne();  // 独自FW固有

// After: Spring Data JPA
Optional<AppAccount> account =
    appAccountRepository.findByLoginIdAndDeletedAtIsNull(
        credentials.getUsername()
    );

見た目はシンプルですが、JPAへの変換で考える点が2つあります。

Spring Data JPAの命名規則への適合

独自FWのDomainクラスは「setLoginId()してselectOne()する」という手続き型でした。JPAのRepositoryはメソッド名が検索条件になる宣言型です。

public interface AppAccountRepository extends JpaRepository<AppAccount, Integer> {
    // メソッド名から自動でSQL生成される
    Optional<AppAccount> findByLoginIdAndDeletedAtIsNull(String loginId);
    Optional<AppAccount> findByMailAddressAndIdNotAndDeletedAtIsNull(String email, int id);
}

deletedAt IS NULL(論理削除チェック)のような条件を、元コードのどこで処理していたかを追う作業がポイントです。独自FWのDomainクラス内に隠れていることが多く、AIに「このDomainクラスが内部でどんなWHERE句を生成しているか推測して」と聞くと、類似パターンから解釈してくれます(ただし推測なので要確認)。

楽観ロックは手動実装が必要

独自FWは楽観ロックを自動でハンドリングしていました。JPA移行後はそのサポートがなくなり、手動チェックが必要になります。

// Before: 独自FW が OptimisticConcurrencyException を自動スロー
domain.setLockVersion(oldLockVersion);
domain.update();

// After: バージョンを手動チェックしてからsave
AppRole entity = appRoleRepository.findById(roleId).orElseThrow();
if (entity.getLockVersion() != Integer.parseInt(form.getLockVersion())) {
    throw new AppException("E_APP_00_0002", "更新失敗");
}
entity.setLockVersion(entity.getLockVersion() + 1);
appRoleRepository.save(entity);

これはAIが自動で変換してくれない部分です。独自FW固有の例外型が消えたタイミングで「ここに楽観ロックが隠れていた」と気づけるかどうかが勘所です。

【変換2】認証:pac4j 3.x → 5.x の地雷を踏まずに進む

今回の移行で最も変更量が多かったのが pac4j の認証まわりです。

APIシグネチャの一覧変更

pac4j 5.x でインターフェースが大きく変わりました。

項目 Before (3.x) After (5.x)
Authenticator 型引数 Authenticator<UsernamePasswordCredentials> Authenticator(ジェネリクス廃止)
validate() 引数 (Credentials, WebContext) (Credentials, WebContext, SessionStore)
ProfileAuthorizer 型引数 ProfileAuthorizer<CommonProfile> ProfileAuthorizer(ジェネリクス廃止)
isAuthorized() 引数 (WebContext, List<Profile>) (WebContext, SessionStore, List<UserProfile>)
Pac4jConstants パッケージ org.pac4j.core.context org.pac4j.core.util

validate()の引数が2つから3つに増えた変更は、コンパイルエラーとして即座に検出できます。が、ProfileAuthorizerのジェネリクス廃止は「CommonProfileUserProfileへの型変更」も伴うため、型を追って直すことになります。

CsrfAuthorizer が消えた話

最もはまったのがCSRF管理です。CsrfAuthorizerを探してもpac4j 5.xには存在しません。

役割が分離されました。

pac4j 3.x pac4j 5.x 役割
CsrfTokenGeneratorAuthorizer CsrfTokenGeneratorMatcher トークン生成
CsrfAuthorizer CsrfAuthorizer(継続) トークン検証

SecurityConfigの設定も変わります。

// Before (web.xml): authorizers にまとめて書いていた
// authorizers="csrfToken,mustBeAnon"

// After (SecurityConfig.java): 生成は matchers、検証は authorizers に分離
SecurityFilter filter = new SecurityFilter(config, "mustBeAnon");
filter.setMatchers("csrfToken");        // ← 生成をMatcherで担当
filter.setAuthorizers("mustBeAuth");    // ← 検証はAuthorizer側

config.addMatcher("csrfToken", new CsrfTokenGeneratorMatcher(...)) という登録も必要です。ドキュメントを読んだだけでは気づきにくく、既存プロジェクトの移行済みコードを参照して判明しました。

セッション操作はSessionStore経由に統一

pac4j 5.xではJ2EContextクラスが廃止されています。セッションへの書き込みはすべてSessionStore経由になります。

// Before: J2EContext にキャストして直接操作
((J2EContext) context).getRequest().getSession(true).setAttribute(key, value);

// After: SessionStore 経由(JEE環境では内部でHttpSessionに委譲)
sessionStore.set(context, key, value);

ただし、フレームワーク外から呼ばれるstaticメソッドではSessionStoreインスタンスを受け取れません。validate()内でsessionStore.set()した値はJEE環境ではHttpSessionに保存されるので、static側ではHttpSessionから直接取得できます。

// static メソッドでのセッション読み取り
HttpSession session = request.getSession(false);
return (session == null) ? null : (String) session.getAttribute(SESSION_KEY_USER_ID);

【変換3】フレームワーク:JAX-RS → Spring MVC

Webフレームワークの置き換えは、変換パターンが明確なので最も機械的に進められます。

// Before: JAX-RS
@Path("/login/")
@Produces(TEXT_HTML)
public class LoginResource {
    @GET
    public Viewable index() {
        Map<String, Object> model = new HashMap<>();
        model.put("callbackUrl", callbackUrl);
        return new Viewable("/login/index", model);
    }
}

// After: Spring MVC
@Controller
@RequestMapping("/login/")
public class LoginResource {
    @Autowired private Config config;

    @GetMapping
    public String index(Model model) {
        model.addAttribute("callbackUrl", callbackUrl);
        return "/login/index";
    }
}

フィルターチェーンもweb.xmlからFilterRegistrationBeanに移行します。

@Configuration
public class SecurityConfig {
    @Bean
    public FilterRegistrationBean<LoggerFilter> loggerFilter() {
        FilterRegistrationBean<LoggerFilter> bean =
            new FilterRegistrationBean<>(new LoggerFilter());
        bean.setOrder(1);
        bean.addUrlPatterns("/*");
        return bean;
    }
    // ... 以降、orderを2〜7でフィルターを登録
}

web.xmlに書かれたフィルターの順序がsetOrder()の数値に対応します。既存のweb.xmlをAIに読み込ませて「これをFilterRegistrationBeanに変換して」と頼むと、変換テーブルをそのままコードに落としてくれます。

commons-fileupload の撤廃

javax.servlet依存のcommons-fileuploadは削除しました。Spring Bootが自動でマルチパートを解析してくれるので、MultipartFileで直接受け取れます。

// Before: commons-fileupload で手動パース
ServletFileUpload upload = new ServletFileUpload(new DiskFileItemFactory());
List<FileItem> items = upload.parseRequest(request);

// After: Spring Boot が自動解析済み
MultipartFile file = form.getUploadFile();
file.transferTo(new File(uploadDir));

【検証】ゼロベース検証:手順書を物理的に試す

AIが生成したコードを「動きそう」と目視確認するだけでは不十分です。手順書に書いた通りに本当に動くかを確認する方法として「ゼロベース検証」を取り入れました。

手順:

  1. 移行済みコードを別ブランチに退避
  2. 元のコードに戻す
  3. 手順書(移行記録の.mdファイル)に書いた変更を、一から手で実施
  4. 退避した移行済みコードとdiffを取る

差分が0なら手順書が完全だということです。差分がある場合、それは「手順書に書き忘れた変更」か「移行時に判断した内容が記録されていない」ことを意味します。

この「再現性テスト」の目的は、コードの正しさではなく手順書の正しさを確認することです。移行作業では往々にして「なんとなく直したら動いた」という変更が紛れ込みます。それが記録されていないと、次に同じ作業をする人が詰まります。

実際に今回の検証で発見したのが、社内共通ライブラリJAR内のクラスを本プロジェクトへコピーした際の変更(jakarta.interceptorへの置換、HexFormatへの書き換え)が移行記録に抜けていた点でした。

【番外】泥臭いトラブル集

ハマり1: AIがクラスを勝手に作った

二段階認証クラスの移行中、参照先クラス(ExternalUtil)が見つからないとAIが判断し、新規クラスを作成してしまいました。

当然ながら、作ったクラスの中身は推測と補完の産物です。実際には別モジュールに存在するクラスでした。

この教訓からルールを作りました:

参照先クラスが存在しない場合、AIは作成せず報告のみ行う。

プロンプトに明示します。

参照先クラスが見つからない場合、クラスを新規作成しないでください。
代わりに「XXXクラスが見つかりません」と報告してください。

レガシー移行では「クラスが存在しない=消えた」ではなく「まだ移行されていない」ことがほとんどです。AIはコンパイルを通そうとして補完しようとするので、この制約は必須です。

ハマり2: VSCode の Java Language Server がMavenの変更を認識しない

pom.xmlにライブラリを追加しても、VSCode上でimportエラーが消えない状況がありました。

解決策は以下の順番で試します。

  1. VSCodeのコマンドパレットから「Java: Clean Java Language Server Workspace」を実行
  2. それでも消えない場合、mvn clean compile をターミナルで実行してエラーが実際にあるか確認
  3. Mavenのエラーが出ない場合、VSCodeの表示上の問題なのでウィンドウのリロード(Developer: Reload Window

mvn compile が通ればビルド上は問題ありません。VSCodeの表示エラーはエディタ側のキャッシュ問題であることが多いので、まずMavenを正として切り分けてください。

ハマり3: JAXB の HexFormat 置き換えで精度確認を忘れない

// Before: javax.xml.bind.DatatypeConverter(Java 11以降削除)
String hex = DatatypeConverter.printHexBinary(bytes);

// After: java.util.HexFormat(Java 17標準)
String hex = HexFormat.of().formatHex(bytes);

DatatypeConverter.printHexBinary()は大文字16進数を返しますが、HexFormat.of().formatHex()はデフォルトで小文字です。大文字が必要な場合は末尾に.toUpperCase()を繋いで変換してください。DBに保存されている既存データの形式との一致を事前に確認してください。

ハマり4: 移行時に既存バグを発見

CustomAuthenticatorの移行中に、元コードにStringの参照比較バグを発見しました。

// 旧コード(バグ): == はオブジェクト参照の比較
(adminAccountId == "1") ? "1" : "0"

// 修正: 文字列の内容比較には equals()
adminAccountId.equals("1") ? "1" : "0"

JVMのString intern化が偶然機能している間は動いてしまいますが、DBから取得したStringは基本的にintern化されません。AIがコードを精読する過程でこのバグが浮かび上がりました。移行作業はコードレビューの機会でもあります。

まとめ

  • 調査フェーズで**「機械的な置換」と「ロジックの書き換え」を分類**することで、AIに任せる範囲と人が判断する範囲が明確になります
  • pac4j 5.xの最大の変化はCSRF管理の役割分離(Authorizer → Matcher)とSessionStore引数の追加。バージョン間差異はAIが混乱しやすい部分なので、公式Changelogを手元に置いておくのが確実です
  • レガシー移行では**「作成ではなく報告」のプロンプト制約**が必須。クラスが見つからない=まだ移行されていない、と読み替えてください
  • ゼロベース検証は、目視確認では気づけない「記録されていない変更」を炙り出す有効な手段です

AIエージェントは「何かを作る」だけでなく「何かを把握する」ツールとして強力です。ブラックボックスの解析から始まる移行作業でこそ、その使いどころがあります。


最後に、GMOコネクトではサービス開発支援や技術支援をはじめ、幅広い支援を行っておりますので、何かありましたらお気軽にお問合せください。

お問合せ:https://gmo-connect.jp/contactus/

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?