0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Spring Boot入門③】Thymeleafによるビュー ― テンプレートエンジンで画面を動的に生成する

0
Last updated at Posted at 2026-04-16

株式会社Good Labでエンジニアをしている コータロー です。
日々、Java・SQL・Gitなどの技術情報や、新人エンジニア向けの学習ノウハウ、
AI活用についての情報を発信しています。

Good Labについて気になった方は、コーポレートサイトもぜひご覧ください。
コーポレートサイト

はじめに

前回(第2回)では、@Controller@RestControllerの違い、HTTPメソッドマッピング、@RequestBodyによるJSON受け取り、ResponseEntityによるレスポンス制御を学び、簡易TODO APIを構築しました。

その中で、@Controllerビュー名(テンプレート名)を返すコントローラーであると紹介しました。第3回では、この@Controllerを本格的に使い、Thymeleaf(タイムリーフ) テンプレートエンジンでHTMLを動的に生成する方法を学びます。

今回学ぶこと

  • Thymeleafの概要とJSPとの違い
  • @Controller + Model でテンプレートにデータを渡す仕組み
  • Thymeleafの基本構文(th:textth:eachth:ifth:href など)
  • フラグメントによるレイアウトの共通化
  • 実践例としてユーザー一覧画面を構築

本記事のコードはすべて第1回で作成したhello-springプロジェクト(com.example.hellospringパッケージ)上で動作します。環境構築がまだの方は第1回を先にご覧ください。spring-boot-starter-thymeleafは第1回のpom.xmlに追加済みです。


1. Thymeleafとは

JSPに代わるテンプレートエンジン

Thymeleafは、Spring Bootにおける標準のテンプレートエンジンです。Servlet/JSP入門シリーズではJSP(JavaServer Pages)を使ってHTMLを動的に生成していましたが、Spring BootではJSPの代わりにThymeleafを使います。

Spring BootがJSPよりThymeleafを推奨する主な理由は以下の通りです。

観点 JSP Thymeleaf
実行方法 Servletにコンパイルして実行 テンプレートとして直接処理
JARパッケージング 制約あり(WARが基本) JARで問題なし
ブラウザでの直接表示 不可(<%= %>等がそのまま表示される) 可能(後述の「自然テンプレート」)
Spring Bootの対応 限定的(追加設定が必要) 公式スターター提供

自然テンプレート(Natural Templates)

Thymeleafの大きな特徴は、テンプレートファイルをブラウザでそのまま開けることです。

<!-- Thymeleaf テンプレート -->
<p th:text="${message}">デフォルトメッセージ</p>
  • サーバーで処理した場合${message} の値(例:こんにちは)が表示される
  • ブラウザでHTMLファイルを直接開いた場合デフォルトメッセージ が表示される

th:text等のThymeleaf属性はHTML標準の属性ではないため、ブラウザは無視します。結果として、<p>タグの中身(デフォルトメッセージ)がそのまま表示されます。これにより、デザイナーがサーバーなしでHTMLのデザインを確認できるメリットがあります。

一方、JSPでは<%= %>やJSTLタグがそのまま表示されてしまい、ブラウザだけでデザインを確認できません。

JSPとの対比表

JSPの書き方 Thymeleafの書き方 用途
<%= value %> th:text="${value}" 値の出力
<c:forEach items="${list}" var="item"> th:each="item : ${list}" ループ
<c:if test="${condition}"> th:if="${condition}" 条件分岐
<c:choose> / <c:when> th:switch / th:case 複数条件分岐
${pageContext.request.contextPath}/path @{/path} URL生成
<jsp:include page="header.jsp"> th:replace="~{fragments :: header}" 部品の組み込み

2. はじめてのThymeleafテンプレート

Controller側: Modelにデータを詰める

まず、コントローラーを作成します。@Controller@RestControllerではない)を使い、Modelオブジェクトにデータを格納してビュー名を返します。

package com.example.hellospring;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class ThymeleafHelloController {

    @GetMapping("/thymeleaf/hello")
    public String hello(Model model) {
        model.addAttribute("name", "Spring Boot");
        model.addAttribute("year", 2026);
        return "hello";
    }
}

ポイント:

  • Model model を引数に宣言すると、Spring Bootが自動でModelオブジェクトを渡してくれる
  • model.addAttribute("キー", 値) でテンプレートに渡すデータを格納する
  • return "hello"src/main/resources/templates/hello.html を返すという意味

テンプレート側: hello.html

src/main/resources/templates/hello.html を作成します。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Hello Thymeleaf</title>
</head>
<body>
    <h1>Hello, Thymeleaf!</h1>
    <p>名前: <span th:text="${name}">デフォルト名</span></p>
    <p>年: <span th:text="${year}">2000</span></p>
</body>
</html>

ポイント:

  • xmlns:th="http://www.thymeleaf.org" はThymeleafの名前空間宣言。IDEの補完を有効にするために記述する(なくても動作する)
  • th:text="${name}"Modelに格納したnameの値を出力する
  • デフォルト名2000 はブラウザで直接開いたときに表示される静的な値

アプリケーションを起動して http://localhost:8080/thymeleaf/hello にアクセスすると、以下のHTMLが描画されます。

<h1>Hello, Thymeleaf!</h1>
<p>名前: <span>Spring Boot</span></p>
<p>年: <span>2026</span></p>

Servlet/JSPとの処理フロー比較

この仕組みは、Servlet/JSPで行っていた以下の処理と本質的に同じです。

Servlet/JSP版:

// Servlet内
request.setAttribute("name", "Spring Boot");
request.setAttribute("year", 2026);
request.getRequestDispatcher("/WEB-INF/views/hello.jsp").forward(request, response);

Spring Boot版:

// Controller内
model.addAttribute("name", "Spring Boot");
model.addAttribute("year", 2026);
return "hello"; // templates/hello.html へフォワード
Servlet/JSP Spring Boot 説明
request.setAttribute() model.addAttribute() データの格納方法
RequestDispatcher.forward() return "ビュー名" テンプレートへの転送
/WEB-INF/views/hello.jsp templates/hello.html テンプレートの配置場所
requestオブジェクトに直接格納 Modelオブジェクト経由 データの受け渡し手段

内部的には、Spring BootのModelに格納したデータは最終的にServletのHttpServletRequestの属性として設定されます。Servlet/JSPで学んだリクエスト属性の仕組みがそのまま活きています。


3. Thymeleafの基本構文

th:text ― テキスト出力(XSSエスケープ済み)

th:textは値をHTMLエスケープして出力します。XSS(クロスサイトスクリプティング)対策が自動的に行われます。

package com.example.hellospring;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class TextDemoController {

    @GetMapping("/thymeleaf/text")
    public String textDemo(Model model) {
        model.addAttribute("safe", "Hello, World!");
        model.addAttribute("dangerous", "<script>alert('XSS')</script>");
        return "text-demo";
    }
}

src/main/resources/templates/text-demo.html:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>th:text デモ</title>
</head>
<body>
    <p th:text="${safe}">安全なテキスト</p>
    <p th:text="${dangerous}">危険なテキスト</p>
</body>
</html>

描画結果:

<p>Hello, World!</p>
<p>&lt;script&gt;alert('XSS')&lt;/script&gt;</p>

<script>タグがエスケープされ、JavaScriptは実行されません。

th:utext ― 非エスケープ出力

th:utext(unescaped text)はHTMLをエスケープせずに出力します。

<p th:utext="${dangerous}">危険なテキスト</p>

描画結果:

<p><script>alert('XSS')</script></p>

th:utextはXSS攻撃のリスクがあります。ユーザーが入力した値をth:utextで出力してはいけません。管理者が管理するHTMLコンテンツ(例:CMS記事の本文)など、信頼できるデータにのみ使用してください。

th:each ― ループ

th:eachは、コレクションの要素を繰り返し処理します。JSPの<c:forEach>に相当します。

package com.example.hellospring;

import java.util.List;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class EachDemoController {

    @GetMapping("/thymeleaf/each")
    public String eachDemo(Model model) {
        List<String> fruits = List.of("りんご", "みかん", "ぶどう");
        model.addAttribute("fruits", fruits);
        return "each-demo";
    }
}

src/main/resources/templates/each-demo.html:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>th:each デモ</title>
</head>
<body>
    <h2>フルーツ一覧</h2>
    <ul>
        <li th:each="fruit : ${fruits}" th:text="${fruit}">フルーツ名</li>
    </ul>
</body>
</html>

描画結果:

<ul>
    <li>りんご</li>
    <li>みかん</li>
    <li>ぶどう</li>
</ul>

ステータス変数を使うと、インデックスや最初/最後の要素かどうかを判定できます。

<ul>
    <li th:each="fruit, stat : ${fruits}">
        <span th:text="${stat.index}">0</span>.
        <span th:text="${fruit}">フルーツ名</span>
        <span th:if="${stat.first}">(最初)</span>
        <span th:if="${stat.last}">(最後)</span>
    </li>
</ul>

描画結果:

<ul>
    <li><span>0</span>. <span>りんご</span> <span>(最初)</span></li>
    <li><span>1</span>. <span>みかん</span></li>
    <li><span>2</span>. <span>ぶどう</span> <span>(最後)</span></li>
</ul>

ステータス変数で使えるプロパティは以下の通りです。

プロパティ 説明
index int 0始まりのインデックス
count int 1始まりのカウント
size int コレクションの要素数
current Object 現在の要素
even boolean 偶数番目か(0始まり)
odd boolean 奇数番目か(0始まり)
first boolean 最初の要素か
last boolean 最後の要素か

th:if / th:unless ― 条件分岐

th:ifは条件が真のとき要素を表示します。th:unlessはその逆です。JSPの<c:if>に相当します。

<p th:if="${user.active}">アクティブユーザーです</p>
<p th:unless="${user.active}">非アクティブユーザーです</p>

th:ifが「真」と評価する値:

  • nullでない
  • boolean型のtrue
  • 0でない数値
  • 空でない文字列(ただし"false""off""no"は偽)
  • boolean、Number、Character以外のオブジェクト

th:switch / th:case ― 複数条件分岐

th:switchは複数の条件を分岐します。Javaのswitch文と同様に、一致したth:caseの内容が表示されます。th:case="*"はデフォルトケースです。

<div th:switch="${user.role}">
    <p th:case="'admin'">管理者です</p>
    <p th:case="'editor'">編集者です</p>
    <p th:case="*">一般ユーザーです</p>
</div>

th:caseで文字列リテラルを比較する場合は、シングルクォートで囲みます('admin')。数値の場合はそのまま書けます(th:case="1")。

th:href / th:src ― URL式(@{...})

Thymeleafでは、@{...}というURL式でリンクやリソースのパスを生成します。コンテキストパスが自動的に付加されるため、パスのハードコーディングを避けられます。

<!-- 静的パス -->
<a th:href="@{/thymeleaf/hello}">Hello画面へ</a>

<!-- パスパラメータ付き -->
<a th:href="@{/users/{id}(id=${user.id})}">ユーザー詳細</a>

<!-- クエリパラメータ付き -->
<a th:href="@{/search(keyword=${keyword}, page=${page})}">検索</a>

<!-- CSS / JavaScript -->
<link rel="stylesheet" th:href="@{/css/style.css}">
<script th:src="@{/js/app.js}"></script>

JSPでは${pageContext.request.contextPath}を毎回書く必要がありましたが、Thymeleafでは@{}が自動で処理します。

th:class / th:classappend ― CSSクラスの動的切り替え

th:classは既存のclass属性を上書きします。th:classappendは既存のclass属性に追記します。

<!-- th:class: クラスを上書き -->
<p th:class="${user.active} ? 'text-green' : 'text-red'">ステータス</p>

<!-- th:classappend: クラスを追記 -->
<p class="base-style" th:classappend="${user.active} ? 'active' : 'inactive'">ステータス</p>

th:classappendの場合、class="base-style"は維持されたまま、条件に応じてactiveまたはinactiveが追加されます。

描画結果(user.activetrueの場合):

<p class="base-style active">ステータス</p>

リテラル置換 |...|

文字列連結を簡潔に書くためのリテラル置換構文です。

<!-- 通常の文字列連結 -->
<p th:text="'こんにちは、' + ${name} + 'さん!'">挨拶</p>

<!-- リテラル置換(同じ結果) -->
<p th:text="|こんにちは、${name}さん!|">挨拶</p>

リテラル置換(|...|)の方が読みやすく、特に複数の変数を含む場合に便利です。


4. レイアウトの共通化

Webアプリケーションでは、ヘッダー、フッター、ナビゲーションなどの共通パーツが多くのページに存在します。Thymeleafではフラグメントの仕組みを使って共通パーツを部品化できます。

th:fragment ― 再利用可能な部品を定義する

src/main/resources/templates/fragments/common.html を作成します。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>

    <!-- ヘッダー -->
    <header th:fragment="header">
        <nav style="background-color: #333; color: white; padding: 10px;">
            <a href="/" style="color: white; margin-right: 20px;">ホーム</a>
            <a href="/thymeleaf/users" style="color: white; margin-right: 20px;">ユーザー一覧</a>
        </nav>
    </header>

    <!-- フッター -->
    <footer th:fragment="footer">
        <hr>
        <p style="text-align: center; color: gray;">&copy; 2026 Hello Spring App</p>
    </footer>

</body>
</html>

th:replace / th:insert ― 部品を組み込む

定義したフラグメントを他のテンプレートに組み込むには、th:replaceまたはth:insertを使います。

属性 動作
th:replace ホスト要素をフラグメントで置換する
th:insert ホスト要素の子要素としてフラグメントを挿入する

具体例で違いを確認します。

<!-- th:replace の場合 -->
<div th:replace="~{fragments/common :: header}">ここが置換される</div>

<!-- th:insert の場合 -->
<div th:insert="~{fragments/common :: header}">ここに挿入される</div>

描画結果:

<!-- th:replace → divごと<header>に置き換わる -->
<header>
    <nav style="background-color: #333; color: white; padding: 10px;">
        <a href="/" style="color: white; margin-right: 20px;">ホーム</a>
        <a href="/thymeleaf/users" style="color: white; margin-right: 20px;">ユーザー一覧</a>
    </nav>
</header>

<!-- th:insert → divの中に<header>が入る -->
<div>
    <header>
        <nav style="background-color: #333; color: white; padding: 10px;">
            <a href="/" style="color: white; margin-right: 20px;">ホーム</a>
            <a href="/thymeleaf/users" style="color: white; margin-right: 20px;">ユーザー一覧</a>
        </nav>
    </header>
</div>

通常はth:replaceを使います。ホスト要素(<div>)を残す必要がない場合が多いためです。

Thymeleaf 3.1以降では、フラグメント式に~{...}構文を使用します。~{}を省略した旧構文(th:replace="fragments/common :: header")は非推奨です。

レイアウトの組み立て

フラグメントを使って共通レイアウトを組み立てた例です。

src/main/resources/templates/layout-demo.html:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>レイアウトデモ</title>
</head>
<body>

    <!-- ヘッダー(フラグメントで置換) -->
    <div th:replace="~{fragments/common :: header}">ヘッダー</div>

    <!-- メインコンテンツ -->
    <main style="padding: 20px;">
        <h1>レイアウトデモページ</h1>
        <p th:text="|このページのメッセージ: ${message}|">メッセージ</p>
    </main>

    <!-- フッター(フラグメントで置換) -->
    <div th:replace="~{fragments/common :: footer}">フッター</div>

</body>
</html>

コントローラー:

package com.example.hellospring;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class LayoutDemoController {

    @GetMapping("/thymeleaf/layout")
    public String layoutDemo(Model model) {
        model.addAttribute("message", "フラグメントでレイアウトを共通化しています");
        return "layout-demo";
    }
}

Servlet/JSPとの対比

JSP Thymeleaf 説明
<jsp:include page="header.jsp" /> th:replace="~{fragments/common :: header}" 共通パーツの組み込み
<%@ include file="footer.jsp" %> th:replace="~{fragments/common :: footer}" 静的インクルード相当
別ファイル(header.jsp 同一ファイル内のth:fragmentで定義 部品の定義方法

JSPではheader.jspのようにファイル単位で分割していましたが、Thymeleafでは1つのファイルに複数のフラグメントを定義できます。もちろん、フラグメントを別ファイルに分けることも可能です。


5. 実践例: ユーザー一覧画面

ここまで学んだ構文を組み合わせて、実践的なユーザー一覧画面を構築します。

Userクラス

src/main/java/com/example/hellospring/User.java:

package com.example.hellospring;

public class User {

    private int id;
    private String name;
    private String email;
    private boolean active;

    public User(int id, String name, String email, boolean active) {
        this.id = id;
        this.name = name;
        this.email = email;
        this.active = active;
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }

    public boolean isActive() {
        return active;
    }
}

Thymeleafは${user.active}のようにプロパティアクセスする際、JavaBeans規約に従いisActive()メソッドを呼び出します。boolean型のgetterはisプレフィックスが標準です。

UserController

src/main/java/com/example/hellospring/UserController.java:

package com.example.hellospring;

import java.util.List;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class UserController {

    @GetMapping("/thymeleaf/users")
    public String userList(Model model) {
        List<User> users = List.of(
            new User(1, "田中太郎", "tanaka@example.com", true),
            new User(2, "佐藤花子", "sato@example.com", true),
            new User(3, "鈴木一郎", "suzuki@example.com", false),
            new User(4, "高橋美咲", "takahashi@example.com", true),
            new User(5, "伊藤健太", "ito@example.com", false)
        );
        model.addAttribute("users", users);
        return "users";
    }
}

第2回で作成したTodoControllerのエンドポイントは/api/todosでした。今回は/thymeleaf/usersとすることで、パスの重複を避けています。

ユーザー一覧テンプレート

src/main/resources/templates/users.html:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>ユーザー一覧</title>
    <style>
        body { font-family: sans-serif; margin: 20px; }
        table { border-collapse: collapse; width: 100%; }
        th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
        th { background-color: #333; color: white; }
        tr:nth-child(even) { background-color: #f9f9f9; }
        .active { color: green; font-weight: bold; }
        .inactive { color: red; font-weight: bold; }
        .empty-message { color: gray; font-style: italic; padding: 20px; }
    </style>
</head>
<body>

    <!-- ヘッダー -->
    <div th:replace="~{fragments/common :: header}">ヘッダー</div>

    <main style="padding: 20px;">
        <h1>ユーザー一覧</h1>

        <!-- ユーザーが存在する場合 -->
        <table th:if="${!users.isEmpty()}">
            <thead>
                <tr>
                    <th>ID</th>
                    <th>名前</th>
                    <th>メールアドレス</th>
                    <th>ステータス</th>
                </tr>
            </thead>
            <tbody>
                <tr th:each="user : ${users}">
                    <td th:text="${user.id}">1</td>
                    <td th:text="${user.name}">名前</td>
                    <td th:text="${user.email}">email@example.com</td>
                    <td th:classappend="${user.active} ? 'active' : 'inactive'"
                        th:text="${user.active} ? 'アクティブ' : '非アクティブ'">
                        ステータス
                    </td>
                </tr>
            </tbody>
        </table>

        <!-- ユーザーが存在しない場合 -->
        <p th:if="${users.isEmpty()}" class="empty-message">
            ユーザーが登録されていません。
        </p>
    </main>

    <!-- フッター -->
    <div th:replace="~{fragments/common :: footer}">フッター</div>

</body>
</html>

使用している構文の整理

この実践例で使用したThymeleaf構文を整理します。

構文 該当箇所 用途
th:replace ヘッダー・フッター フラグメントの組み込み
th:if ${!users.isEmpty()} / ${users.isEmpty()} ユーザー有無の条件分岐
th:each user : ${users} ユーザーリストのループ
th:text ${user.id}${user.name} テキスト出力
th:classappend ${user.active} ? 'active' : 'inactive' CSSクラスの動的切り替え
三項演算子 ${user.active} ? 'アクティブ' : '非アクティブ' 条件に応じた表示切り替え

アプリケーションを起動して http://localhost:8080/thymeleaf/users にアクセスすると、ユーザー一覧がテーブル形式で表示されます。アクティブなユーザーは緑色、非アクティブなユーザーは赤色で表示されます。


練習問題

問題1: 商品一覧テーブル ⭐

以下の仕様で商品一覧画面を作成してください。

仕様:

  • エンドポイント: GET /thymeleaf/products
  • Productクラスを作成(idnamepriceの3フィールド)
  • コントローラーで3つ以上の商品データをModelに格納する
  • テンプレートproducts.htmlでテーブル表示する
  • th:eachで商品をループ表示する

ヒント: UserクラスとUserControllerの構造を参考にしてください。

模範解答

src/main/java/com/example/hellospring/Product.java:

package com.example.hellospring;

public class Product {

    private int id;
    private String name;
    private int price;

    public Product(int id, String name, int price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public int getPrice() {
        return price;
    }
}

src/main/java/com/example/hellospring/ProductController.java:

package com.example.hellospring;

import java.util.List;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class ProductController {

    @GetMapping("/thymeleaf/products")
    public String productList(Model model) {
        List<Product> products = List.of(
            new Product(1, "ノートPC", 120000),
            new Product(2, "ワイヤレスマウス", 3500),
            new Product(3, "USBハブ", 2800),
            new Product(4, "モニターアーム", 15000)
        );
        model.addAttribute("products", products);
        return "products";
    }
}

src/main/resources/templates/products.html:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>商品一覧</title>
    <style>
        body { font-family: sans-serif; margin: 20px; }
        table { border-collapse: collapse; width: 100%; }
        th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
        th { background-color: #333; color: white; }
        tr:nth-child(even) { background-color: #f9f9f9; }
    </style>
</head>
<body>
    <h1>商品一覧</h1>
    <table>
        <thead>
            <tr>
                <th>ID</th>
                <th>商品名</th>
                <th>価格(円)</th>
            </tr>
        </thead>
        <tbody>
            <tr th:each="product : ${products}">
                <td th:text="${product.id}">1</td>
                <td th:text="${product.name}">商品名</td>
                <td th:text="|${product.price}円|">0円</td>
            </tr>
        </tbody>
    </table>
</body>
</html>

http://localhost:8080/thymeleaf/products にアクセスすると、4つの商品がテーブル形式で表示されます。

問題2: 在庫あり/なし表示 + CSSクラス切り替え ⭐⭐

問題1のProductクラスを拡張し、在庫数(stock)フィールドを追加してください。

仕様:

  • エンドポイント: GET /thymeleaf/products-stock
  • Productクラスにstockint型)フィールドを追加する(問題1と別のクラスでも可)
  • 在庫が0のとき「在庫なし」を赤文字で表示する(th:ifまたは三項演算子 + th:classappend
  • 在庫が1以上のとき「在庫あり(N個)」を緑文字で表示する
  • 商品がない場合は「商品が登録されていません。」と表示する(th:if
模範解答

src/main/java/com/example/hellospring/StockProduct.java:

package com.example.hellospring;

public class StockProduct {

    private int id;
    private String name;
    private int price;
    private int stock;

    public StockProduct(int id, String name, int price, int stock) {
        this.id = id;
        this.name = name;
        this.price = price;
        this.stock = stock;
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public int getPrice() {
        return price;
    }

    public int getStock() {
        return stock;
    }

    public boolean isInStock() {
        return stock > 0;
    }
}

src/main/java/com/example/hellospring/StockProductController.java:

package com.example.hellospring;

import java.util.List;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class StockProductController {

    @GetMapping("/thymeleaf/products-stock")
    public String productStockList(Model model) {
        List<StockProduct> products = List.of(
            new StockProduct(1, "ノートPC", 120000, 5),
            new StockProduct(2, "ワイヤレスマウス", 3500, 0),
            new StockProduct(3, "USBハブ", 2800, 12),
            new StockProduct(4, "モニターアーム", 15000, 0)
        );
        model.addAttribute("products", products);
        return "products-stock";
    }
}

src/main/resources/templates/products-stock.html:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>商品一覧(在庫管理)</title>
    <style>
        body { font-family: sans-serif; margin: 20px; }
        table { border-collapse: collapse; width: 100%; }
        th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
        th { background-color: #333; color: white; }
        tr:nth-child(even) { background-color: #f9f9f9; }
        .in-stock { color: green; font-weight: bold; }
        .out-of-stock { color: red; font-weight: bold; }
        .empty-message { color: gray; font-style: italic; padding: 20px; }
    </style>
</head>
<body>
    <h1>商品一覧(在庫管理)</h1>

    <table th:if="${!products.isEmpty()}">
        <thead>
            <tr>
                <th>ID</th>
                <th>商品名</th>
                <th>価格(円)</th>
                <th>在庫状況</th>
            </tr>
        </thead>
        <tbody>
            <tr th:each="product : ${products}">
                <td th:text="${product.id}">1</td>
                <td th:text="${product.name}">商品名</td>
                <td th:text="|${product.price}円|">0円</td>
                <td th:classappend="${product.inStock} ? 'in-stock' : 'out-of-stock'"
                    th:text="${product.inStock} ? |在庫あり(${product.stock}個)| : '在庫なし'">
                    在庫状況
                </td>
            </tr>
        </tbody>
    </table>

    <p th:if="${products.isEmpty()}" class="empty-message">
        商品が登録されていません。
    </p>
</body>
</html>

http://localhost:8080/thymeleaf/products-stock にアクセスすると、在庫がある商品は緑色で「在庫あり(N個)」、在庫がない商品は赤色で「在庫なし」と表示されます。

問題3: ヘッダー・フッターのフラグメント化 + 複数ページ構成 ⭐⭐⭐

問題1・問題2の画面に共通のヘッダー・フッターを適用し、ナビゲーションで画面を行き来できるようにしてください。

仕様:

  • src/main/resources/templates/fragments/shop-common.html にフラグメントを定義する
    • header フラグメント: ナビゲーションバー(「ホーム」「商品一覧」「在庫管理」へのリンク)
    • footer フラグメント: コピーライト表示
  • 問題1のproducts.htmlと問題2のproducts-stock.htmlの両方にフラグメントを適用する
  • ナビゲーションのリンクにはth:href="@{/path}"を使う
  • 現在表示中のページのリンクを太字にする(ヒント: フラグメントに引数を渡す)
模範解答

src/main/resources/templates/fragments/shop-common.html:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>

    <header th:fragment="header(currentPage)">
        <nav style="background-color: #2c3e50; padding: 10px 20px;">
            <a th:href="@{/}"
               th:style="${currentPage == 'home'} ? 'color: white; margin-right: 20px; font-weight: bold;' : 'color: #bdc3c7; margin-right: 20px;'">
                ホーム
            </a>
            <a th:href="@{/thymeleaf/products}"
               th:style="${currentPage == 'products'} ? 'color: white; margin-right: 20px; font-weight: bold;' : 'color: #bdc3c7; margin-right: 20px;'">
                商品一覧
            </a>
            <a th:href="@{/thymeleaf/products-stock}"
               th:style="${currentPage == 'stock'} ? 'color: white; font-weight: bold;' : 'color: #bdc3c7;'">
                在庫管理
            </a>
        </nav>
    </header>

    <footer th:fragment="footer">
        <hr>
        <p style="text-align: center; color: gray;">&copy; 2026 Spring Boot Shop</p>
    </footer>

</body>
</html>

問題1のproducts.htmlを修正して共通ヘッダー・フッターを適用します。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>商品一覧</title>
    <style>
        body { font-family: sans-serif; margin: 0; }
        main { padding: 20px; }
        table { border-collapse: collapse; width: 100%; }
        th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
        th { background-color: #333; color: white; }
        tr:nth-child(even) { background-color: #f9f9f9; }
    </style>
</head>
<body>
    <div th:replace="~{fragments/shop-common :: header('products')}">ヘッダー</div>

    <main>
        <h1>商品一覧</h1>
        <table>
            <thead>
                <tr>
                    <th>ID</th>
                    <th>商品名</th>
                    <th>価格(円)</th>
                </tr>
            </thead>
            <tbody>
                <tr th:each="product : ${products}">
                    <td th:text="${product.id}">1</td>
                    <td th:text="${product.name}">商品名</td>
                    <td th:text="|${product.price}円|">0円</td>
                </tr>
            </tbody>
        </table>
    </main>

    <div th:replace="~{fragments/shop-common :: footer}">フッター</div>
</body>
</html>

問題2のproducts-stock.htmlにも同様に適用します。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>商品一覧(在庫管理)</title>
    <style>
        body { font-family: sans-serif; margin: 0; }
        main { padding: 20px; }
        table { border-collapse: collapse; width: 100%; }
        th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
        th { background-color: #333; color: white; }
        tr:nth-child(even) { background-color: #f9f9f9; }
        .in-stock { color: green; font-weight: bold; }
        .out-of-stock { color: red; font-weight: bold; }
        .empty-message { color: gray; font-style: italic; padding: 20px; }
    </style>
</head>
<body>
    <div th:replace="~{fragments/shop-common :: header('stock')}">ヘッダー</div>

    <main>
        <h1>商品一覧(在庫管理)</h1>

        <table th:if="${!products.isEmpty()}">
            <thead>
                <tr>
                    <th>ID</th>
                    <th>商品名</th>
                    <th>価格(円)</th>
                    <th>在庫状況</th>
                </tr>
            </thead>
            <tbody>
                <tr th:each="product : ${products}">
                    <td th:text="${product.id}">1</td>
                    <td th:text="${product.name}">商品名</td>
                    <td th:text="|${product.price}円|">0円</td>
                    <td th:classappend="${product.inStock} ? 'in-stock' : 'out-of-stock'"
                        th:text="${product.inStock} ? |在庫あり(${product.stock}個)| : '在庫なし'">
                        在庫状況
                    </td>
                </tr>
            </tbody>
        </table>

        <p th:if="${products.isEmpty()}" class="empty-message">
            商品が登録されていません。
        </p>
    </main>

    <div th:replace="~{fragments/shop-common :: footer}">フッター</div>
</body>
</html>

ポイント:

  • フラグメントに引数を渡すことで、現在のページを識別できる(header('products')header('stock')
  • 引数を使って、現在のページのリンクスタイルを動的に変更している
  • @{/path}を使うことで、コンテキストパスが変わっても対応できる

まとめ

JSPとThymeleafの対比表(総まとめ)

機能 JSP Thymeleaf
値の出力 <%= value %> / ${value} th:text="${value}"
非エスケープ出力 <%= value %>(デフォルトで非エスケープ) th:utext="${value}"
ループ <c:forEach items="${list}" var="item"> th:each="item : ${list}"
条件分岐 <c:if test="${cond}"> th:if="${cond}"
複数条件 <c:choose> / <c:when> th:switch / th:case
URL生成 ${pageContext.request.contextPath}/path @{/path}
部品化 <jsp:include page="header.jsp"> th:fragment + th:replace
XSS対策 <c:out value="${val}"> が必要 th:textがデフォルトでエスケープ
ブラウザ直接表示 不可 可(自然テンプレート)
ファイル拡張子 .jsp .html

今回学んだこと

学んだこと キーワード
Thymeleafの概要 自然テンプレート、JSPとの違い
データの受け渡し Model.addAttribute()return "ビュー名"
テキスト出力 th:text(エスケープ)、th:utext(非エスケープ)
ループ th:each、ステータス変数
条件分岐 th:ifth:unlessth:switch / th:case
URL式 @{/path}、パスパラメータ、クエリパラメータ
CSSクラス制御 th:classth:classappend
リテラル置換 |...| 構文
レイアウト共通化 th:fragmentth:replaceth:insert

次回予告

次回(第4回)では、フォーム処理とバリデーションを扱います。th:actionth:objectを使ったフォーム送信、@ModelAttributeによるフォームデータの受け取り、Bean Validationによる入力検証を学びます。


Spring Boot入門シリーズ 全10回(予定):

  1. Servlet/JSPからの移行と環境構築
  2. コントローラとルーティング
  3. 👉 Thymeleafによるビュー(本記事)
  4. フォーム処理とバリデーション
  5. Spring Data JPA(データベース連携)
  6. RESTful API設計
  7. Spring Security(認証・認可)
  8. 例外処理とエラーハンドリング
  9. テストの書き方(JUnit + MockMvc)
  10. 総合演習:掲示板アプリをSpring Bootで再構築

参考


@kotaro_ai_lab
AI活用や開発効率化について発信しています。フォローお気軽にどうぞ!

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?