Java入門 / コード生成AI

【Gemini Code Assist】Thymeleaf を利用して画面を作成する | VS Code + Gemini Code Assist で Spring Boot アプリケーション(2)

2025.05.23

前回までの記事では、VS Code で Gemini Code Assist を利用して Java プログラミングを行う方法と、簡単な Spring Boot アプリケーション(REST API )を作成する方法について説明しました。

当記事では、作成した Spring Boot アプリケーションに、Thymeleaf テンプレートを利用して画面を追加するところまで Gemini Code Assist を利用して実際に行ってみます。

◆Gemini Code Assist の記事一覧はこちら
◆Java入門 記事一覧はこちら

当記事の前提

当記事では、前回の記事(VS Code + Gemini Code Assist で Spring Boot アプリケーション(1)REST API)で作成した Spring Boot プロジェクトに対して、Thymeleaf テンプレートを利用して画面を追加します。

当記事で行っている手順をお試しされる場合は、前回の記事をご参照の上、同じ内容の Spring Boot プロジェクトを準備してお試しください。

Thymeleaf テンプレートの作成

事前準備

Thymeleaf ライブラリの確認

VS Code でデフォルト設定で Spring Boot プロジェクトを作成した場合、Thymeleaf のライブラリが読み込まれていないため、Maven を利用して追加します。(作成時に追加していた場合は不要です)

まず、以下の内容を pom.xml の dependencies の中に追加します。もし同じ内容が既に設定されていた場合は Thymeleaf を追加済です。

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>


pom.xml を保存すると、右下にビルド確認のメッセージが表示されますので、Yes をクリックしてビルドを進めてください。


メッセージが出てこない、または閉じてしまった場合は、左側のエクスプローラーから「MAVEN」→「demo(プロジェクト名)」→「Lifecycle」→「package」の順に選択することでビルドすることもできます。ビルドに失敗する場合は一度 clean を実行してから、再度お試しください。

Gemini Code Assist の状態を確認

Gemini Code Assist のチャットスペースからファイルを作成させるには、プロジェクトの場所(ルートフォルダ)を認識できていると比較的簡単です。(毎回作成先をフルパスで指定することもできます)

前回作成したプロジェクトを開きなおした場合などでは、Gemini Code Assist が一時的に記憶していた情報を覚えていない可能性があるため、再度パスを指定してプロジェクトのルートフォルダを再確認しておきましょう。


また、今回作成する Thymeleaf テンプレートで表示するのは、前回の記事で Gemini Code Assist を利用して生成した CartInfo クラスの情報です。Listで保持している CartItem クラスとあわせ、チャット欄で「@」を利用しファイルを指定して内容を再確認させておきましょう。

package com.example.demo.bean;

import java.util.List;

/**
 * カート全体の情報を格納するBeanクラスです。
 * cartInfo.json の各項目に対応します。
 */
public class CartInfo {

    /**
     * ユーザーID
     * 例: "USER789"
     */
    private String userId;

    /**
     * 送付者名
     * 例: "山田 太郎"
     */
    private String senderName;

    /**
     * 送付先名
     * 例: "佐藤 花子"
     */
    private String recipientName;

    /**
     * 郵便番号
     * 例: "100-0005"
     */
    private String postalCode;

    /**
     * 送付先住所・都道府県
     * 例: "東京都"
     */
    private String recipientAddressPrefecture;

    /**
     * 送付先住所・市区町村
     * 例: "千代田区"
     */
    private String recipientAddressCity;

    /**
     * 送付先住所・番地等
     * 例: "丸の内1-2-3"
     */
    private String recipientAddressLine1;

    /**
     * 送付先住所・建物名 (任意)
     * 例: "パシフィックセンチュリープレイス丸の内 20F"
     */
    private String recipientAddressLine2;

    /**
     * カート内商品情報のリスト
     */
    private List<CartItem> items;

    /**
     * デフォルトコンストラクタ
     */
    public CartInfo() {
    }

    /**
     * 全てのフィールドを初期化するコンストラクタ
     * @param userId ユーザーID
     * @param senderName 送付者名
     * @param recipientName 送付先名
     * @param postalCode 郵便番号
     * @param recipientAddressPrefecture 送付先住所・都道府県
     * @param recipientAddressCity 送付先住所・市区町村
     * @param recipientAddressLine1 送付先住所・番地等
     * @param recipientAddressLine2 送付先住所・建物名
     * @param items カート内商品リスト
     */
    public CartInfo(String userId, String senderName, String recipientName, String postalCode,
                    String recipientAddressPrefecture, String recipientAddressCity,
                    String recipientAddressLine1, String recipientAddressLine2, List<CartItem> items) {
        this.userId = userId;
        this.senderName = senderName;
        this.recipientName = recipientName;
        this.postalCode = postalCode;
        this.recipientAddressPrefecture = recipientAddressPrefecture;
        this.recipientAddressCity = recipientAddressCity;
        this.recipientAddressLine1 = recipientAddressLine1;
        this.recipientAddressLine2 = recipientAddressLine2;
        this.items = items;
    }

    // --- GetterおよびSetterメソッド ---

    /** @return ユーザーID */
    public String getUserId() { return userId; }
    /** @param userId ユーザーID */
    public void setUserId(String userId) { this.userId = userId; }

    /** @return 送付者名 */
    public String getSenderName() { return senderName; }
    /** @param senderName 送付者名 */
    public void setSenderName(String senderName) { this.senderName = senderName; }

    /** @return 送付先名 */
    public String getRecipientName() { return recipientName; }
    /** @param recipientName 送付先名 */
    public void setRecipientName(String recipientName) { this.recipientName = recipientName; }

    /** @return 郵便番号 */
    public String getPostalCode() { return postalCode; }
    /** @param postalCode 郵便番号 */
    public void setPostalCode(String postalCode) { this.postalCode = postalCode; }

    /** @return 送付先住所・都道府県 */
    public String getRecipientAddressPrefecture() { return recipientAddressPrefecture; }
    /** @param recipientAddressPrefecture 送付先住所・都道府県 */
    public void setRecipientAddressPrefecture(String recipientAddressPrefecture) { this.recipientAddressPrefecture = recipientAddressPrefecture; }

    /** @return 送付先住所・市区町村 */
    public String getRecipientAddressCity() { return recipientAddressCity; }
    /** @param recipientAddressCity 送付先住所・市区町村 */
    public void setRecipientAddressCity(String recipientAddressCity) { this.recipientAddressCity = recipientAddressCity; }

    /** @return 送付先住所・番地等 */
    public String getRecipientAddressLine1() { return recipientAddressLine1; }
    /** @param recipientAddressLine1 送付先住所・番地等 */
    public void setRecipientAddressLine1(String recipientAddressLine1) { this.recipientAddressLine1 = recipientAddressLine1; }

    /** @return 送付先住所・建物名 */
    public String getRecipientAddressLine2() { return recipientAddressLine2; }
    /** @param recipientAddressLine2 送付先住所・建物名 */
    public void setRecipientAddressLine2(String recipientAddressLine2) { this.recipientAddressLine2 = recipientAddressLine2; }

    /** @return カート内商品リスト */
    public List<CartItem> getItems() { return items; }
    /** @param items カート内商品リスト */
    public void setItems(List<CartItem> items) { this.items = items; }

    @Override
    public String toString() {
        return "CartInfo{" +
                "userId='" + userId + '\'' +
                ", senderName='" + senderName + '\'' +
                ", recipientName='" + recipientName + '\'' +
                ", postalCode='" + postalCode + '\'' +
                ", recipientAddressPrefecture='" + recipientAddressPrefecture + '\'' +
                ", recipientAddressCity='" + recipientAddressCity + '\'' +
                ", recipientAddressLine1='" + recipientAddressLine1 + '\'' +
                ", recipientAddressLine2='" + recipientAddressLine2 + '\'' +
                ", items=" + (items != null ? items.size() + " items" : "null") +
                '}';
    }
}


他にも、新しく作成するファイルに必要な情報を持つファイルについての情報を Gemini Code Assist が正しく認識できない場合、コード生成時に既存のファイルを認識できずに新しく作成しようとしたり、仮定の情報を基に新しいコードを作成する場合があります。

作業途中で齟齬が発生するような場合は、チャットの入力欄で「@ファイル名」を利用し必要なファイルの内容を認識させて、再度プロンプトを実行してみましょう。


参考:個人向けの Gemini Code Assist を使用したコード(developers.google.com)

テンプレートファイルの作成

情報を表示するための Thymeleaf テンプレートを、Gemini Code Assist を利用して作成します。作成するファイルのパスと名前、どのようなデザインにしたいか、特定条件時の表示などテンプレートファイルで行いたいことを、チャット欄に入力して送信します。

今回は以下のようなプロンプトを送信しました。

@CartInfo.java
このクラスの情報を表示する Thymeleaf のテンプレートファイルを、プロジェクトの src/main/resources/templates フォルダ内に新しく作成してください。
・ファイル名は viewcart.html としてください。
・デザインは明るいグレーを基調としたメタリックなイメージで、カートが見つからなかった場合や、カートに商品が入っていない場合には、それぞれ状態がわかるような表示としてください。
・静的HTMLとして表示した時に表示されるサンプルデータの住所は架空の住所/建物としてください。


しばらく待つと、Gemini Code Assist から回答が返ってきます。


HTMLコードだけでなく、生成されたコードのポイントについても説明されていました。


テンプレートの内容を見ると、静的HTMLファイルとして表示した場合のみのサンプルデータが準備されているなど、Thymeleaf を使うメリットが考慮された内容になっています。

画面のデザインについては、HTMLコードだけで把握するのは難しいので、ひとまずこの内容でファイル(viewcart.html)を保存します。

(実際に提案されたHTMLコードの内容)

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>カート情報</title>
    <style>
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background-color: #f0f2f5; /* 明るいグレー */
            color: #333;
            margin: 0;
            padding: 20px;
            display: flex;
            justify-content: center;
            align-items: flex-start;
            min-height: 100vh;
        }
        .container {
            background: linear-gradient(145deg, #e6e6e6, #ffffff); /* メタリック感を出すためのグラデーション */
            border-radius: 12px;
            box-shadow: 8px 8px 16px #c4c4c4,
                        -8px -8px 16px #ffffff; /* ニューモーフィズム風の影 */
            padding: 30px;
            width: 100%;
            max-width: 900px;
            border: 1px solid #d1d1d1; /* 細い境界線 */
        }
        h1, h2 {
            color: #4a4a4a; /* 少し濃いグレー */
            border-bottom: 2px solid silver;
            padding-bottom: 10px;
            margin-top: 0;
        }
        h1 {
            text-align: center;
            margin-bottom: 30px;
            font-size: 2em;
        }
        h2 {
            font-size: 1.5em;
            margin-top: 25px;
            margin-bottom: 15px;
        }
        .info-section, .items-section {
            margin-bottom: 30px;
            padding: 20px;
            background-color: #f9f9f9; /* セクション背景 */
            border-radius: 8px;
            border: 1px solid #e0e0e0;
        }
        .info-grid {
            display: grid;
            grid-template-columns: 150px 1fr;
            gap: 10px;
        }
        .info-grid dt {
            font-weight: bold;
            color: #555;
        }
        .info-grid dd {
            margin-left: 0;
            color: #333;
        }
        table {
            width: 100%;
            border-collapse: collapse;
            margin-top: 15px;
        }
        th, td {
            border: 1px solid #c0c0c0; /* メタリックな境界線 */
            padding: 12px 15px;
            text-align: left;
        }
        th {
            background-color: #d0d0d0; /* ヘッダー背景 */
            color: #222;
            font-weight: bold;
        }
        tr:nth-child(even) {
            background-color: #e9e9e9; /* テーブル行の交互色 */
        }
        .message {
            padding: 15px 20px;
            margin: 20px 0;
            border-radius: 8px;
            text-align: center;
            font-size: 1.1em;
            font-weight: bold;
        }
        .message-error {
            background-color: #ffebee; /* 赤系 */
            color: #c62828;
            border: 1px solid #e57373;
        }
        .message-info {
            background-color: #e3f2fd; /* 青系 */
            color: #1565c0;
            border: 1px solid #64b5f6;
        }
        .total-summary {
            text-align: right;
            margin-top: 20px;
            font-size: 1.2em;
            font-weight: bold;
        }
        .total-summary span {
            color: #d32f2f; /* 合計金額の色 */
        }
    </style>
</head>
<body>

<div class="container">
    <h1>カート情報</h1>

    <!-- カートが見つからない場合 -->
    <div th:if="${cartInfo == null}" class="message message-error">
        カートが見つかりませんでした。
    </div>

    <div th:unless="${cartInfo == null}" th:object="${cartInfo}">
        <div class="info-section">
            <h2>お客様情報</h2>
            <dl class="info-grid">
                <dt>ユーザーID:</dt>
                <dd th:text="*{userId}">USER_SAMPLE_001</dd>
                <dt>送付者名:</dt>
                <dd th:text="*{senderName}">架空 送付者</dd>
            </dl>
        </div>

        <div class="info-section">
            <h2>お届け先情報</h2>
            <dl class="info-grid">
                <dt>お名前:</dt>
                <dd th:text="*{recipientName}">架空 宛名</dd>
                <dt>郵便番号:</dt>
                <dd th:text="*{postalCode}">〒999-9999</dd>
                <dt>住所:</dt>
                <dd>
                    <span th:text="*{recipientAddressPrefecture}">架空県</span>
                    <span th:text="*{recipientAddressCity}">架空市</span>
                    <span th:text="*{recipientAddressLine1}">見本町1-23-45</span>
                    <span th:if="*{recipientAddressLine2 != null && !recipientAddressLine2.isEmpty()}" th:text="*{recipientAddressLine2}">架空ビルディング 101号室</span>
                </dd>
            </dl>
        </div>

        <div class="items-section">
            <h2>ご注文商品</h2>
            <!-- カートに商品がない場合 -->
            <div th:if="${#lists.isEmpty(cartInfo.items)}" class="message message-info">
                カートに商品がありません。
            </div>

            <div th:unless="${#lists.isEmpty(cartInfo.items)}">
                <table>
                    <thead>
                        <tr>
                            <th>商品ID</th>
                            <th>商品名</th>
                            <th>単価 (税込)</th>
                            <th>数量</th>
                            <th>小計 (税込)</th>
                            <th>商品ページ</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr th:each="item : ${cartInfo.items}">
                            <td th:text="${item.productId}">ITEM_SAMPLE_01</td>
                            <td th:text="${item.productName}">サンプル商品名</td>
                            <td th:text="${#numbers.formatInteger(item.priceWithTax, 0, 'COMMA')} + ' 円'">1,000 円</td>
                            <td th:text="${item.quantity}">1</td>
                            <td th:text="${#numbers.formatInteger(item.totalAmount, 0, 'COMMA')} + ' 円'">1,000 円</td>
                            <td><a th:href="${item.productPageUrl}" th:text="${item.productPageUrl}" target="_blank">https://example.com/sample</a></td>
                        </tr>
                        <!-- 静的HTML表示用のサンプルデータ (Thymeleaf処理時は表示されない) -->
                        <tr th:if="${#lists.isEmpty(cartInfo.items) && cartInfo == null}"> <!-- この条件は実際にはThymeleaf処理時には満たされないが、静的表示の意図を示す -->
                            <td>ITEM_STATIC_01</td>
                            <td>静的サンプル商品A</td>
                            <td>1,500 円</td>
                            <td>2</td>
                            <td>3,000 円</td>
                            <td><a href="#">#</a></td>
                        </tr>
                        <tr th:if="${#lists.isEmpty(cartInfo.items) && cartInfo == null}">
                            <td>ITEM_STATIC_02</td>
                            <td>静的サンプル商品B</td>
                            <td>500 円</td>
                            <td>3</td>
                            <td>1,500 円</td>
                            <td><a href="#">#</a></td>
                        </tr>
                    </tbody>
                </table>
                <div class="total-summary">
                    総合計: <span th:text="${#numbers.formatInteger(#aggregates.sum(cartInfo.items.![totalAmount]), 0, 'COMMA')} + ' 円'">0 円</span>
                </div>
            </div>
        </div>
    </div>
</div>

</body>
</html>


※実際には、実行するユーザーのプロンプトの傾向によって多少異なる内容が提案されると思いますが、概ね同じような内容のテンプレートが提案されると思います。

デザイン確認(ブラウザ表示)

テンプレートファイルを保存したら、実際にWebブラウザ上に表示して、デザインを確認してみます。ブラウザのURLの欄にファイルパスを直接入力するか、VS Code からファイルをブラウザにドラッグ&ドロップして、作成したHTMLファイルを表示します。


修正したい箇所などがあれば、内容をチャットで指示します。今回作成したファイルにもいくつか気になる点がありましたので、以下の内容で修正の指示を行います。

@viewcart.html 
以下の変更を加えてください。
・カートに商品がない場合のメッセージは、カートが見つからない場合のメッセージと同じスタイルとする。
・カート内商品の商品IDは非表示とする。
・商品情報の数値項目(単価、数量、小計)は右寄せにする。ヘッダーの項目名は変えなくて良い。
・項目名の「総合計」は「合計金額」とし、合計金額のフォント色は項目名部と同じ色とする。


修正する内容と一緒に、修正後のHTMLコードが Gemini Code Assist から提示されました。


提示された修正内容を適用後、Webブラウザで表示を更新して変更内容を確認します。指摘した箇所が指定した通り修正されました。他にも変更したい箇所があれば、チャットで修正を繰り返して完成させてください。


(実際に修正案として提案されたHTMLコードの内容)

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>カート情報</title>
    <style>
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background-color: #f0f2f5; /* 明るいグレー */
            color: #333;
            margin: 0;
            padding: 20px;
            display: flex;
            justify-content: center;
            align-items: flex-start;
            min-height: 100vh;
        }
        .container {
            background: linear-gradient(145deg, #e6e6e6, #ffffff); /* メタリック感を出すためのグラデーション */
            border-radius: 12px;
            box-shadow: 8px 8px 16px #c4c4c4,
                        -8px -8px 16px #ffffff; /* ニューモーフィズム風の影 */
            padding: 30px;
            width: 100%;
            max-width: 900px;
            border: 1px solid #d1d1d1; /* 細い境界線 */
        }
        h1, h2 {
            color: #4a4a4a; /* 少し濃いグレー */
            border-bottom: 2px solid silver;
            padding-bottom: 10px;
            margin-top: 0;
        }
        h1 {
            text-align: center;
            margin-bottom: 30px;
            font-size: 2em;
        }
        h2 {
            font-size: 1.5em;
            margin-top: 25px;
            margin-bottom: 15px;
        }
        .info-section, .items-section {
            margin-bottom: 30px;
            padding: 20px;
            background-color: #f9f9f9; /* セクション背景 */
            border-radius: 8px;
            border: 1px solid #e0e0e0;
        }
        .info-grid {
            display: grid;
            grid-template-columns: 150px 1fr;
            gap: 10px;
        }
        .info-grid dt {
            font-weight: bold;
            color: #555;
        }
        .info-grid dd {
            margin-left: 0;
            color: #333;
        }
        table {
            width: 100%;
            border-collapse: collapse;
            margin-top: 15px;
        }
        th, td {
            border: 1px solid #c0c0c0; /* メタリックな境界線 */
            padding: 12px 15px;
            text-align: left;
        }
        th {
            background-color: #d0d0d0; /* ヘッダー背景 */
            color: #222;
            font-weight: bold;
        }
        tr:nth-child(even) {
            background-color: #e9e9e9; /* テーブル行の交互色 */
        }
        .message {
            padding: 15px 20px;
            margin: 20px 0;
            border-radius: 8px;
            text-align: center;
            font-size: 1.1em;
            font-weight: bold;
        }
        .message-error {
            background-color: #ffebee; /* 赤系 */
            color: #c62828;
            border: 1px solid #e57373;
        }
        .message-info {
            background-color: #e3f2fd; /* 青系 */
            color: #1565c0;
            border: 1px solid #64b5f6;
        }
        .total-summary {
            text-align: right;
            margin-top: 20px;
            font-size: 1.2em;
            font-weight: bold;
        }
        .total-summary span {
            /* color: #d32f2f; */ /* 合計金額の色を項目名と同じにするため削除 */
        }
    </style>
</head>
<body>

<div class="container">
    <h1>カート情報</h1>

    <!-- カートが見つからない場合 -->
    <div th:if="${cartInfo == null}" class="message message-error">
        カートが見つかりませんでした。
    </div>

    <div th:unless="${cartInfo == null}" th:object="${cartInfo}">
        <div class="info-section">
            <h2>お客様情報</h2>
            <dl class="info-grid">
                <dt>ユーザーID:</dt>
                <dd th:text="*{userId}">USER_SAMPLE_001</dd>
                <dt>送付者名:</dt>
                <dd th:text="*{senderName}">架空 送付者</dd>
            </dl>
        </div>

        <div class="info-section">
            <h2>お届け先情報</h2>
            <dl class="info-grid">
                <dt>お名前:</dt>
                <dd th:text="*{recipientName}">架空 宛名</dd>
                <dt>郵便番号:</dt>
                <dd th:text="*{postalCode}">〒999-9999</dd>
                <dt>住所:</dt>
                <dd>
                    <span th:text="*{recipientAddressPrefecture}">架空県</span>
                    <span th:text="*{recipientAddressCity}">架空市</span>
                    <span th:text="*{recipientAddressLine1}">見本町1-23-45</span>
                    <span th:if="*{recipientAddressLine2 != null && !recipientAddressLine2.isEmpty()}" th:text="*{recipientAddressLine2}">架空ビルディング 101号室</span>
                </dd>
            </dl>
        </div>

        <div class="items-section">
            <h2>ご注文商品</h2>
            <!-- カートに商品がない場合 -->
            <div th:if="${#lists.isEmpty(cartInfo.items)}" class="message message-error">
                カートに商品がありません。
            </div>

            <div th:unless="${#lists.isEmpty(cartInfo.items)}">
                <table>
                    <thead>
                        <tr>
                            <th>商品名</th>
                            <th>単価 (税込)</th>
                            <th>数量</th>
                            <th>小計 (税込)</th>
                            <th>商品ページ</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr th:each="item : ${cartInfo.items}">
                            <td th:text="${item.productName}">サンプル商品名</td>
                            <td style="text-align: right;" th:text="${#numbers.formatInteger(item.priceWithTax, 0, 'COMMA')} + ' 円'">1,000 円</td>
                            <td style="text-align: right;" th:text="${item.quantity}">1</td>
                            <td style="text-align: right;" th:text="${#numbers.formatInteger(item.totalAmount, 0, 'COMMA')} + ' 円'">1,000 円</td>
                            <td><a th:href="${item.productPageUrl}" th:text="${item.productPageUrl}" target="_blank">https://example.com/sample</a></td>
                        </tr>
                        <!-- 静的HTML表示用のサンプルデータ (Thymeleaf処理時は表示されない) -->
                        <tr th:if="${#lists.isEmpty(cartInfo.items) && cartInfo == null}"> <!-- この条件は実際にはThymeleaf処理時には満たされないが、静的表示の意図を示す -->
                            <td>静的サンプル商品A</td>
                            <td style="text-align: right;">1,500 円</td>
                            <td style="text-align: right;">2</td>
                            <td style="text-align: right;">3,000 円</td>
                            <td><a href="#">#</a></td>
                        </tr>
                        <tr th:if="${#lists.isEmpty(cartInfo.items) && cartInfo == null}">
                            <td>静的サンプル商品B</td>
                            <td style="text-align: right;">500 円</td>
                            <td style="text-align: right;">3</td>
                            <td style="text-align: right;">1,500 円</td>
                            <td><a href="#">#</a></td>
                        </tr>
                    </tbody>
                </table>
                <div class="total-summary">
                    合計金額: <span th:text="${#numbers.formatInteger(#aggregates.sum(cartInfo.items.![totalAmount]), 0, 'COMMA')} + ' 円'">0 円</span>
                </div>
            </div>
        </div>
    </div>
</div>

</body>
</html>


実際のWebアプリケーション開発時は、HTMLとスタイルシート(CSS)を別のファイルに分けることが多いですが、今回は割愛してこの内容のまま進めます。

Javaクラスの作成

クラスの作成(リファクタリング)

一般的に Spring Boot アプリケーションでは、データの取得処理など業務的なロジックはサービス層のクラスに記述します。例えば、今回の記事で行うように同じ情報でREST API と画面の両方を作成する際に、カート情報の取得処理がクラスに定義されていれば、複数のコントローラークラスから処理を共有できます。

前回の記事では、カートの情報を取得するメソッドはコントローラークラスに生成されていました。ここで、処理の内容が変わらないよう サービス層のクラスを作成して再構成(リファクタリング)を行い、ロジックを再利用可能とします。

クラスを作成して処理を委譲する

Gemini Code Assist のチャットで、リファクタリング対象のクラスおよびメソッドと、作成するクラスのファイルを作成する場所を指定します。

このプロジェクトのsrc/main/java/com/example/demo/service に DemoService クラスを作成して、そこにDemoController クラスの fetchUserCartData メソッドと、そこから呼び出される処理を移植してください。
このとき、DemoController の各メソッドの動作が変わらないようにしてください。


しばらく待つと、DemoService クラスの作成内容と、DemoController クラスの変更内容の2つが一緒に提案されました。


(Controllerクラスの変更内容)


それぞれの内容を確認してファイルに保存します。この時、必ず新しく作成した DemoService クラスから保存するようにご注意ください。DemoService クラスを先に生成しないと、DemoController クラスからの呼び出し位置でコンパイルエラーとなるためです。


テンプレートファイルの場合と同様に、ソースを確認して変更したい箇所があれば再度チャットで指示します。

今回提案された内容では、DemoController クラスで利用する DemoService クラスのインスタンスが、コンストラクタで注入されるようになっていました。これを Spring Boot プロジェクトらしく Autowired アノテーションを利用するように指示しました。

@DemoController.java
demoService へのインジェクションは、コンストラクタではなく 変数へのAutowired アノテーションを利用してください。


以下の通り、修正内容が提案されました。こちらも修正内容を確認して保存します。

動作確認

リファクタリングする前と内容が変わっていないか確認します。Spring Boot アプリケーションを起動して、Webブラウザで前回作成した REST API にアクセスします。


リファクタリング前(前回記事参照)と同じ内容が表示されました。これでリファクタリングは完了です。

Controller クラスの作成

テンプレートファイルと サービスクラスの作成が完了したら、画面を表示するための Controller クラスを作成します。Gemini Code Assist のチャットスペースで、作成に必要な情報と一緒に Controller クラスの作成内容を指示します。

今回は以下の内容で指示を行いました。URI、データの取得方法、表示するテンプレートなど登場人物が多いため、少し長めのプロンプトになりました。

@CartInfo.java
このクラスの情報を取得して Thymeleaf のテンプレートファイル viewcart.html に表示するコントローラークラス CartController を、src/main/java/com/example/demo/controller に作成してください。

画面を表示するURIは "/viewcart/{userId}"として、キーとなるuserIdをURLから取得してください。

表示するカートのデータは、DemoController クラスの getUserCart メソッドと同じように、DemoService クラスの fetchUserCartData メソッドを用いて取得してください。

CartController クラスを作成するのにあわせて、viewcart.html にも変更が必要な場合は、こちらも修正してください。但し、デザインは変更せずに、nullチェックなど業務的に必要な箇所のみ修正としてください。


作成する CartController クラスの内容が提案されました。


ファイルの内容についての説明を確認して保存します。今回は テンプレートファイル(viewcart.html)の修正は不要でしたが、指示の内容によっては追加で修正されることもあるでしょう。


ファイルを保存したら、Spring Boot アプリケーションを起動し、Webブラウザで「http://localhost:8080/viewcart/0001」にアクセスします。DemoService クラスを通じて JSON ファイルから取得したカートの内容が画面に表示されました。




いかがでしたでしょうか。HTMLのデザインをはじめ、Spring Boot のクラスや Thymeleaf テンプレートまで、Gemini Code Assist を利用して一通り作成することができました。

実際に使ってみると、慣れれば簡単な画面などは数分から数十分程度で形にできる可能性を感じられるのではないでしょうか。その一方で、生成されたコードが細かな仕様やプロジェクト全体の構成に適合しているかを判断するには、やはり一定の知識や経験が求められる場面もありそうです。


とはいえ、AIがコード生成のスピードを劇的に向上させてくれることは間違いないでしょう。プログラミングの学習においても、作成したコードをレビューしてもらったり、AIが提案するコードを参考にしながらアレンジを加えたりすることで、学習効率を大きく高めるツールとして活用できると思います。

生成AIを使いこなすことは、エンジニアにとっても今後ますます重要なスキルとなっていくことが予想されます。Gemini Code Assist をはじめとした生成AIを、まずは気軽に体験してみることをお勧めします。

株式会社GSI 採用サイト

新しいこと、始めよう

あなたとともに歩を進めるWEBメディア

広告

広告