Spring BootでREST APIを作るのには慣れてきたけど、HTMLを返すWebアプリの作り方はよく分からない、という方は多いと思います。今回はそういった方に向けて、Thymeleafを使ったサーバーサイドレンダリングの基本から、フォーム処理・バリデーション・Spring Security連携まで順を追って説明します。
ThymeleafとSpring Bootの関係
ThymeleafはJava向けのテンプレートエンジンで、HTMLファイルにそのまま属性を書き足していくスタイルが特徴です。JSPと違ってブラウザで直接開いてもHTMLとして表示されるので、デザイナーとの協業がしやすいという利点があります。
Spring Bootでは spring-boot-starter-thymeleaf を追加するだけで ThymeleafViewResolver が自動設定されます。@Controller のハンドラがテンプレート名(文字列)を返すと、src/main/resources/templates/ 以下の対応する .html ファイルが自動的に解決されます。
@RestController との違いで迷うところですが、シンプルです。@RestController はレスポンスボディに直接値を書き込みます(JSON返却など)。@Controller はテンプレート名を返してビュー解決を行います。
依存関係の追加
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-validation'
}
ディレクトリ構成はこんな感じになります。
src/main/resources/
├── templates/ # .html テンプレートを置く場所
│ ├── index.html
│ └── fragments/
│ └── layout.html
└── static/ # CSS・JS・画像など静的ファイル
├── css/
└── js/
最初のHTMLレスポンスを返す
@Controller でモデルにデータを詰めてテンプレート名を返すだけです。
@Controller
public class HomeController {
@GetMapping("/")
public String index(Model model) {
model.addAttribute("message", "こんにちは、Thymeleaf!");
model.addAttribute("items", List.of("りんご", "みかん", "ぶどう"));
return "index"; // templates/index.html を解決する
}
}
テンプレート側はこのように書きます。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>トップページ</title>
<link rel="stylesheet" th:href="@{/css/style.css}">
</head>
<body>
<h1 th:text="${message}">デフォルトメッセージ</h1>
<ul>
<li th:each="item : ${items}" th:text="${item}">サンプル</li>
</ul>
<p th:if="${items.size() > 2}">3件以上あります</p>
</body>
</html>
th:text は値をHTMLエスケープして出力します。HTMLタグを含む文字列をそのまま出力したい場合は th:utext を使いますが、XSSに注意が必要です。th:href の @{...} はURLを生成する式で、コンテキストパスを自動的に考慮してくれます。
フォームオブジェクトとデータバインディング
フォーム処理には「コマンドオブジェクト」と呼ばれるPOJOを使います。
public class UserForm {
@NotBlank(message = "名前は必須です")
@Size(max = 50, message = "50文字以内で入力してください")
private String name;
@Email(message = "メールアドレスの形式が正しくありません")
private String email;
// getter / setter
}
GETハンドラで空のフォームオブジェクトをモデルに渡し、POSTハンドラで受け取ります。
@Controller
@RequestMapping("/users")
public class UserController {
@GetMapping("/new")
public String newUser(Model model) {
model.addAttribute("userForm", new UserForm());
return "users/new";
}
@PostMapping
public String create(@Valid @ModelAttribute UserForm userForm,
BindingResult result) {
if (result.hasErrors()) {
return "users/new"; // バリデーションエラー時はフォームに戻る
}
// 保存処理...
return "redirect:/users";
}
}
バリデーションアノテーションの詳細は Spring Bootで@Validを使ったバリデーション を参照してください。
テンプレートでは th:object と th:field を使います。
<form th:action="@{/users}" th:object="${userForm}" method="post">
<div>
<label>名前</label>
<input type="text" th:field="*{name}">
<span th:if="${#fields.hasErrors('name')}" th:errors="*{name}" style="color:red"></span>
</div>
<div>
<label>メール</label>
<input type="email" th:field="*{email}">
<span th:if="${#fields.hasErrors('email')}" th:errors="*{email}" style="color:red"></span>
</div>
<button type="submit">登録</button>
</form>
th:field="*{name}" と書くと id="name" name="name" value="..." が自動生成されます。th:errors はそのフィールドに紐づくエラーメッセージを全て表示してくれます。
GETハンドラで model.addAttribute("userForm", new UserForm()) を忘れると、テンプレートレンダリング時に th:object が null になってエラーになります。これが初学者がよく引っかかるポイントです。
テンプレートの断片化
ヘッダーやフッターを各ページで繰り返し書くのは辛いので、th:fragment で共通パーツを切り出しましょう。
<!-- fragments/layout.html -->
<header th:fragment="header">
<nav>
<a th:href="@{/}">ホーム</a>
<a th:href="@{/users/new}">新規登録</a>
</nav>
</header>
各ページテンプレートからはこのように埋め込みます。
<body>
<div th:replace="~{fragments/layout :: header}"></div>
<!-- ページ固有のコンテンツ -->
</body>
th:replace は対象要素ごと置き換え、th:insert は対象要素の中に挿入します。ヘッダーやフッターのような独立したパーツは th:replace の方が自然です。
Spring Securityとの連携
Spring Securityを使っている場合、thymeleaf-extras-springsecurity6 を追加すると sec:authorize などのカスタム属性が使えるようになります。
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<body>
<div sec:authorize="isAuthenticated()">
<p>ようこそ、<span sec:authentication="name"></span> さん</p>
<a th:href="@{/logout}">ログアウト</a>
</div>
<div sec:authorize="!isAuthenticated()">
<a th:href="@{/login}">ログイン</a>
</div>
</body>
</html>
sec:authorize の中にはSpring SecurityのSpEL式がそのまま使えます。Spring Security自体の設定については Spring Bootのセキュリティ基本認証チュートリアル を参照してください。
なお、Spring SecurityはデフォルトでCSRFトークンをフォームに自動挿入してくれます。Thymeleafと組み合わせると <input type="hidden" name="_csrf" ...> が自動で追加されるので、特に意識しなくて大丈夫です。
まとめ
ThymeleafはSpring Bootとの相性が非常に良く、スターターを追加するだけで動き始めるのが嬉しいところです。
@Controllerでテンプレート名を返してHTMLレスポンスを作るth:text・th:each・th:ifで動的なHTMLを組み立てるth:objectとth:fieldでフォームとオブジェクトをバインドするth:errorsでバリデーションエラーをテンプレートに表示するth:fragmentとth:replaceでレイアウトを共通化するthymeleaf-extras-springsecurity6で認証状態に応じた表示切替ができる
REST APIとは違うアプローチですが、管理画面や社内ツールなど「バックエンドでHTMLを作りたい」場面では今でも十分実用的な選択肢です。ぜひ試してみてください。