この章の内容は後で参照できれば大丈夫で覚える必要はありません。

前回は「セッション管理」について学びました。セッションにより複数のHTMLページでデータを保持することができ、ログインや買い物かごの機能が実現できました。

今回はThymeleafを使いデータをHTMLテンプレートで動的に表示する方法を学びましょう。

今回の学習の重点をオレンジの枠線で図示します。

Thymeleafを学び、データをHTMLテンプレートで動的に表示する

1. なぜThymeleaf(テンプレートエンジン)が必要なのか?

Thymeleafを使うと、HTMLファイルの中にStandard Expression(標準式)や制御構文を書くことで、Javaのオブジェクトの値を動的に埋め込んだり、条件分岐や繰り返し表示ができます。

なぜThymeleaf?

  1. デザイナーとの協業がしやすい:HTMLファイルをそのままブラウザでプレビューしやすい
  2. Spring Bootとの相性が良い

2. Thymeleafの基本

ThymeleafではHTMLタグ属性に対してth:*を使います。

2.1. 変数展開の基本

HTMLの中に以下のように記述します。

<p th:text="${msg}">ここにメッセージが入ります</p>

${msg} → コントローラからmodel.addAttribute("msg", "Hello") された文字列などを埋め込みます。

Controller側

package com.example.demo.controller;

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

@Controller
public class MainController{
	@GetMapping("/")
	public String sample(Model model) {
		model.addAttribute("msg", "Hello World");
	
		return "sample";
	}
}

sample.html(抜粋)

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"></head>
<body>
  <p th:text="${msg}">ここにメッセージ</p>
</body>
</html>

変数展開のイメージ

これで / にアクセスすると <p>Hello World</p> が表示されます。

"ここにメッセージ" は、通常 表示されない 文字列です。なぜなら、Thymeleafの th:text 属性は、指定された変数 ${msg} の値でタグの内容を置き換えるからです。つまり、msg に値がある場合は、<p> の中身が msg の値に置き換えられ、元の "ここにメッセージ" は消えてしまいます。

文字列はシングルクートで囲む

変数と文字列を組み合わせる場合

変数と文字列を組み合わせて表示したい場合、文字列部分は シングルクォートで囲む 必要があります。

例:変数と文字列の結合

<p th:text="${name} + 'さん、こんにちは!'"></p>

この場合、name の値が "田中" だったとすると、以下のHTMLに変換されます:

<p>田中さん、こんにちは!</p>

ダブルクォートを使うとエラーになる

th:text はHTMLの属性なので、通常は ダブルクォート(")で囲まれた属性値 の中に記述します。そのため、文字列の開始・終了にダブルクォートを使うと、HTMLの属性と衝突する ため、エラーが発生します。

間違った例(エラー)

<p th:text="${name} + "さん、こんにちは!""></p>  <!-- NG -->

このようにダブルクォートを使うと、th:text の属性値が "Hello, で終了したと解釈され、正しく動作しません。

調べてみましょう

Tymeleafでは[[${msg}]]記法を使うこともできます。

<p>[[${msg}]]</p>

この書き方では、[[${msg}]]の位置に${msg}の値が挿入されます。

HTMLエスケープ処理が行われ、安全に変数を表示できます。

sample.html(抜粋)

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
</head>
<body>
  <p>[[${msg}]]</p>
</body>
</html>

この場合も / にアクセスすると、以下のように表示されます。

<p>Hello World</p>

このように、th:text属性と[[${msg}]]は同じ結果になりますが、[[${msg}]]記法の場合は、HTMLタグの中のプレースホルダーの位置に変数の値を展開する形になります。

th:text → タグ内の内容を丸ごと置換する

[[${msg}]] → タグ内の指定位置に挿入する

本研修では、th:text を優先して使っていきます。

実験

以下は、Thymeleafテンプレートで悪意あるJavaScriptが挿入される例(XSS攻撃の例)です。

th:utext は、「unescaped text」(エスケープされていないテキスト)を意味するものです。HTMLをそのまま出力したいときに使います。以下を実験してみましょう。

Controller側の例(安全ではない文字列を渡す場合)
@GetMapping("/")
public String sample(Model model) {
    model.addAttribute("msg", "<script>alert('攻撃!');</script>");
    return "sample";
}
安全でない書き方(HTMLタグをそのまま出力する場合)
th:utext属性を使用する場合(危険)
<p th:utext="${msg}">ここにメッセージ</p>

結果(ブラウザのレンダリング)

<p><script>alert('攻撃!');</script></p>

このコードがブラウザで実行されると、JavaScriptのダイアログが表示され、攻撃が成立してしまいます。

安全な書き方(自動エスケープで安全に表示される)
th:text属性を使用した場合(安全)
<p th:text="${msg}">ここにメッセージ</p>

結果(エスケープされて安全)

<p>&lt;script&gt;alert(&#39;攻撃!&#39;);&lt;/script&gt;</p>


HTMLタグとして認識されず、ブラウザでそのままテキストとして表示されます。。

2.4 ==で文字列の比較ができる

Thymeleafの式内で、文字列を比較する場合は、JavaSEとは異なり、==で比較可能です。なお文字列リテラルは '(シングルクォーテーション)で囲む必要があります。

※ここから先はThymeleafと対応するHTMLを上下に並べて配置しますので見比べて下さい。

 <p th:text="${msg == 'Hello World' }">メッセージがHello Worldかどうか?</p>
<p>true</p> <!--msgがHello Worldの場合 -->

ポイント:

  1. Thymeleafでは内部で equals() による比較を自動で行うため、Javaのように equals()を呼ぶ必要はありません。
  2. ただし、equals()を明示的に呼ぶことも可能です。
<p th:text="${msg.equals('Hello World') }">メッセージがHello Worldかどうか?</p>
<p>true</p> <!--msgがHello Worldの場合 -->

2.5 数値の比較 (==)

数値についても同様に比較可能です。

<p th:text="${age == 20 }">ageが20かどうか?</p>
<p>false</p> <!--ageが19の場合 -->

数値の場合、以下のような大小比較も簡単に行えます。

<p th:text="${age <= 20 }">ageが20以上かどうか?</p>
<p>true</p> <!--ageが19の場合 -->

3. Thymeleafでの条件分岐・繰り返し

3.1 条件分岐

th:if, th:unless

例えばVIPユーザーと一般ユーザーで表示画面を変えたいような場合には、th:ifth:unless を使うのが最も簡単です。

th:if は条件が「真(true)」のときに要素を表示し、th:unless は条件が「偽(false)」のときに要素を表示します。どちらもテンプレートの中で条件分岐を簡潔に記述できるため、ユーザーの権限や状態によって表示を切り替えたい場面で非常に便利です。

また、th:if と th:unless を併用することで、HTMLの構造を分かりやすく保ちながら柔軟な表示制御が可能となります。たとえば、ログインしているかどうかを判定して、ログイン中のメッセージとログイン前の案内を切り替えるといったケースにも応用できます。

ちなみに英語の【unless 】には~しない限りという意味があります。例えば、「unless it rains」で雨が降らない限りという意味です。

<p th:if="${age >= 20}">お酒が飲めます</p>
<p th:unless="${age >= 20}">まだ飲めません</p>
<p>まだ飲めません</p> <!--ageが19の場合 -->

  1. th:if="${条件式}" → 条件式がtrueならタグを表示、falseなら非表示
  2. th:unless="${条件式}" → th:ifの逆で、条件式がfalseなら表示、trueなら非表示

参考(JavaSE)

int age = 19; // 任意の年齢を設定

if (age >= 20) {
    System.out.println("お酒が飲めます");
} else {
    System.out.println("まだ飲めません");
}

3.2 null の扱いについて

変数がnullかどうかは、Thymeleafでは以下のように記述します。

nullの場合を判定

<p th:if="${user == null}">ユーザーが存在しません。</p>
<p>ユーザーが存在しません。</p> <!--userがnullの場合 -->

空文字の扱いについて

文字列が空文字かどうかの判定もよく行います。

<p th:if="${name == ''}">名前が入力されていません</p>
<p>名前が入力されていません</p> <!--nameが空文字の場合 -->

また、空文字やnullをまとめて判定するには以下の書き方が便利です。

<!-- 文字列がnullまたは空文字の場合 -->
<p th:if="${#strings.isEmpty(name)}">名前が未入力です</p>

#stringsはThymeleafが提供する便利なユーティリティオブジェクトです。

3.3 プロパティアクセスのNullPointerException対策

Thymeleafテンプレートでオブジェクトのプロパティにアクセスする際、対象のオブジェクトがnullの場合にはNullPointerExceptionに似たエラーが発生することがあります。たとえば、user.nameにアクセスしようとして、user自体がnullの場合、テンプレート処理中に例外が発生し、画面の描画に失敗してしまう可能性があります。これを防ぐためには、プロパティにアクセスする前に、そのオブジェクトがnullでないことを確認する対策が必要です。

th:ifやth:unlessを使って、条件付きで要素の表示を制御できます。たとえば、次のように記述することで、安全にuser.nameを扱うことができます。

<p th:if="${user != null}" th:text="${user.name}">名前</p>

このようにすることで、userがnullでない場合にのみuser.nameが評価されるため、テンプレートエンジンによる評価エラーを回避できます。

また、テンプレート内で複数のプロパティアクセスが必要な場合、三項演算子を使って簡潔に条件分岐を書くこともできます。

<p th:text="${user != null ? user.name : '未設定'}"></p>

このようにすることで、userがnullなら「未設定」と表示し、安全かつ柔軟なテンプレート記述が可能になります。

3.4 th:switch / th:case

JavaSEのswitchと同じことをするには、Thymeleafの th:switch / th:case を使います。

<div th:switch="${bloodType}">
  <p th:case="'A'">慎重な性格</p>
  <p th:case="'B'">大胆な性格</p>
  <p th:case="'O'">おおらかな性格</p>
  <p th:case="*">不思議な性格</p> <!-- default相当 -->
</div>
<div>
		
		
		
    <p>不思議な性格</p> <!-- default相当 -->
</div>

  • th:switch="${変数}"
  • th:case="'A'" → Aに一致する場合のみ表示
  • th:case="*" → マッチしない場合(デフォルト)

参考(JavaSE)

String bloodType = "A"; // 任意の血液型を設定

switch (bloodType) {
    case "A":
        System.out.println("慎重な性格");
        break;
    case "B":
        System.out.println("大胆な性格");
        break;
    case "O":
        System.out.println("おおらかな性格");
        break;
    default:
        System.out.println("不思議な性格");
        break;
}

3.5 繰り返し

データベースで取得した商品情報や顧客情報をWebページに一覧表示したいときに便利なのが th:each 属性です。これは JavaSEの拡張for文(for-each文) に相当し、リストや配列などのコレクションを1件ずつ順に取り出して表示する際に使われます。

<ul>
  <li th:each="name : ${names}" th:text="${name}">名前</li>
</ul>
<ul>
	<li>imai</li>
	<li>matsuda</li>
	<li>tabuchi</li>
	<li>hashi</li>
</ul>
<!-- namesが4人の名前の場合 -->

  1. ${names} は、Controller側で List<String> などをodel.addAttribute("names", namesList) しておく。
  2. name : ${names} → 変数nameにリストの要素が順番に入る
  3. th:text="${name}" → 文字列を表示

例)オブジェクトのリストをテーブル表示

<table>
  <thead>
    <tr><th>ID</th><th>名前</th><th>価格</th></tr>
  </thead>
  <tbody>
    <tr th:each="car : ${cars}">
      <td th:text="${car.id}"></td>
      <td th:text="${car.name}"></td>
      <td th:text="${car.price}"></td>
    </tr>
  </tbody>
</table>

このように、th:each はThymeleafでリストデータを表形式やカード形式などで表示したいときに非常に便利な属性です。リスト表示の基本として、フォーム連携やテーブル更新処理とも相性が良く、Webアプリケーション開発における重要な機能の1つです。

参考(JavaSE)

  for (Car car : cars) {
     System.out.printf(car.getId(), car.getName(), car.getPrice());
  }

3.6 th:eachとth:ifの組み合わせ

例えば、名前が「tabuchi」の場合のみ表示する、というケースを考えてみます。

<ul>
    <li th:each="name : ${names}" th:if="${name == 'tabuchi'}">
        [[${name}]]
    </li>
</ul>
<ul>
    <li>tabuchi</li>
</ul>
<!-- namesが4人の名前の場合 -->

参考(JavaSE)

for (String name : names) {
     if ("tabuchi".equals(name)) {
           System.out.println(name);
     }
}

5. CSS・JavaScriptに変数の値を渡す

5.1 CSSに変数を渡す

CSSに変数を渡す最もシンプルな方法はThymeleafで以下のようにHTMLの属性として「th:style=」を設定する方法です。

<body th:style="'background-color: ' + ${bgColor}">

以前フォームのところで学んだ天気予報プログラムのプロトタイプを例にCSSに変数の値を渡す例を説明します。天気によって背景色を変えたいとします。晴れなら"yellow"、雨なら"blue"、曇りなら"gray"とします。

背景色が変わる天気予報プログラム

入力フォーム

    <a th:href="@{/weather(weather='晴れ')}">晴れ</a>
    <a th:href="@{/weather(weather='曇り')}">曇り</a>
    <a th:href="@{/weather(weather='雨')}">雨</a>

Controller

package com.example.demo.controller;

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

@Controller
public class WeatherController {

	@GetMapping("/weather")
	public String weather(String weather, Model model) {
		// 天気に応じて背景色を設定
		String backgroundColor;
		switch (weather) {
		case "曇り":
			backgroundColor = "gray";
			break;
		case "雨":
			backgroundColor = "blue";
			break;
		default: // 晴れ
			backgroundColor = "yellow";
			break;
		}

		// Modelに値をセット
		model.addAttribute("weather", weather);
		model.addAttribute("bgColor", backgroundColor);

		return "weather";
	}

	@GetMapping("/link")
	public String showLink() {
		return "link";
	}
}

HTML内で変数を埋め込む

<body th:style="'background-color: ' + ${bgColor}">
  今は <span th:text="${weather}"></span> なんですね。
</body>

5.2 JavaScriptに変数を渡す

JavaScriptのconfirm()メソッドを使ってログアウトの確認をする例で説明します。

confirm()メソッドを使ってログアウトの確認をする

Controller

    @GetMapping("/home")
    public String home(Model model) {
        String username = "yusei"; // ここでは仮のユーザー名
        model.addAttribute("username", username);
        return "home"; // home.html を表示
    }

    @GetMapping("/logout")
    public String logout() {
        return "redirect:/home"; // ログアウト後、ホームへリダイレクト
    }

サーバーから渡された値をJavaScriptで利用する(JavaScriptが分かる人向け)

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

<head>
	<meta charset="UTF-8">
	<title>Home</title>
</head>

<body>
	<!-- 画面上にユーザー名を表示。未ログイン(nullまたは空文字)の場合は「ゲスト」と表示する(三項演算子) -->
	<h1>ようこそ、<span th:text="${#strings.isEmpty(username) ? 'ゲスト' : username}" id ="username"></span> さん!</h1>

	<!-- ユーザーがクリックしたときに確認ダイアログを出すためのログアウトリンク -->
	<a href="#" onclick="confirmLogout()">ログアウト</a>

	<!-- JavaScript セクションでサーバー側の変数をクライアント側でも使えるようにする -->
	<script>
		// サーバーから渡されたusernameの値をidを手掛かりに特定して、その要素のテキストを変数に代入
		let username = document.getElementById('username').innerText;

		// ログアウト時に確認ダイアログを表示。ユーザー名を表示して確認を促す。
		function confirmLogout() {
			//confirm()はユーザーが「OK」を押した場合 → true を返す.
			//ユーザーが「キャンセル」を押した場合 → false を返す
			let isConfirmed = confirm(username + " さん、本当にログアウトしますか?");
			if (isConfirmed) {
				// ユーザーが「OK」を押したら /logout にリダイレクトしてログアウト処理へ進む
				window.location.href = "/logout";
			}
		}
	</script>

</body>

</html>


6.Thymeleaf を使った共通レイアウトの作成

Webアプリケーションを開発する際、各ページで同じヘッダーやフッターなどの共通パーツを毎回記述するのは非効率です。複数のWebページを統一したレイアウトにしたい場合、th:fragment を使うことで、ヘッダーやフッターなどの共通部分を別のテンプレートとして定義し、複数のページで再利用できます。

以下に、main.html(メインレイアウト)、header.html(ヘッダー)、footer.html(フッター)の3つのファイルを用いた実装方法を説明します。

ヘッダーやフッターなどの共通部分を別のテンプレートとして定義

※サンプルは「http://localhost:8080/replace」で実行できます。
※メニューの「会社概要」や「お問い合わせ」のリンク先は未作成です。

コントローラー

package com.example.demo.controller;

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

@Controller
public class ReplaceController {
	@GetMapping("/replace")
	public String sample() {
		// 今回はtemplatesフォルダに階層構造を持たせた例です。
		// Webフォルダを作成し、その中にmain.htmlを作りました。
		return "web/main";
	}
}

templatesフォルダがルートなので"web/main"でmain.htmlを指定しています。

ヘッダーファイル

<!-- src/main/resources/templates/web/header.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
    <header th:fragment="header">
        <h1>サイトのタイトル</h1>
        <nav>
            <ul>
                <li><a th:href="@{/}">ホーム</a></li>
                <li><a th:href="@{/about}">会社概要</a></li>
                <li><a th:href="@{/contact}">お問い合わせ</a></li>
            </ul>
        </nav>
    </header>
</body>
</html>

th:fragment="header" で、この <header> を再利用可能な部品(フラグメント)として定義しています。英語の【fragment】には「断片」といった意味があります。

フッターファイル

<!-- src/main/resources/templates/web/footer.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
    <footer th:fragment="footer">
        <p>Copyright © Say consulting group, Inc All Rights Reserved.</p>
    </footer>
</body>
</html>

ヘッダーと同様にth:fragment="footer" で、フッターを再利用可能な部品として定義しています。

メインレイアウト

<!-- src/main/resources/templates/web/main.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>メインページ</title>
</head>
<body>

    <!-- ヘッダーを読み込む -->
    <div th:replace="~{web/header :: header}"></div>

    <main>
        <p>ここにメインコンテンツが入ります。</p>
    </main>

    <!-- フッターを読み込む -->
    <div th:replace="~{web/footer :: footer}"></div>

</body>
</html>

ポイントは以下の3点です。

  • web/header は src/main/resources/templates/web/header.html を意味します(templates フォルダが基準)
  • :: header は header.html 内に定義された th:fragment="header" を意味します
  • ~{...} は web/header を相対パスとして評価し、ファイルシステム上で適切に解決します

結合後のHTML

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>メインページ</title>
</head>
<body>

    <!-- ヘッダーの挿入 -->
    <header>
        <h1>サイトのタイトル</h1>
        <nav>
            <ul>
                <li><a href="/">ホーム</a></li>
                <li><a href="/about">会社概要</a></li>
                <li><a href="/contact">お問い合わせ</a></li>
            </ul>
        </nav>
    </header>

    <!-- メインコンテンツ -->
    <main>
        <p>ここにメインコンテンツが入ります。</p>
    </main>

    <!-- フッターの挿入 -->
    <footer>
        <p>Copyright © Say consulting group, Inc All Rights Reserved.</p>
    </footer>

</body>
</html>

例題

あなたがHTMLの講座で作成したWebページのheader/footerを共通部品化しなさい。

最後にここまでで紹介しきれなかったThymeleafでよく使われるth:属性と、追加可能なHTML属性との対応表を作成しました。


Thymeleafの th:* 属性対応表

th: 属性使用対象タグの例HTML属性 or 内容説明
th:value<input>value属性入力フィールドの初期値に変数をセット。
th:href<a>, <link>などhref属性リンクの遷移先URLを変数で制御。
th:src<img>, <script>などsrc属性画像やスクリプトなどのソースパスを指定。
th:styleすべてのタグstyle属性インラインCSSのスタイルを動的に設定。
th:classすべてのタグclass属性CSSクラスを動的に指定。
th:classappendすべてのタグclass属性既存のclass属性に追加。
th:checked<input type="checkbox">checked属性チェック状態を制御(trueでチェック)。
th:selected<option>selected属性プルダウンの選択状態を制御。
th:disabledフォーム要素disabled属性非活性化のON/OFFを変数で制御。
th:readonly<input>, <textarea>readonly属性読み取り専用にするかを変数で制御。

補足情報

th:で始まる属性を複数同時に使うことも可能です。例:

<a th:href="@{/home}" th:text="'ようこそ ' + ${name} + ' さん'"></a>

第6章の今回はHTMLに動きを与える方法について学びました。

第7章は「DTOとは?データの受け渡しを整理し、コードをシンプルにする」です。