はじめに (対象読者・この記事でわかること)
この記事は、Java・Spring Boot を使って Web アプリケーション開発を行っているエンジニア、特に Thymeleaf をテンプレートエンジンとして採用している方を対象としています。
ページ全体をリロードせずに、画面の一部だけを動的に更新したい、というニーズに応える具体的な実装方法を学びたい方に最適です。
本記事を読むことで、以下ができるようになります。
- Spring Boot + Thymeleaf プロジェクトに Ajax を組み込み、サーバー側からフラグメント(部分テンプレート)を返す方法
- jQuery(または vanilla JS)で取得した HTML を DOM に差し込む手順
- 実装時に陥りがちなエラーや、デバッグのコツ
このテーマは、ユーザー体験を向上させるために頻繁に求められる「部分更新」の実装を、フレームワークの標準機能だけでシンプルに実現する点に焦点を当てています。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。
Java の基本的な文法とオブジェクト指向概念
Spring Boot の基本的なプロジェクト構成(@Controller, @Service など)
HTML/CSS の基礎知識
任意だが、簡単な JavaScript(特に fetch API や jQuery の AJAX)に慣れていると実装が楽です
Spring Boot と Thymeleaf で部分画面更新を行う背景
Web アプリケーションでは、ユーザー操作に応じてページ全体を再描画するよりも、必要な箇所だけを差し替える方がレスポンスが速く、ユーザー体験が向上します。
Spring Boot と Thymeleaf の組み合わせは、サーバーサイドで HTML を生成する点で強力ですが、デフォルトでは「ページ全体」しか返しません。そこで、フラグメント(部分テンプレート)と Ajax を組み合わせることで、サーバー側で必要な HTML 片だけを返し、クライアント側で差し替えることが可能になります。
ここで重要になる概念は次の3点です。
-
Thymeleaf フラグメント
th:fragmentで定義したテンプレートの一部を、別テンプレートから呼び出す仕組みです。これにより、共通ヘッダーやテーブルの行、モーダルウィンドウなどを再利用できます。 -
Ajax リクエスト
JavaScript(今回は jQuery)で非同期リクエストを送り、サーバー側のコントローラがフラグメントだけを返すようにします。レスポンスは HTML 文字列になるため、クライアント側でそのまま DOM に挿入できます。 -
Spring MVC の
@ResponseBodyとModelAndView
フラグメントだけ返すエンドポイントは、@ResponseBodyで文字列として HTML を返すか、ModelAndViewでビュー名だけを指定し、th:replaceでフラグメントに差し替える形にします。
この仕組みを正しく組み合わせると、たとえば「一覧テーブルのページング」や「検索結果のリアルタイム更新」など、一般的な UI パターンをシンプルに実装できます。
実装手順:Spring Boot + Thymeleaf で画面の一部だけを非同期に更新する
以下では、商品一覧ページに「検索ボックス」で絞り込みを行い、結果テーブルだけを更新するサンプルを通じて、実装の全体像を解説します。プロジェクトは Maven、Spring Boot 3.2、Thymeleaf 3.1、jQuery 3.7 を使用します。
ステップ 1:プロジェクトの雛形作成
Bash$ spring init \ --dependencies=web,thymeleaf,lombok \ --package-name=com.example.demo \ demo-thymeleaf-ajax $ cd demo-thymeleaf-ajax $ ./mvnw clean package
src/main/resources/templates 配下に layout.html(ベースレイアウト)と product-list.html(一覧ページ)を作成します。
layout.html(共通レイアウト)
Html<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title th:replace="~{::title}">Demo</title> <script src="https://code.jquery.com/jquery-3.7.0.min.js"></script> </head> <body> <header th:replace="fragments/header :: header"></header> <main th:replace="~{::section}"></main> <footer th:replace="fragments/footer :: footer"></footer> </body> </html>
fragments/header.html(ヘッダーのフラグメント)
Html<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" th:fragment="header"> <header> <h1>商品一覧デモ</h1> </header> </html>
fragments/footer.html(フッターのフラグメント)
Html<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" th:fragment="footer"> <footer> <p>© 2025 Demo Company</p> </footer> </html>
ステップ 2:コントローラとサービスの実装
src/main/java/com/example/demo/controller/ProductController.java を作成します。
Javapackage com.example.demo.controller; import com.example.demo.service.ProductService; import com.example.demo.model.Product; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; import java.util.List; @Controller @RequiredArgsConstructor public class ProductController { private final ProductService productService; // 初期表示:全商品一覧 @GetMapping("/products") public String list(@RequestParam(value = "q", required = false) String query, Model model) { List<Product> products = (query == null || query.isBlank()) ? productService.findAll() : productService.search(query); model.addAttribute("products", products); model.addAttribute("query", query); return "product-list"; } // Ajax 用エンドポイント:フラグメントだけ返す @GetMapping(value = "/products/fragment", produces = "text/html") public String listFragment(@RequestParam(value = "q", required = false) String query, Model model) { List<Product> products = (query == null || query.isBlank()) ? productService.findAll() : productService.search(query); model.addAttribute("products", products); model.addAttribute("query", query); // → product-list-fragment.html がフラグメントとして返る return "product-list-fragment :: list"; } }
続いて、ダミーデータを返す ProductService を作ります。
Javapackage com.example.demo.service; import com.example.demo.model.Product; import org.springframework.stereotype.Service; import java.util.*; import java.util.stream.*; @Service public class ProductService { private final List<Product> allProducts = List.of( new Product(1L, "Apple iPhone 15", 999), new Product(2L, "Samsung Galaxy S24", 899), new Product(3L, "Google Pixel 8", 799), new Product(4L, "Sony Xperia 5", 749), new Product(5L, "Nokia X30", 499) ); public List<Product> findAll() { return allProducts; } public List<Product> search(String keyword) { return allProducts.stream() .filter(p -> p.getName().toLowerCase().contains(keyword.toLowerCase())) .collect(Collectors.toList()); } }
Product エンティティはシンプルな POJO。
Javapackage com.example.demo.model; import lombok.AllArgsConstructor; import lombok.Data; @Data @AllArgsConstructor public class Product { private Long id; private String name; private int price; }
ステップ 3:テンプレートの作成
product-list.html(全体ページ)
Html<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" th:replace="layout :: layout"> <head> <title>商品一覧</title> </head> <body> <section> <div> <input type="text" id="searchBox" placeholder="商品名で検索" th:value="${query}" /> <button id="searchBtn">検索</button> </div> <!-- 結果テーブルはフラグメントで差し替え --> <div id="productTable" th:replace="product-list-fragment :: list(${products}, ${query})"> <!-- 初回ロード時はサーバ側で埋め込まれる --> </div> </section> <script th:inline="javascript"> /*<![CDATA[*/ $(function() { $('#searchBtn').on('click', function() { const q = $('#searchBox').val(); $.ajax({ url: /*[[@{/products/fragment}]]*/ '/products/fragment', data: { q: q }, success: function(fragment) { $('#productTable').html(fragment); }, error: function(xhr) { alert('検索に失敗しました: ' + xhr.status); } }); }); // Enter キーでも検索できるように $('#searchBox').on('keypress', function(e) { if (e.which === 13) { $('#searchBtn').click(); } }); }); /*]]>*/ </script> </body> </html>
product-list-fragment.html(テーブル部分のみのフラグメント)
Html<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" th:fragment="list (products, query)"> <table border="1" width="100%"> <thead> <tr><th>ID</th><th>商品名</th><th>価格 (¥)</th></tr> </thead> <tbody> <tr th:each="p : ${products}"> <td th:text="${p.id}"></td> <td th:text="${p.name}"></td> <td th:text="${p.price}"></td> </tr> <tr th:if="${#lists.isEmpty(products)}"> <td colspan="3" style="text-align:center;">該当する商品がありません</td> </tr> </tbody> </table> </html>
ポイント:
th:replace="product-list-fragment :: list(${products}, ${query})"で、最初のフルページ表示時にもフラグメントが埋め込まれます。- Ajax の
successコールバックで受け取った HTML を#productTableに上書きするだけで、ページ全体のリロードが不要になります。 th:inline="javascript"により、Thymeleaf が URL 変数を自動展開します(@{/products/fragment})。
ステップ 4:動作確認とデバッグ
- アプリケーション起動
bash $ ./mvnw spring-boot:run - ブラウザで
http://localhost:8080/productsを開くと、全商品がテーブルに表示されます。 - 検索ボックスに「Apple」と入力し「検索」ボタンをクリックすると、サーバーが
/products/fragment?q=Appleに対して部分テンプレートを返し、テーブルが「Apple iPhone 15」のみになることを確認してください。
ハマった点やエラー解決
| 発生した問題 | 原因 | 解決策 |
|---|---|---|
| 404 エラーでフラグメントが取得できない | Ajax の URL がコンテキストパスを考慮していなかった | th:inline="javascript" 内で @{/products/fragment} を使用し、Spring が生成する正しいパスに置き換える |
| クリック時にテーブルが空になる | th:replace でフラグメントに渡すパラメータ名が一致していなかった |
コントローラの listFragment で model.addAttribute("products", ...) を設定し、テンプレート側の list (products, query) と名前を合わせた |
| JavaScript のエラー「$ is not defined」 | jQuery がロードされていなかった | <script src="https://code.jquery.com/jquery-3.7.0.min.js"></script> を layout.html の <head> に追加 |
さらに高度な実装例
- Spring WebFlux + Thymeleaf Reactive
非同期ストリームでリアクティブにデータを配信し、Server-Sent Eventsと組み合わせると、リアルタイム更新が可能です。 - Alpine.js で軽量化
jQuery を除外し、Alpine.js のx-data/x-on:clickで同等の機能を実装すると、バンドルサイズが小さくなります。 - キャッシュ制御
Cache-Controlヘッダーや ETag を設定し、頻繁にリクエストが走らないように最適化できます。
まとめ
本記事では、Spring Boot と Thymeleaf を組み合わせ、Ajax とフラグメント を活用した「画面の一部だけを非同期に更新」する手順を、実装サンプルとともに解説しました。
- フラグメント(
th:fragment)で再利用可能なテンプレート部品を定義し、サーバー側で必要な部分だけを返す - Ajax(jQuery)で非同期にフラグメントを取得し、クライアント側の DOM に差し替えるだけでページ全体のリロードを防止
- 実装時の典型的なエラー(URL パス、属性名の不一致、jQuery 未読込)とその対処法を紹介
これにより、ユーザー体験が向上すると同時に、サーバー・クライアント双方の負荷を抑えることができます。次は WebSocket や Server‑Sent Events を組み合わせたリアルタイム更新や、Alpine.js での軽量化戦略についても取り上げる予定です。
参考資料
- Spring Boot 公式ドキュメント – Thymeleaf
- Thymeleaf 公式マニュアル – フラグメント
- jQuery AJAX 入門
- 書籍: 「Spring Boot & Thymeleaf 実践ガイド」 (技術評論社, 2023)
