タイトル: Thymeleaf
SEOタイトル: Thymeleaf 入門: Spring Boot 標準テンプレートの記法・レイアウト・JSP との違い
| この記事の要点 |
|
Thymeleaf とは
Thymeleaf は Spring Boot 標準のテンプレートエンジン。最大の特徴は th: 属性で記述するため素の HTML としてブラウザで開けること。デザイナーがそのまま編集できるため、JSP に比べて開発フローが分業しやすい。
| 項目 | Thymeleaf | JSP |
|---|---|---|
| HTML として開ける | ◯ | × |
| Spring Boot 標準 | ◯ | △ (要設定) |
| 記法 | HTML 属性 th:* | カスタムタグ <c:if> |
| レイアウト | fragment / decorator | include / sitemesh |
| 学習コスト | 低(HTML が分かれば) | 中 |
セットアップ (Spring Boot)
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>src/main/resources/
├── templates/ ← Thymeleaf テンプレート (デフォルト)
│ ├── index.html
│ └── user/
│ ├── list.html
│ └── detail.html
└── static/ ← CSS / JS / 画像
├── css/app.css
└── js/app.js
基本記法
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title th:text="${title}">タイトル</title>
<link rel="stylesheet" th:href="@{/css/app.css}">
</head>
<body>
<!-- テキスト埋め込み -->
<h1 th:text="${user.name}">山田太郎</h1>
<!-- HTML として埋め込み (XSS 注意) -->
<div th:utext="${user.bio}">紹介文</div>
<!-- 属性値 -->
<a th:href="@{/user/{id}(id=${user.id})}">詳細</a>
<img th:src="@{/img/avatar.png}" th:alt="${user.name}">
<!-- 条件分岐 -->
<p th:if="${user.admin}">管理者です</p>
<p th:unless="${user.admin}">一般ユーザーです</p>
<!-- if / else 相当: switch -->
<div th:switch="${user.role}">
<p th:case="'ADMIN'">管理者</p>
<p th:case="'USER'">ユーザー</p>
<p th:case="*">ゲスト</p>
</div>
<!-- 繰り返し -->
<ul>
<li th:each="u : ${users}" th:text="${u.name}">名前</li>
</ul>
<!-- インデックスや状態取得 -->
<tr th:each="u, stat : ${users}"
th:class="${stat.odd}? 'odd' : 'even'">
<td th:text="${stat.count}">1</td>
<td th:text="${u.name}">名前</td>
</tr>
</body>
</html>
フォーム
<form th:action="@{/user/save}" th:object="${form}" method="post">
<!-- フィールドバインド -->
<input type="text" th:field="*{name}" />
<span th:if="${#fields.hasErrors('name')}"
th:errors="*{name}">エラー</span>
<input type="email" th:field="*{email}" />
<!-- ラジオ -->
<input type="radio" th:field="*{gender}" value="M" />男
<input type="radio" th:field="*{gender}" value="F" />女
<!-- セレクト -->
<select th:field="*{prefecture}">
<option value="">選択してください</option>
<option th:each="p : ${prefectures}"
th:value="${p.code}"
th:text="${p.name}">県名</option>
</select>
<!-- チェックボックス -->
<input type="checkbox" th:field="*{terms}" />利用規約に同意
<button type="submit">送信</button>
</form>
Spring MVC との連携
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping
public String list(Model model) {
model.addAttribute("users", userService.findAll());
model.addAttribute("title", "ユーザー一覧");
return "user/list"; // → templates/user/list.html
}
@GetMapping("/new")
public String form(Model model) {
model.addAttribute("form", new UserForm());
return "user/edit";
}
@PostMapping("/save")
public String save(@Valid @ModelAttribute("form") UserForm form,
BindingResult result,
RedirectAttributes ra) {
if (result.hasErrors()) return "user/edit";
userService.save(form);
ra.addFlashAttribute("message", "保存しました");
return "redirect:/user";
}
}
レイアウト (fragment)
共通ヘッダ・フッタを切り出して再利用:
<!-- templates/fragments/layout.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>共通</title>
</head>
<body>
<header th:fragment="header">
<h1>サンプルサイト</h1>
<nav>
<a th:href="@{/}">ホーム</a>
<a th:href="@{/user}">ユーザー</a>
</nav>
</header>
<footer th:fragment="footer">
<p>© 2026 Example</p>
</footer>
</body>
</html><!-- templates/user/list.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>ユーザー一覧</title>
</head>
<body>
<!-- fragment を挿入 -->
<div th:replace="~{fragments/layout :: header}"></div>
<main>
<h2>ユーザー一覧</h2>
<ul>
<li th:each="u : ${users}" th:text="${u.name}"></li>
</ul>
</main>
<div th:replace="~{fragments/layout :: footer}"></div>
</body>
</html>
th:replace と th:insert の違い:
| 属性 | 動作 |
|---|---|
th:replace | 呼び出し側のタグごと置換 |
th:insert | 呼び出し側のタグ中に挿入 |
th:include | fragment の中身のみ挿入(非推奨、3.0+ で deprecated) |
JS / CSS との連携
<!-- JS にサーバー側変数を渡す -->
<script th:inline="javascript">
/*<![CDATA[*/
var user = {
id: /*[[${user.id}]]*/ 0,
name: /*[[${user.name}]]*/ ''
};
var contextPath = /*[[@{/}]]*/ '/';
/*]]>*/
</script>
<!-- CSS にも値を埋められる -->
<style th:inline="css">
.user-bg { background-image: url(/*[[@{/img/bg.jpg}]]*/ ''); }
</style>
ユーティリティオブジェクト
<!-- 日付フォーマット -->
<span th:text="${#dates.format(user.birthday, 'yyyy/MM/dd')}">2000/01/01</span>
<!-- 数値フォーマット -->
<span th:text="${#numbers.formatDecimal(price, 1, 'COMMA', 2, 'POINT')}">1,234.56</span>
<!-- 文字列操作 -->
<span th:text="${#strings.toUpperCase(user.name)}">YAMADA</span>
<span th:text="${#strings.abbreviate(text, 30)}">短縮...</span>
<!-- リスト操作 -->
<span th:text="${#lists.size(users)}">10</span>
<!-- null safe -->
<span th:text="${user?.address?.zip ?: '未登録'}">000-0000</span>
FAQ
Q: th:text と th:utext の違い
A: th:text は HTML エスケープあり(XSS 安全)、th:utext はエスケープなし(XSS 注意、自己責任)。原則 th:text。
Q: Thymeleaf 2 と 3 の違い
A: 3.0 から th:include 廃止、~{...} fragment 構文導入、HTML5 への移行など。Spring Boot 2.0+ は Thymeleaf 3 が標準。
Q: 開発中に変更が反映されない
A: spring.thymeleaf.cache=false を application.properties に。または DevTools を入れる。本番は cache=true。