Skip to main content

コントローラからビューへの値の受け渡しとThymeleaf

この章では、Spring MVCでコントローラからビューに値を渡し、Thymeleafを使って動的にHTMLを生成する方法を学ぶ。 静的なHTMLだけではなく、リクエストごとに異なる値を画面に表示できるようになることで、動的にページを表示する仕組みを理解する。

学習のゴール

  • ModeladdAttribute を使ってコントローラからビューに値を渡せる
  • ${...} / [[...]] / th:text による動的な値の表示方法を理解する
  • th:utext の危険性(XSS脆弱性)と安全な表示方法を理解する
  • th:ifth:switch による条件分岐、th:each による繰り返し表示を実装できる
  • Javaオブジェクトのgetterを通じてフィールドを参照する仕組みを理解できる
  • th:object を使ってオブジェクト参照を省略し、テンプレートを簡潔に書ける
  • ユーティリティオブジェクト(#strings, #numbers, #dates)を使って文字列・数値・日付を処理できる
  • フラグメントとレイアウトの仕組みを使って、共通部分を再利用し保守性を高められる

Thymeleafの概要

Thymeleaf(タイムリーフ)は、Spring Bootのデフォルトビューエンジンである。 HTMLファイルに「動的な表記」を埋め込むことで、サーバ側の値をブラウザに表示する仕組みを提供する。

  • 特徴
    • 普通のHTMLとしてブラウザで開ける(デザインと開発を分けやすい)
    • ${...} を使ってサーバから渡された値を埋め込める
    • if文やfor文のような制御も可能

サンプル:変数をHTMLに埋め込む

<p>こんにちは、[[${name}]] さん!</p>
  • ${name} はコントローラから渡された変数を参照している

コントローラからどのように値を渡すのかを次で学ぶ


Modelと値の受け渡し

コントローラからビューへ値を渡すには、Model に値を追加する。

Model と「MVC層のModel」は別物

ここで扱う Model は「コントローラからビューに値を渡すための入れ物」を指す。 MVCの「Model層(ドメインモデルや業務ロジックを担う部分)」とは別物であることに注意。

@Controller
public class HelloController {
@GetMapping("/hello")
public String hello(Model model) { // Modelを引数に受け取ることでビューに値を渡せる
model.addAttribute("name", "太郎");
return "hello"; // templates/hello.html を返す
}
}

ビュー(hello.html)では ${name} を使って表示できる。

<p>こんにちは、[[${name}]] さん!</p>
  • model.addAttribute("name", "太郎")${name} が "太郎" に置き換わる
  • ${} 内の変数名と addAttribute の第1引数が対応している

th:text と [[...]] の違い

Thymeleafには値を表示する方法が複数ある。

<p th:text="${msg}">dummy</p>

これは属性で値を出力する方法。公式の書き方に忠実。

もう一つはインライン式 [[...]] を使う方法。こちらの方がシンプルで推奨。

<p>[[${msg}]]</p>
<p>ようこそ [[${msg}]] さん!</p>
  • th:text … 属性に書く。プレースホルダの dummy が置き換わる
  • [[...]] … 本文に直接埋め込めるため、直感的でシンプル

th:utext と XSS脆弱性

th:utext を使うと、値が「HTMLとして解釈」されて埋め込まれる。 そのため、タグがそのまま展開される。

model.addAttribute("msg", "<b>太郎</b>");
<p th:utext="${msg}"></p>

<p><b>太郎</b></p>

→ 結果、太字で 太郎 と表示される。

しかし、もしユーザ入力をそのまま th:utext で表示すると、次のように 悪意のあるJavaScriptが埋め込まれる危険 がある。

model.addAttribute("msg", "<script>alert('XSS');</script>");
<p th:utext="${msg}"></p>

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

→ ページを開いたときに alert が実行される。これが XSS(クロスサイトスクリプティング)攻撃

th:utext は原則使わない

th:utext はユーザー入力を含む値に使うと XSS攻撃の原因 になる。

  • 通常は th:text[[...]] を使う
    • 値は自動的にHTMLエスケープ(サニタイズ)され、タグとして解釈されない
    • 例: <b>太郎</b>&lt;b&gt;太郎&lt;/b&gt; と変換され、ただの文字列として表示される

th:utext を使うのは、信頼できる静的なHTMLを埋め込みたい特別な場合に限る。


条件分岐

th:if

条件が成立した場合のみ、その要素を表示する。

<p th:if="${age >= 20}">あなたは成人です。</p>
<p th:if="${age < 20}">あなたは未成年です。</p>
  • age が 20以上なら「成人です。」が表示される
  • age が 20未満なら「未成年です。」が表示される

th:switch

Javaの switch 文のように、値に応じて分岐処理を行える。

<div th:switch="${role}">
<p th:case="'admin'">管理者です</p>
<p th:case="'user'">一般ユーザーです</p>
<p th:case="*">その他</p>
</div>
  • role が "admin" なら「管理者です」が表示される
  • role が "user" なら「一般ユーザーです」が表示される
  • どちらでもなければ th:case="*" が選ばれ「その他」が表示される

繰り返し(th:each

リストや配列を繰り返し表示するには th:each を使う。

<ul>
<li th:each="user : ${users}" th:text="${user}">dummy</li>
</ul>

users が ["Taro", "Hanako"] の場合 → 出力結果は次のようになる。

<ul>
<li>Taro</li>
<li>Hanako</li>
</ul>

ループの状態を表すステータスも取得できる。

<li th:each="user, stat : ${users}">
[[${stat.index}]]: [[${user}]]
</li>
  • stat.index : 0から始まるインデックス
  • stat.count : 1から始まるカウント

users が ["Taro", "Hanako"] の場合、出力結果は次のようになる。

<li>0: Taro</li>
<li>1: Hanako</li>

オブジェクトとフィールドアクセス

Thymeleafでは、Javaオブジェクトの getterメソッド を通じて値にアクセスできる。

public class User {
// フィールドは private で隠蔽
private String name;
private int age;

//getter
public String getName() { return name; }
public int getAge() { return age; }

//setter
public void setName(String name) { this.name = name; }
public void setAge(int age) { this.age = age; }
}

コントローラで model.addAttribute("user", user); と渡した場合、ビューでは次のように書ける。

  • user.namegetName() が呼び出される
  • user.agegetAge() が呼び出される

つまり、user.フィールド名 の形で、getterを経由して値を参照できる。

<p>[[${user.name}]] ([[${user.age}]])</p>

th:object を使ったオブジェクト名の省略

th:object を使うと、そのブロック内で指定したオブジェクトを「基準」として扱える。

<div th:object="${user}">
<p>[[*{name}]] ([[*{age}]])</p>
</div>
  • th:object="${user}" で、このブロック内の参照対象を user に設定
  • *{name}user.getName() を呼び出す
  • *{age}user.getAge() を呼び出す
th:object でテンプレートをシンプルに

user.name と毎回書かずに、*{フィールド名} の形で簡潔にアクセスできる。 フィールドが多いオブジェクトを扱う場合に特に効果的である。


ユーティリティオブジェクト

Thymeleafには便利なユーティリティが組み込まれており、# から始まる名前で呼び出せる。 文字列操作・数値フォーマット・日付処理などを簡単に行える。

文字列(#strings)

<p>[[${#strings.toUpperCase('spring')}]]</p>
  • 文字列を大文字に変換する
  • 出力例: <p>SPRING</p>
<p th:if="${#strings.isEmpty(msg)}">空文字です</p>
  • 文字列が空かどうかを判定する
  • msg が空文字なら <p>空文字です</p> が表示される
<p>[[${#strings.length('abc')}]]</p>
  • 文字列の長さを返す
  • 出力例: <p>3</p>

数値(#numbers)

<p>[[${#numbers.formatDecimal(12345.678, 1, 'POINT', 2, 'COMMA')}]]</p>
  • 数値をフォーマットする
  • 第2引数: 整数部の最小桁数
  • 第3引数: 小数点の区切り文字
  • 第4引数: 小数部の桁数
  • 第5引数: 整数部の区切り文字
  • 出力例: <p>12,345.68</p>

日付(#dates)

<p>[[${#dates.createNow()}]]</p>
  • 現在時刻を返す
  • 出力例: <p>2025-10-04T10:15:30.123+09:00</p> のようなISO形式
<p>[[${#dates.format(today, 'yyyy/MM/dd')}]]</p>
  • 日付を指定フォーマットで表示する
  • today に 2000-01-01 が渡された場合 → <p>2000/01/01</p>

staticリソースの参照

CSSやJavaScript、画像などの静的リソースを参照するには @{...} を使う。

<link rel="stylesheet" th:href="@{/css/styles.css}" />
<script th:src="@{/js/scripts.js}"></script>
<img th:src="@{/images/logo.png}" alt="Logo" />

staticディレクトリとは

Spring Bootでは、静的リソースは 必ず 以下の場所に配置する。

src/main/resources/static

@{/css/styles.css} と書くと、src/main/resources/static/css/styles.css を参照する。 コントローラを経由せずにブラウザから直接アクセス可能なファイルを置く場所である。


フラグメントとレイアウト

複数のHTMLファイルで同じヘッダーやフッターを繰り返し書いていると、修正時に管理が大変になる。 Thymeleafの「フラグメント」や「レイアウト」を使うと、共通部分を切り出して再利用できる。

  • フラグメント : 部品ごと(ヘッダーやフッター単位)に共通化したいときに便利
  • レイアウトのベース化 : ページ全体の枠組み(共通デザイン)を統一したいときに便利

👉 フラグメントは「部品の再利用」、レイアウトは「ページ全体の統一」という違いがある。

重複したデザインの例

<!-- page1.html -->
<html>
<body>
<header>共通ヘッダー</header>
<main>ページ1の内容</main>
<footer>共通フッター</footer>
</body>
</html>
<!-- page2.html -->
<html>
<body>
<header>共通ヘッダー</header>
<main>ページ2の内容</main>
<footer>共通フッター</footer>
</body>
</html>

上記のように <header><footer> が重複してしまっている。 これをフラグメントやレイアウトで共通化する。

フラグメントの定義と利用

定義:

<!-- templates/fragments/header.html -->
<div th:fragment="siteHeader">
共通ヘッダー
</div>
<!-- templates/fragments/footer.html -->
<div th:fragment="siteFooter">
共通フッター
</div>

利用:

<!-- templates/page.html -->
<html>
<body>
<div th:replace="fragments/header :: siteHeader"></div>
<main>ページごとの内容</main>
<div th:replace="fragments/footer :: siteFooter"></div>
</body>
</html>
  • th:fragment="siteHeader" : フラグメントとして名前を付ける
  • th:replace="fragments/header :: siteHeader" : そのフラグメントを呼び出す

👉 フラグメントは「ヘッダーやフッターなどの共通パーツを複数のページで再利用したいとき」に使う。

レイアウトのベース化

定義:

<!-- templates/layout/base.html -->
<html>
<body>
<header>共通ヘッダー</header>
<main th:replace="${content}"></main>
<footer>共通フッター</footer>
</body>
</html>

利用:

<!-- templates/page.html -->
<div th:replace="layout/base :: layout (~{::main})">
<main>ページごとのコンテンツ</main>
</div>
  • ベースレイアウトを1つ作り、<main> の部分だけ各ページで差し替える
  • これにより「共通部分の保守性」が大きく向上する

👉 レイアウトは「サイト全体の枠組みを共通化したいとき」に使う。

まとめ

  • フラグメント : 部分的な共通化(ヘッダー・フッター・メニューなど)に適している
  • レイアウトのベース化 : ページ全体の統一デザインに適している

両方を組み合わせて使うと、保守性の高いビュー構造を作れる。 例えば、レイアウトの中でヘッダー・フッターをフラグメントとして呼び出すことも可能。


よくある質問

Q. [[${name}]]th:text="${name}" はどちらを使えばよいですか?

A. どちらでもよいが、テキストの中にJavaの変数を自然に埋め込む場合は [[...]] の方がシンプルで読みやすい。 th:text はHTMLタグの属性として書くため、タグ内のテキスト全体を置き換えたい場合に向いている。


Q. [[${name}]] でnullを表示したらどうなりますか?

A. null の場合は何も表示されない(空文字として扱われる)。 null をデフォルト値に置き換えたい場合は Thymeleafのエルビス演算子が使える。

[[${name} ?: '名無し']]

Q. th:each でインデックスを1始まりで表示するにはどうすればよいですか?

A. stat.count を使う。

<li th:each="item, stat : ${items}">
[[${stat.count}]]: [[${item}]]
</li>

stat.index は0始まり、stat.count は1始まりである。


Q. Thymeleafテンプレートのファイルはどこに置きますか?

A. src/main/resources/templates/ 配下に置く。 コントローラで return "hello" と書くと、templates/hello.html が自動的に解決される。 サブディレクトリを使いたい場合は return "user/detail" のように書くと templates/user/detail.html が使われる。


本章のまとめ

  • コントローラからビューへの値渡しは Model#addAttribute を使う
  • ${...} 内の変数はコントローラから渡された値であり、MVCのModel層とは別物
  • 値の出力には th:text[[...]] を用いる。th:utext はXSSリスクがあるため原則使わない
  • 条件分岐は th:if / th:switch、繰り返しは th:each を使える
  • th:each のループには stat.indexstat.count などのステータスが使える
  • Javaオブジェクトのフィールドは obj.field と書くとgetterを通じて参照される
  • th:object を使うとオブジェクト名を省略して *{フィールド名} でアクセスできる
  • ユーティリティオブジェクト(#strings, #numbers, #dates)で文字列・数値・日付処理が簡単にできる
  • フラグメント は部品の共通化(ヘッダーやフッター)に、レイアウトのベース化 はページ全体の共通化に適している
  • 両者を組み合わせることで、保守性が高く再利用性のあるビュー構造を作れる
  • 次章では、コントローラで受け取った入力値を 検証(バリデーション)する方法 を学ぶ

ThymeleafはSpringMVCで主に使われるテンプレートエンジンである。その説明として、最も適切なものを選べ。

正解

B. Javaサーバーサイドで動作するHTMLテンプレートエンジンで、SpringMVCとの統合をサポートしている

解説

Thymeleafはサーバーサイド(Java)で動作するHTMLテンプレートエンジンである。

特徴:

  • HTMLファイルにThymeleaf専用の属性(th:textth:eachなど)を記述する
  • 通常のHTMLとして開いても表示できる(ナチュラルテンプレーティング)
  • Spring Bootの公式テンプレートエンジンとして広く使用されている

コントローラから渡されたデータを埋め込んでHTMLを動的に生成できる。

SpringMVCにおける Model オブジェクトの役割として、最も適切なものを選べ。

正解

D. コントローラからビュー(テンプレート)にデータを渡すためのオブジェクト

解説

Model オブジェクトはコントローラからThymeleafなどのビューにデータを渡すための入れ物である。

使い方:

@GetMapping("/greeting")
public String greeting(Model model) {
    model.addAttribute("name", "山田太郎");
    model.addAttribute("age", 25);
    return "greeting";
}

ThymeleafのHTMLで ${name} のように参照できる。

ModelAndView を使う方法もあるが、Model を引数として受け取る方法が一般的である。

Thymeleafの th:textth:utext の違いとして、正しいものを選べ。

正解

A. `th:text`はHTMLエスケープして安全に表示し、`th:utext`はエスケープせずHTMLとして解釈して表示する

解説

th:textth:utext の違いはHTMLエスケープの有無である。

  • th:text: 値をHTMLエスケープして表示(安全)
    • <script>&lt;script&gt; として表示される
    • XSS攻撃を防ぐことができる
  • th:utext: エスケープせずHTMLとして解釈(要注意)
    • <b>太字</b> → 実際に 太字 として表示される
    • ユーザー入力をそのまま使うとXSS危険

基本的には th:text を使い、HTMLを含む信頼できるデータにのみ th:utext を使う。

Thymeleafの主要な属性とその用途として、正しいものを選べ。

正解

C. `th:text`(テキスト表示)・`th:if`(条件分岐)・`th:each`(繰り返し)

解説

Thymeleafの主要な属性:

属性用途
th:textテキストコンテンツの表示(HTMLエスケープあり)
th:if条件が真のときのみ要素を表示
th:eachリストや配列を繰り返し表示
th:hrefリンクのURL指定
th:src画像のパス指定
th:valueフォームのvalue属性
th:objectフォームのバインド対象オブジェクト指定

コントローラから "message" というキーで "こんにちは!" をThymeleafのビューに渡すコードを完成させよ。

@GetMapping("/greet") public String greet(Model model) {
("message", "こんにちは!"); return "greet"; }

解答例
model.addAttribute("message", "こんにちは!");
解説

model.addAttribute("キー", 値) でModelにデータを追加する。

  • キーは文字列で、ThymeleafのHTML内で ${キー} の形式で参照できる
  • 値はどのようなオブジェクトでも渡すことができる(String、Integer、List、独自クラスなど)

複数のデータを渡す場合は、addAttribute を複数回呼び出す。

"message" というキーでモデルに格納されたデータを表示するHTMLを完成させよ。

<p
>ここにメッセージが表示されます</p>

解答例
<p th:text="${message}"></p>
解説

th:text="${変数名}" で、ModelにaddAttributeした値を要素のテキストコンテンツとして表示できる。

th:text がある場合、HTMLのタグ内のテキストは置き換えられる。

<!-- "こんにちは!" が表示される -->
<p th:text="${message}">仮テキスト</p>

静的なプレースホルダーとして 仮テキスト と書いても、実行時に動的に置換される。

Thymeleafのインライン式を使って "username" の値をテキスト中に埋め込むHTMLを完成させよ。

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

解答例
<p>こんにちは、[[${username}]]さん!</p>
解説

Thymeleafでは [[${変数名}]](インライン式)を使って、テキストの途中に値を埋め込むことができる。

<p>こんにちは、[[${username}]]さん!</p>
<!-- "こんにちは、山田太郎さん!" と表示される -->

th:text との違い:

  • th:text: 要素のテキスト全体を置き換える
  • [[...]]: テキストの一部に値を埋め込める(インライン)

どちらもHTMLエスケープが行われるため安全である。

モデルの isLoggedIntrue のときだけ「ログイン中です」を表示するHTMLを完成させよ。

<p
>ログイン中です</p>

解答例
<p th:if="${isLoggedIn}">ログイン中です</p>
解説

th:if="${条件}" は条件が true の場合のみ要素を表示する。

条件が false の場合はその要素がHTMLから取り除かれる(コメントアウトではなく完全に削除)。

反対の条件には th:unless="${条件}" を使う(条件が false のときに表示)。

<p th:if="${isLoggedIn}">ログイン中です</p>
<p th:unless="${isLoggedIn}">ログインしていません</p>

itemsItemオブジェクトのリスト)の各要素の name を繰り返し表示するHTMLを完成させよ。

<ul> <li
th:text="${item.name}">商品名</li> </ul>

解答例
<li th:each="item : ${items}" th:text="${item.name}">商品名</li>
解説

th:each="変数 : ${リスト}" でリストの各要素を繰り返し表示できる。

Javaの拡張for文(for (Item item : items))と同様の概念である。

<ul>
  <li th:each="item : ${items}" th:text="${item.name}">商品名</li>
</ul>

ループのインデックスなども取得できる:

<li th:each="item, stat : ${items}">
  [[${stat.index}]]: [[${item.name}]]
</li>

user オブジェクトの name フィールドを th:object を使って表示するHTMLを完成させよ。

<div
> <p th:text="
">名前</p> </div>

解答例
<div th:object="${user}"><p th:text="*{name}">名前</p></div>
解説

th:object="${オブジェクト}" を使うと、その要素の子要素内で *{フィールド名} という短縮記法でフィールドにアクセスできる。

<!-- th:objectなし -->
<p th:text="${user.name}">名前</p>

<!-- th:objectあり -->
<div th:object="${user}">
  <p th:text="*{name}">名前</p>
  <p th:text="*{email}">メール</p>
</div>

フォームのバインディングにもよく使われる(th:field="*{name}")。

Thymeleafの th:utext を使う際の注意点として、最も適切なものを選べ。

正解

B. `th:utext`はHTMLエスケープを行わないため、ユーザーが入力した値をそのまま使うとXSSの脆弱性が生じる

解説

th:utext は値をHTMLとして解釈するため、ユーザーが入力した値をそのまま使うとクロスサイトスクリプティング(XSS)攻撃が可能になる。

XSSの危険例:

<!-- ユーザーが "<script>alert('XSS')</script>" と入力した場合 -->
<p th:utext="${userInput}"><!-- スクリプトが実行される! -->

安全な使い方:

  • 基本的には th:text を使う
  • th:utext を使う場合は、表示するデータが管理者など信頼できるソースからのものであることを確認する

name を大文字に変換して表示するHTMLを完成させよ(#strings.toUpperCase() を使う)。

<p th:text="${
}">NAME</p>

解答例
<p th:text="${#strings.toUpperCase(name)}">NAME</p>
解説

Thymeleafには便利なユーティリティオブジェクトが用意されている。

主なユーティリティ:

  • #strings: 文字列操作(toUpperCasetoLowerCasecontainsisEmptyなど)
  • #dates: 日付操作(formatyearmonthなど)
  • #numbers: 数値フォーマット(formatDecimalなど)
  • #lists: リスト操作(sizecontainsなど)

${...} の中で #ユーティリティ名.メソッド名(引数) の形式で使用する。

fragments/header.html にヘッダーのフラグメントを定義し、別のページからそれを使うコードを完成させよ。

フラグメント定義(fragments/header.html)

<header
> <h1>サイトタイトル</h1> </header>

フラグメントの使用(index.html)

<div
></div>

解答例
<header th:fragment="header">...</header> <div th:replace="~{fragments/header :: header}"></div>
解説

Thymeleafのフラグメント機能を使うと、ヘッダーやフッターなどの共通部品を別ファイルに定義して再利用できる。

フラグメントの定義fragments/header.html):

<header th:fragment="header">
  <h1>サイトタイトル</h1>
</header>

フラグメントの使用

<!-- th:replace: 要素ごと置き換える -->
<div th:replace="~{fragments/header :: header}"></div>

<!-- th:insert: 要素の中に挿入する -->
<div th:insert="~{fragments/header :: header}"></div>