Thymeleaf とは?最新のテンプレートエンジンを徹底解説

従来のJSPと比較したThymeleafの革新的な特徴

Thymeleafは、モダンなJavaアプリケーション開発において最も人気のあるテンプレートエンジンの1つです。従来のJSPと比較して、以下のような革新的な特徴を持っています:

1. ナチュラルテンプレート機能

Thymeleafの最大の特徴は、HTMLファイルをそのまま静的プロトタイプとして使用できる「ナチュラルテンプレート」機能です。

<!-- JSPの場合 -->
<span><%= user.getName() %></span>

<!-- Thymeleafの場合 -->
<span th:text="${user.name}">John Doe</span>

この例では、Thymeleafを使用した場合、テンプレートファイルをブラウザで直接開いても「John Doe」と表示され、アプリケーション実行時には動的にユーザー名が表示されます。

2. 型安全性の向上

Thymeleafは、Spring Frameworkとの統合により、強力な型安全性を提供します:

// Controllerでのモデル設定
@GetMapping("/user")
public String showUser(Model model) {
    User user = userService.getCurrentUser();
    model.addAttribute("user", user);
    return "user/profile";
}
<!-- テンプレートでの型安全なアクセス -->
<div th:object="${user}">
    <p th:text="*{name}">ユーザー名</p>
    <p th:text="*{email}">メールアドレス</p>
</div>

3. モジュール化とレイアウト管理

Thymeleafは、効率的なテンプレートの再利用とレイアウト管理を可能にします:

<!-- fragments/header.html -->
<header th:fragment="pageHeader">
    <h1>サイトヘッダー</h1>
    <nav>...</nav>
</header>

<!-- main.html -->
<div th:replace="fragments/header :: pageHeader"></div>

Spring Bootとの親和性が高い理由

Thymeleafは、Spring Bootと特に相性が良く、以下の理由で多くの開発者に選ばれています:

1. 自動設定機能

Spring Bootのauto-configuration機能により、最小限の設定で開始できます:

<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

2. Spring Security との統合

セキュリティ機能との連携が容易です:

<!-- セキュリティ統合の例 -->
<div th:if="${#authorization.expression('hasRole(''ADMIN'')')}">
    管理者向けコンテンツ
</div>

3. Spring MVCとの完全な互換性

  • コントローラーとの連携が直感的
  • フォームバインディングが簡単
  • バリデーション機能との統合が容易

4. 開発者体験の向上

  1. ホットリロード対応
    • spring-boot-devtoolsとの連携でライブリロードが可能
    • テンプレートのキャッシュが開発モードで自動的に無効化
  2. IDEサポート
    • Springツールスイートでの完全なサポート
    • コード補完や構文ハイライトが利用可能
  3. デバッグのしやすさ
    • エラーメッセージが分かりやすい
    • テンプレート処理のトレースが可能

これらの特徴により、Thymeleafは特にSpring Bootを使用する現代のJavaWeb開発において、最適なテンプレートエンジンの選択肢となっています。

Thymeleafの基本機能と実装手順

開発環境のセットアップと依存関係の設定

Spring Bootプロジェクトでのthymeleafの導入は非常に簡単です。以下の手順で環境を構築できます。

1. Maven依存関係の追加

<dependencies>
    <!-- Spring Boot Starter for Thymeleaf -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>

    <!-- 開発者ツール(オプション:ホットリロード用) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
</dependencies>

2. アプリケーションプロパティの設定

# application.properties
spring.thymeleaf.cache=false        # 開発時はキャッシュを無効化
spring.thymeleaf.mode=HTML          # テンプレートモードの設定
spring.thymeleaf.encoding=UTF-8     # エンコーディング設定
spring.thymeleaf.prefix=classpath:/templates/    # テンプレートの配置場所
spring.thymeleaf.suffix=.html       # テンプレートの拡張子

基本的な構文とテンプレート作成の流れ

1. 基本的な式の種類

<!-- 変数式 -->
<p th:text="${message}">デフォルトメッセージ</p>

<!-- 選択変数式 -->
<div th:object="${user}">
    <p th:text="*{name}">ユーザー名</p>
    <p th:text="*{email}">メール</p>
</div>

<!-- リンク式 -->
<a th:href="@{/user/{id}(id=${user.id})}">ユーザープロフィール</a>

<!-- メッセージ式(i18n) -->
<p th:text="#{welcome.message}">ようこそ</p>

2. 主要な属性の使用方法

<!-- テキスト設定 -->
<p th:text="${message}">メッセージ</p>
<p th:utext="${htmlContent}">HTMLコンテンツ</p>

<!-- 条件分岐 -->
<div th:if="${isAdmin}">管理者メニュー</div>
<div th:unless="${isAdmin}">一般ユーザーメニュー</div>

<!-- 繰り返し -->
<ul>
    <li th:each="item : ${items}" th:text="${item.name}">商品名</li>
</ul>

<!-- 属性設定 -->
<input th:value="${user.name}" th:readonly="${readonly}">

データバインディングとモデル操作の基礎

1. コントローラーでのモデル設定

@Controller
@RequestMapping("/users")
public class UserController {

    @GetMapping("/profile")
    public String showProfile(Model model) {
        // ユーザー情報の設定
        User user = new User("山田太郎", "yamada@example.com");
        model.addAttribute("user", user);

        // リストデータの設定
        List<String> roles = Arrays.asList("USER", "ADMIN");
        model.addAttribute("roles", roles);

        return "user/profile";
    }

    @PostMapping("/update")
    public String updateProfile(@ModelAttribute User user, 
                              BindingResult result) {
        if (result.hasErrors()) {
            return "user/profile";
        }
        // ユーザー更新処理
        return "redirect:/users/profile";
    }
}

2. フォームバインディングの実装

<form th:action="@{/users/update}" th:object="${user}" method="post">
    <div>
        <label>名前:</label>
        <input type="text" th:field="*{name}">
        <span th:if="${#fields.hasErrors('name')}" 
              th:errors="*{name}" 
              class="error">
            名前エラー
        </span>
    </div>

    <div>
        <label>メール:</label>
        <input type="email" th:field="*{email}">
        <span th:if="${#fields.hasErrors('email')}" 
              th:errors="*{email}" 
              class="error">
            メールエラー
        </span>
    </div>

    <button type="submit">更新</button>
</form>

3. ユーティリティオブジェクトの活用

<!-- 日付フォーマット -->
<p th:text="${#temporals.format(localDate, 'yyyy/MM/dd')}">2024/01/01</p>

<!-- 数値フォーマット -->
<p th:text="${#numbers.formatDecimal(price, 1, 'COMMA', 2, 'POINT')}">1,234.56</p>

<!-- 文字列操作 -->
<p th:text="${#strings.toUpperCase(text)}">TEXT</p>

<!-- リスト操作 -->
<p th:text="${#lists.size(items)}">アイテム数</p>

これらの基本機能を理解することで、Thymeleafを使用した効果的なテンプレート開発が可能になります。次のセクションでは、より高度なテクニックについて説明していきます。

実践Thymeleafテクニック集

条件分岐とループ処理の効率的な実装方法

1. 高度な条件分岐

<!-- switch文による複数条件の制御 -->
<div th:switch="${user.role}">
    <p th:case="'ADMIN'">管理者向けコンテンツ</p>
    <p th:case="'MANAGER'">マネージャー向けコンテンツ</p>
    <p th:case="*">一般ユーザー向けコンテンツ</p>
</div>

<!-- 複合条件の使用 -->
<div th:if="${user.age >= 20 and user.verified}">
    成人済み認証ユーザー向けコンテンツ
</div>

<!-- Elvis演算子の活用 -->
<span th:text="${user.nickname ?: user.fullName}">デフォルト名</span>

2. 効率的なループ処理

<!-- ステータス変数の活用 -->
<tr th:each="item, stat : ${items}">
    <td th:text="${stat.index + 1}">1</td>
    <td th:text="${item.name}">商品名</td>
    <td th:class="${stat.odd}? 'odd' : 'even'">
        <span th:text="${item.price}">1000</span>円
    </td>
    <!-- 最初と最後の要素の特別処理 -->
    <td th:if="${stat.first}">最新商品!</td>
    <td th:if="${stat.last}">最終商品</td>
</tr>

<!-- ネストされたループの実装 -->
<div th:each="category : ${categories}">
    <h3 th:text="${category.name}">カテゴリ名</h3>
    <ul>
        <li th:each="product : ${category.products}"
            th:text="${product.name}">商品名</li>
    </ul>
</div>

フラグメントを活用したコンポーネント設計

1. 再利用可能なフラグメントの定義

<!-- fragments/common.html -->
<!-- ヘッダーフラグメント -->
<header th:fragment="pageHeader(title)">
    <h1 th:text="${title}">ページタイトル</h1>
    <nav th:replace="fragments/navigation :: mainNav">
        <ul>
            <li><a href="#">Home</a></li>
            <li><a href="#">About</a></li>
        </ul>
    </nav>
</header>

<!-- フッターフラグメント(パラメータ付き) -->
<footer th:fragment="pageFooter(year)">
    <p th:text="'© ' + ${year} + ' My Company'">© 2024 My Company</p>
</footer>

2. フラグメントの高度な使用方法

<!-- メインページでのフラグメント使用 -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <!-- 共通CSSとJSの読み込み -->
    <th:block th:replace="fragments/common :: headerResources">
        <link rel="stylesheet" href="default.css">
        <script src="default.js"></script>
    </th:block>
</head>
<body>
    <!-- パラメータ付きフラグメントの挿入 -->
    <div th:replace="fragments/common :: pageHeader('ようこそ')">
        ヘッダー部分
    </div>

    <!-- 条件付きフラグメント挿入 -->
    <div th:replace="fragments/common :: ${user.premium ? 'premiumContent' : 'basicContent'}">
        コンテンツ部分
    </div>

    <!-- フラグメント内容のカスタマイズ -->
    <div th:replace="fragments/common :: contentBlock(~{::customContent})">
        <div th:fragment="customContent">
            <p>カスタムコンテンツ</p>
        </div>
    </div>
</body>
</html>

フォーム処理とバリデーションの実装

1. 高度なフォーム処理

@Controller
@RequestMapping("/products")
public class ProductController {

    @GetMapping("/create")
    public String showForm(Model model) {
        ProductForm form = new ProductForm();
        model.addAttribute("productForm", form);
        return "products/form";
    }

    @PostMapping("/create")
    public String createProduct(
            @Valid @ModelAttribute("productForm") ProductForm form,
            BindingResult result,
            RedirectAttributes redirectAttributes) {

        if (result.hasErrors()) {
            return "products/form";
        }

        // 成功メッセージの設定
        redirectAttributes.addFlashAttribute(
            "message", "商品が正常に作成されました。");
        return "redirect:/products";
    }
}

2. 高度なバリデーション実装

<!-- products/form.html -->
<form th:action="@{/products/create}" 
      th:object="${productForm}" 
      method="post"
      class="product-form">

    <!-- グローバルエラーメッセージ -->
    <div th:if="${#fields.hasGlobalErrors()}" 
         class="alert alert-danger">
        <p th:each="err : ${#fields.globalErrors()}" 
           th:text="${err}">エラーメッセージ</p>
    </div>

    <!-- 商品名入力フィールド -->
    <div class="form-group" 
         th:classappend="${#fields.hasErrors('name')}? 'has-error'">
        <label th:for="name">商品名</label>
        <input type="text" 
               th:field="*{name}"
               class="form-control"
               th:errorclass="is-invalid">
        <div class="invalid-feedback" 
             th:if="${#fields.hasErrors('name')}"
             th:errors="*{name}">
            商品名エラー
        </div>
    </div>

    <!-- 価格入力フィールド(カスタムバリデーション) -->
    <div class="form-group">
        <label th:for="price">価格</label>
        <input type="number" 
               th:field="*{price}"
               class="form-control"
               th:errorclass="is-invalid"
               min="0">
        <div class="invalid-feedback" 
             th:if="${#fields.hasErrors('price')}"
             th:errors="*{price}">
            価格エラー
        </div>
    </div>

    <!-- 動的なカテゴリー選択 -->
    <div class="form-group">
        <label>カテゴリー</label>
        <select th:field="*{categoryId}" class="form-control">
            <option value="">カテゴリーを選択</option>
            <option th:each="category : ${categories}"
                    th:value="${category.id}"
                    th:text="${category.name}">
                カテゴリー名
            </option>
        </select>
    </div>

    <!-- 送信ボタン -->
    <button type="submit" 
            class="btn btn-primary"
            th:disabled="${#fields.hasErrors()}">
        登録
    </button>
</form>

これらの実践的なテクニックを活用することで、保守性が高く、再利用可能なThymeleafテンプレートを作成できます。次のセクションでは、セキュリティ対策について詳しく説明していきます。

Thymeleafのセキュリティ対策

XSS対策攻撃とエスケープ処理の重要性

1. XSS攻撃のリスク

Thymeleafは、デフォルトでHTMLエスケープを提供していますが、適切な使用方法を理解することが重要です。

<!-- 悪意のあるスクリプトが含まれる可能性のある変数 -->
<!-- NG: エスケープなしでの出力 -->
<div th:utext="${userInput}">危険な出力</div>

<!-- OK: 適切なエスケープ処理 -->
<div th:text="${userInput}">安全な出力</div>

2. エスケープ処理の実装

@Controller
public class ContentController {

    @GetMapping("/content")
    public String showContent(Model model) {
        // HTMLエスケープ処理の実装例
        String userInput = "<script>alert('危険!')</script>";
        String escapedContent = HtmlUtils.htmlEscape(userInput);
        model.addAttribute("content", escapedContent);

        return "content/view";
    }
}

3. セキュアな出力方法

<!-- 1. 変数出力時の適切なエスケープ -->
<div>
    <!-- 通常のテキスト出力(自動エスケープ) -->
    <p th:text="${content}">コンテンツ</p>

    <!-- JavaScript内での安全な出力 -->
    <script th:inline="javascript">
        const userContent = /*[[${content}]]*/ 'デフォルト値';
    </script>
</div>

<!-- 2. URL生成時の安全な処理 -->
<a th:href="@{/user/{id}(id=${userId})}">ユーザープロフィール</a>

<!-- 3. 条件付きコンテンツ表示 -->
<div th:remove="${isUnsafe} ? 'all' : 'none'">
    保護されたコンテンツ
</div>

CSRF対策の実装方法

1. Spring Securityとの連携

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf()
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            .and()
            .authorizeRequests()
                .antMatchers("/public/**").permitAll()
                .anyRequest().authenticated();
    }
}

2. テンプレートでのCSRFトークン実装

<!-- フォームでのCSRFトークン実装 -->
<form th:action="@{/user/update}" method="post">
    <!-- CSRFトークンの自動追加 -->
    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />

    <input type="text" name="username" />
    <button type="submit">更新</button>
</form>

<!-- Ajax呼び出しでのCSRF対策 -->
<script th:inline="javascript">
    const csrfToken = /*[[${_csrf.token}]]*/ '';
    const csrfHeader = /*[[${_csrf.headerName}]]*/ '';

    // Ajaxリクエストの設定
    axios.defaults.headers.common[csrfHeader] = csrfToken;
</script>

3. セキュリティヘッダーの設定

@Configuration
public class SecurityHeaderConfig {

    @Bean
    public WebMvcConfigurer webMvcConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addInterceptors(InterceptorRegistry registry) {
                registry.addInterceptor(new HandlerInterceptor() {
                    @Override
                    public boolean preHandle(HttpServletRequest request, 
                                          HttpServletResponse response, 
                                          Object handler) {
                        // セキュリティヘッダーの設定
                        response.setHeader("X-Content-Type-Options", "nosniff");
                        response.setHeader("X-Frame-Options", "DENY");
                        response.setHeader("X-XSS-Protection", "1; mode=block");
                        response.setHeader("Content-Security-Policy", 
                            "default-src 'self'; script-src 'self' 'unsafe-inline'");
                        return true;
                    }
                });
            }
        };
    }
}

セキュリティチェックリスト

  1. 入力値の検証
@RestController
public class InputValidationExample {

    @PostMapping("/api/data")
    public ResponseEntity<?> processInput(
            @RequestBody @Valid UserInput input, 
            BindingResult result) {

        if (result.hasErrors()) {
            return ResponseEntity.badRequest()
                .body(result.getAllErrors());
        }

        // 入力値の追加検証
        if (!InputValidator.isValidInput(input.getContent())) {
            return ResponseEntity.badRequest()
                .body("Invalid input format");
        }

        // 安全な処理の実行
        return ResponseEntity.ok()
            .body("処理成功");
    }
}
  1. 出力のエンコーディング
<!-- 特殊文字を含む可能性のあるデータの出力 -->
<table>
    <tr th:each="item : ${items}">
        <!-- HTMLエスケープ処理 -->
        <td th:text="${item.name}">商品名</td>

        <!-- URLエンコーディング -->
        <td>
            <a th:href="@{/items/{id}(id=${#uris.escapePathSegment(item.id)})}">
                詳細
            </a>
        </td>

        <!-- JavaScript用エスケープ -->
        <td th:onclick="|showDetails('${#strings.escapeJavaScript(item.description)}')|">
            説明を表示
        </td>
    </tr>
</table>

これらのセキュリティ対策を適切に実装することで、安全なWebアプリケーションの開発が可能になります。次のセクションでは、パフォーマンス最適化について説明していきます。

パフォーマンス最適化のベストプラクティス

キャッシュ機能の効果的な活用方法

1. テンプレートキャッシュの設定

# application.properties
# プロダクション環境での設定
spring.thymeleaf.cache=true
spring.thymeleaf.cache-period=3600
spring.thymeleaf.cache-ttl=3600

# 開発環境での設定
spring.thymeleaf.cache=false

2. カスタムキャッシュ設定

@Configuration
public class ThymeleafConfig {

    @Bean
    public SpringResourceTemplateResolver templateResolver() {
        SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
        resolver.setPrefix("classpath:/templates/");
        resolver.setSuffix(".html");
        resolver.setTemplateMode(TemplateMode.HTML);

        // キャッシュ設定
        resolver.setCacheable(true);
        resolver.setCacheTTLMs(3600000L); // 1時間

        // テンプレート解決の最適化
        resolver.setCharacterEncoding("UTF-8");
        resolver.setCheckExistence(true);

        return resolver;
    }

    @Bean
    public ISpringTemplateEngine templateEngine() {
        SpringTemplateEngine engine = new SpringTemplateEngine();
        engine.setTemplateResolver(templateResolver());

        // キャッシュサイズの設定
        engine.setCacheManager(new StandardCacheManager() {
            @Override
            protected void initializeCaches() {
                super.initializeCaches();
                getTemplateCache().setMaxSize(200);
            }
        });

        return engine;
    }
}

テンプレート処理の高速化テクニック

1. フラグメントの最適化

<!-- 効率的なフラグメント定義 -->
<!-- fragments/common.html -->
<div th:fragment="userInfo(user)" th:remove="tag">
    <span th:text="${user.name}">ユーザー名</span>
    <span th:text="${user.email}">メール</span>
</div>

<!-- メインページでの効率的な使用 -->
<div>
    <!-- フラグメントのインライン化 -->
    <th:block th:replace="fragments/common :: userInfo(${currentUser})"/>

    <!-- 複数フラグメントの一括読み込み -->
    <th:block th:replace="fragments/common :: common-resources"/>
</div>

2. データ処理の最適化

@Controller
public class OptimizedController {

    @GetMapping("/users")
    public String listUsers(Model model) {
        // データの事前処理とキャッシュ
        List<UserDTO> users = userService.getCachedUsers();

        // ページネーションの最適化
        Page<UserDTO> userPage = new PageImpl<>(users, 
            PageRequest.of(0, 10), users.size());

        model.addAttribute("userPage", userPage);
        return "users/list";
    }
}

// ビューでの効率的なデータ表示
<table>
    <tr th:each="user, stat : ${userPage.content}" 
        th:if="${stat.index < 10}">
        <td th:text="${user.name}">名前</td>
    </tr>
</table>

3. リソース最適化テクニック

<!-- リソースの最適化 -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <!-- CSSの遅延読み込み -->
    <link rel="preload" 
          th:href="@{/css/styles.css}" 
          as="style" 
          onload="this.onload=null;this.rel='stylesheet'">

    <!-- 重要なスクリプトの即時読み込み -->
    <script th:src="@{/js/critical.js}" 
            defer></script>

    <!-- 非重要なスクリプトの遅延読み込み -->
    <script th:src="@{/js/non-critical.js}" 
            async></script>
</head>
<body>
    <!-- 画像の遅延読み込み -->
    <img th:src="@{/images/large-image.jpg}" 
         loading="lazy" 
         alt="遅延読み込み画像">
</body>
</html>

4. パフォーマンス監視と測定

@Configuration
public class PerformanceMonitorConfig {

    @Bean
    public FilterRegistrationBean<PerformanceMonitorFilter> performanceMonitorFilter() {
        FilterRegistrationBean<PerformanceMonitorFilter> registrationBean 
            = new FilterRegistrationBean<>();

        registrationBean.setFilter(new PerformanceMonitorFilter());
        registrationBean.addUrlPatterns("/*");
        return registrationBean;
    }
}

@Component
public class PerformanceMonitorFilter implements Filter {

    private static final Logger logger = LoggerFactory.getLogger(PerformanceMonitorFilter.class);

    @Override
    public void doFilter(ServletRequest request, 
                        ServletResponse response, 
                        FilterChain chain) throws IOException, ServletException {
        long startTime = System.currentTimeMillis();

        try {
            chain.doFilter(request, response);
        } finally {
            long endTime = System.currentTimeMillis();
            long processingTime = endTime - startTime;

            if (processingTime > 1000) { // 1秒以上かかった処理を記録
                logger.warn("Slow processing detected: {} ms for URI: {}", 
                    processingTime, 
                    ((HttpServletRequest) request).getRequestURI());
            }
        }
    }
}

パフォーマンス最適化チェックリスト

  1. テンプレートの最適化
  • フラグメントの適切な使用
  • 不要なネストの削除
  • 条件分岐の最適化
  1. リソース管理
  • 静的リソースのキャッシュ設定
  • 適切なキャッシュヘッダーの設定
  • リソースの圧縮
  1. データアクセスの最適化
  • N+1問題の回避
  • 必要なデータのみの取得
  • 適切なページネーション

これらの最適化テクニックを適切に実装することで、Thymeleafアプリケーションのパフォーマンスを大幅に改善できます。次のセクションでは、実践的なユースケースについて説明していきます。

実践的なユースケースとサンプルコード

レスポンシブなレイアウトの実装例

1. モバイルファーストのレイアウト設計

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>レスポンシブダッシュボード</title>

    <style th:inline="css">
        /* レスポンシブグリッドの定義 */
        .grid-container {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
            gap: 1rem;
            padding: 1rem;
        }

        /* カードコンポーネント */
        .card {
            background: #fff;
            border-radius: 8px;
            padding: 1rem;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }

        /* メディアクエリ */
        @media (max-width: 768px) {
            .grid-container {
                grid-template-columns: 1fr;
            }
        }
    </style>
</head>
<body>
    <!-- レスポンシブなダッシュボード実装 -->
    <div class="grid-container">
        <!-- 統計カード -->
        <div class="card" th:each="stat : ${statistics}">
            <h3 th:text="${stat.title}">統計タイトル</h3>
            <p th:text="${stat.value}">値</p>
            <div th:replace="fragments/charts :: ${stat.chartType}(${stat.data})">
                チャート
            </div>
        </div>
    </div>

    <!-- レスポンシブなナビゲーション -->
    <nav>
        <button class="menu-toggle" th:onclick="'toggleMenu()'">
            メニュー
        </button>
        <ul class="nav-items" th:classappend="${isMobile} ? 'mobile' : ''">
            <li th:each="item : ${menuItems}">
                <a th:href="@{${item.url}}" th:text="${item.name}">
                    メニュー項目
                </a>
            </li>
        </ul>
    </nav>
</body>
</html>

2. データテーブルのレスポンシブ対応

<!-- レスポンシブテーブルコンポーネント -->
<div class="table-container">
    <table class="responsive-table">
        <thead>
            <tr>
                <th th:each="header : ${headers}" 
                    th:text="${header}">ヘッダー</th>
            </tr>
        </thead>
        <tbody>
            <tr th:each="row : ${data}">
                <td th:each="cell, stat : ${row}"
                    th:data-label="${headers[stat.index]}"
                    th:text="${cell}">
                    セルデータ
                </td>
            </tr>
        </tbody>
    </table>
</div>

<style>
@media (max-width: 768px) {
    .responsive-table td {
        display: block;
    }
    .responsive-table td::before {
        content: attr(data-label);
        font-weight: bold;
    }
}
</style>

REST APIとの連携パターン

1. API呼び出しと結果表示

@Controller
@RequestMapping("/api-demo")
public class ApiDemoController {

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/users")
    public String getUserData(Model model) {
        // API呼び出し
        ResponseEntity<List<UserDTO>> response = restTemplate.exchange(
            "https://api.example.com/users",
            HttpMethod.GET,
            null,
            new ParameterizedTypeReference<List<UserDTO>>() {}
        );

        model.addAttribute("users", response.getBody());
        return "users/list";
    }
}
<!-- users/list.html -->
<div class="user-grid">
    <!-- APIデータの表示 -->
    <div th:each="user : ${users}" class="user-card">
        <img th:src="${user.avatarUrl}" alt="ユーザーアバター">
        <h3 th:text="${user.name}">ユーザー名</h3>
        <p th:text="${user.email}">メールアドレス</p>

        <!-- 非同期アクション -->
        <button th:onclick="'loadUserDetails(' + ${user.id} + ')'">
            詳細を表示
        </button>
    </div>
</div>

<!-- 非同期データ読み込みのスクリプト -->
<script th:inline="javascript">
async function loadUserDetails(userId) {
    try {
        const response = await fetch(`/api/users/${userId}`);
        const data = await response.json();
        updateUserDetails(data);
    } catch (error) {
        console.error('Error:', error);
    }
}
</script>

非同期処理の実装方法

1. WebSocketを使用したリアルタイム更新

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(new RealTimeUpdateHandler(), "/websocket")
               .setAllowedOrigins("*");
    }
}

@Component
public class RealTimeUpdateHandler extends TextWebSocketHandler {

    @Override
    protected void handleTextMessage(WebSocketSession session, 
                                   TextMessage message) throws Exception {
        // メッセージ処理ロジック
        String payload = message.getPayload();
        // 処理結果を送信
        session.sendMessage(new TextMessage("更新完了: " + payload));
    }
}
<!-- リアルタイム更新UI -->
<div id="real-time-updates">
    <div th:each="update : ${updates}" 
         th:id="'update-' + ${update.id}"
         class="update-item">
        <span th:text="${update.content}">更新内容</span>
        <span th:text="${update.timestamp}" 
              class="timestamp">タイムスタンプ</span>
    </div>
</div>

<script th:inline="javascript">
    const socket = new WebSocket('ws://localhost:8080/websocket');

    socket.onmessage = function(event) {
        const update = JSON.parse(event.data);
        addUpdateToUI(update);
    };

    function addUpdateToUI(update) {
        const container = document.getElementById('real-time-updates');
        const updateElement = document.createElement('div');
        updateElement.className = 'update-item';
        updateElement.innerHTML = `
            <span>${update.content}</span>
            <span class="timestamp">${update.timestamp}</span>
        `;
        container.prepend(updateElement);
    }
</script>

2. 非同期タスク処理

@Controller
public class AsyncTaskController {

    @Autowired
    private AsyncTaskService asyncTaskService;

    @PostMapping("/process")
    public String startProcess(@RequestParam("taskId") String taskId, 
                             RedirectAttributes attributes) {
        // 非同期タスクの開始
        CompletableFuture<ProcessResult> future = 
            asyncTaskService.processTask(taskId);

        attributes.addFlashAttribute("taskId", taskId);
        return "redirect:/task/status";
    }

    @GetMapping("/task/status")
    public String checkStatus(@ModelAttribute("taskId") String taskId, 
                            Model model) {
        model.addAttribute("taskId", taskId);
        return "task/status";
    }
}
<!-- task/status.html -->
<div class="task-status-container">
    <h2>タスク状態監視</h2>

    <div id="status-display"
         th:data-task-id="${taskId}"
         class="status-panel">
        <div class="progress-bar">
            <div class="progress" 
                 style="width: 0%"
                 id="task-progress"></div>
        </div>
        <p id="status-message">処理中...</p>
    </div>
</div>

<script th:inline="javascript">
    const taskId = /*[[${taskId}]]*/ '';

    async function pollTaskStatus() {
        try {
            const response = await fetch(`/api/task/${taskId}/status`);
            const status = await response.json();

            updateProgressUI(status);

            if (!status.completed) {
                setTimeout(pollTaskStatus, 1000);
            }
        } catch (error) {
            console.error('Error:', error);
        }
    }

    function updateProgressUI(status) {
        document.getElementById('task-progress').style.width = 
            `${status.progress}%`;
        document.getElementById('status-message').textContent = 
            status.message;
    }

    // ポーリング開始
    pollTaskStatus();
</script>

これらの実装例は、Thymeleafを使用した現代的なWebアプリケーション開発の実践的なアプローチを示しています。次のセクションでは、トラブルシューティングについて説明していきます。

Thymeleafのトラブルシューティング

よくあるエラーとその解決方法

1. テンプレート解決エラー

Template might not exist or might not be accessible

// エラーの例
org.thymeleaf.exceptions.TemplateInputException: Error resolving template "users/list"

// 解決策1:テンプレートパスの設定確認
@Configuration
public class ThymeleafConfig {
    @Bean
    public SpringResourceTemplateResolver templateResolver() {
        SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
        resolver.setPrefix("classpath:/templates/");  // パスの確認
        resolver.setSuffix(".html");                 // 拡張子の確認
        resolver.setTemplateMode(TemplateMode.HTML);
        return resolver;
    }
}

// 解決策2:ファイル配置の確認
// src/main/resources/templates/users/list.html の存在確認

2. 変数アクセスエラー

Property or field not found

<!-- エラーの例 -->
org.thymeleaf.exceptions.TemplateProcessingException: 
Exception evaluating SpringEL expression: "user.firstName"

<!-- 解決策1:nullチェックの追加 -->
<span th:if="${user != null}" th:text="${user.firstName}">名前</span>

<!-- 解決策2:デフォルト値の設定 -->
<span th:text="${user?.firstName ?: '未設定'}">名前</span>

<!-- 解決策3:オブジェクト構造の確認 -->
<div th:object="${user}">
    <span th:text="*{firstName}">名前</span>
</div>

3. 式の構文エラー

<!-- エラーの例 -->
<!-- 誤った構文 -->
<div th:if="user.isAdmin">管理者メニュー</div>

<!-- 正しい構文 -->
<div th:if="${user.isAdmin()}">管理者メニュー</div>
<div th:if="${user.admin}">管理者メニュー</div>

<!-- 複雑な条件式の場合 -->
<div th:if="${user.role == 'ADMIN' and user.enabled}">
    管理者メニュー
</div>

デバッグとログ出力のテクニック

1. デバッグモードの活用

# application.properties
# デバッグモードの有効化
spring.thymeleaf.cache=false
logging.level.org.thymeleaf=DEBUG
logging.level.org.springframework.web=DEBUG

2. エラートレース表示の設定

@Configuration
public class ThymeleafDebugConfig {

    @Bean
    public ThymeleafViewResolver viewResolver() {
        ThymeleafViewResolver resolver = new ThymeleafViewResolver();
        resolver.setTemplateEngine(templateEngine());
        resolver.setCharacterEncoding("UTF-8");

        // デバッグ情報の表示設定
        Map<String, Object> variables = new HashMap<>();
        variables.put("debug", true);  // デバッグフラグ
        resolver.setStaticVariables(variables);

        return resolver;
    }
}

3. カスタムエラーページの実装

<!-- error/404.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>ページが見つかりません</title>
</head>
<body>
    <div class="error-container">
        <h1>404 - ページが見つかりません</h1>

        <!-- エラー詳細(開発環境のみ) -->
        <div th:if="${debug}" class="error-details">
            <p>リクエストURL: <span th:text="${path}">URL</span></p>
            <p>エラーメッセージ: <span th:text="${message}">メッセージ</p>

            <!-- スタックトレース -->
            <div th:if="${trace}" class="stack-trace">
                <pre th:text="${trace}">スタックトレース</pre>
            </div>
        </div>

        <a th:href="@{/}" class="btn-home">ホームに戻る</a>
    </div>
</body>
</html>

4. ログ出力の実装

@Controller
public class DebugDemoController {

    private static final Logger logger = 
        LoggerFactory.getLogger(DebugDemoController.class);

    @GetMapping("/debug-demo")
    public String debugDemo(Model model) {
        try {
            // 処理の開始をログ
            logger.debug("デバッグデモの処理を開始");

            // モデルの状態をログ
            Map<String, Object> modelMap = new HashMap<>();
            model.asMap().forEach((k, v) -> 
                modelMap.put(k, v != null ? v.toString() : "null"));
            logger.debug("現在のモデル状態: {}", modelMap);

            // 処理の実行
            SomeBusinessLogic logic = new SomeBusinessLogic();
            Result result = logic.process();

            // 結果をログ
            logger.info("処理結果: {}", result);
            model.addAttribute("result", result);

        } catch (Exception e) {
            // エラーログ
            logger.error("処理中にエラーが発生: {}", e.getMessage(), e);
            throw e;
        }

        return "debug/demo";
    }
}

デバッグのベストプラクティス

  1. 段階的なデバッグアプローチ
   // 1. コントローラーでの値の確認
   logger.debug("コントローラーに渡された値: {}", value);

   // 2. サービスレイヤーでの処理確認
   logger.debug("サービス処理開始: パラメータ = {}", params);

   // 3. モデル状態の確認
   logger.debug("モデルの状態: {}", model.asMap());
  1. テンプレートでのデバッグ情報表示
   <!-- 開発環境でのみ表示されるデバッグ情報 -->
   <div th:if="${@environment.getActiveProfiles().contains('dev')}">
       <h4>デバッグ情報</h4>
       <pre th:text="${#vars}">変数一覧</pre>
   </div>
  1. エラーハンドリングの実装
   @ControllerAdvice
   public class GlobalErrorHandler {

       private static final Logger logger = 
           LoggerFactory.getLogger(GlobalErrorHandler.class);

       @ExceptionHandler(Exception.class)
       public String handleError(Exception e, Model model) {
           logger.error("予期せぬエラーが発生: {}", e.getMessage(), e);
           model.addAttribute("error", e);
           return "error/general";
       }
   }

これらのトラブルシューティング手法を活用することで、Thymeleafアプリケーションの問題を効率的に特定し解決することができます。次のセクションでは、プロダクション環境での運用ポイントについて説明していきます。

プロダクション環境での運用ポイント

本番環境での設定最適化

1. パフォーマンス設定

# application-prod.properties
# キャッシュ設定
spring.thymeleaf.cache=true
spring.thymeleaf.cache-period=3600
spring.resources.cache.period=3600
spring.resources.chain.strategy.content.enabled=true
spring.resources.chain.strategy.content.paths=/**

# 圧縮設定
server.compression.enabled=true
server.compression.mime-types=text/html,text/css,application/javascript
server.compression.min-response-size=1024

# コネクションプール設定
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.idle-timeout=300000

2. セキュリティ設定

@Configuration
@Profile("prod")
public class ProductionSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .requiresChannel()
                .anyRequest().requiresSecure()  // HTTPS強制
            .and()
            .headers()
                .contentSecurityPolicy("default-src 'self'")
                .frameOptions().deny()
                .xssProtection().block(true)
            .and()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                .maximumSessions(1)  // 同時セッション数制限
            .and()
            .csrf()
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
    }
}

3. エラーハンドリング設定

@ControllerAdvice
@Profile("prod")
public class ProductionErrorHandler {

    @ExceptionHandler(Exception.class)
    public String handleError(Exception e, Model model) {
        // エラーページへの最小限の情報提供
        model.addAttribute("errorCode", "E-SYSTEM");
        model.addAttribute("errorMessage", "システムエラーが発生しました");

        return "error/production";
    }
}

モニタリングと保守の重要ポイント

1. メトリクス収集の実装

@Configuration
@Profile("prod")
public class MonitoringConfig {

    @Bean
    public MeterRegistry meterRegistry() {
        return new SimpleMeterRegistry();
    }

    @Bean
    public TimedAspect timedAspect(MeterRegistry registry) {
        return new TimedAspect(registry);
    }
}

@Component
public class ThymeleafMetricsAspect {

    private final MeterRegistry registry;

    public ThymeleafMetricsAspect(MeterRegistry registry) {
        this.registry = registry;
    }

    @Around("execution(* org.thymeleaf.TemplateEngine.process(..))")
    public Object measureTemplateProcessing(ProceedingJoinPoint joinPoint) throws Throwable {
        Timer.Sample sample = Timer.start(registry);
        try {
            return joinPoint.proceed();
        } finally {
            sample.stop(registry.timer("thymeleaf.template.processing"));
        }
    }
}

2. ヘルスチェックの実装

@Component
public class ThymeleafHealthIndicator implements HealthIndicator {

    private final TemplateEngine templateEngine;

    public ThymeleafHealthIndicator(TemplateEngine templateEngine) {
        this.templateEngine = templateEngine;
    }

    @Override
    public Health health() {
        try {
            // テンプレートエンジンの状態確認
            Context context = new Context();
            context.setVariable("test", "health-check");
            templateEngine.process("fragments/health-check", context);

            return Health.up()
                .withDetail("status", "Template engine is working")
                .build();

        } catch (Exception e) {
            return Health.down()
                .withException(e)
                .build();
        }
    }
}

3. ログ管理の実装

<!-- logback-spring.xml -->
<configuration>
    <springProfile name="prod">
        <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <file>logs/application.log</file>
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <fileNamePattern>logs/application.%d{yyyy-MM-dd}.log</fileNamePattern>
                <maxHistory>30</maxHistory>
                <totalSizeCap>3GB</totalSizeCap>
            </rollingPolicy>
            <encoder>
                <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
            </encoder>
        </appender>

        <root level="INFO">
            <appender-ref ref="FILE" />
        </root>
    </springProfile>
</configuration>

4. パフォーマンスモニタリング

@Component
@Profile("prod")
public class PerformanceMonitor {

    private final MeterRegistry registry;
    private final Logger logger = LoggerFactory.getLogger(PerformanceMonitor.class);

    public PerformanceMonitor(MeterRegistry registry) {
        this.registry = registry;

        // メモリ使用量の監視
        Gauge.builder("jvm.memory.used", Runtime.getRuntime(), 
            runtime -> runtime.totalMemory() - runtime.freeMemory())
            .register(registry);

        // テンプレート処理時間の監視
        Timer.builder("template.processing")
            .publishPercentiles(0.5, 0.95, 0.99)
            .register(registry);
    }

    @Scheduled(fixedRate = 60000)  // 1分ごとに実行
    public void reportMetrics() {
        // メトリクスのログ出力
        registry.getMeters().forEach(meter -> {
            logger.info("Metric: {} = {}", 
                meter.getId(), 
                meter.measure().iterator().next().getValue());
        });
    }
}

運用管理のベストプラクティス

  1. 定期的なメンテナンス計画
  • テンプレートキャッシュの定期的なクリア
  • 不要なセッションの削除
  • ログローテーション
  1. スケーリング戦略
  • 負荷に応じたインスタンス数の調整
  • セッション管理の分散化
  • キャッシュの分散化
  1. バックアップ戦略
  • テンプレートファイルの定期バックアップ
  • 設定ファイルのバージョン管理
  • ログファイルのアーカイブ

これらの運用ポイントを適切に実装することで、安定したプロダクション環境を維持することができます。次のセクションでは、Thymeleafの今後の展望について説明していきます。

Thymeleafの今後の展望

最新バージョンの新機能と改善点

1. パフォーマンスの向上

Thymeleaf 3.xシリーズでは、以下の改善が実装されています:

// 新しいパーサーエンジンの活用例
@Configuration
public class ThymeleafConfig {

    @Bean
    public SpringTemplateEngine templateEngine() {
        SpringTemplateEngine engine = new SpringTemplateEngine();
        // 新パーサーの設定
        engine.setEnableSpringELCompiler(true);

        // パフォーマンス最適化設定
        Set<IDialect> dialects = new HashSet<>();
        dialects.add(new SpringStandardDialect());
        engine.setDialects(dialects);

        return engine;
    }
}

2. モダンな機能の統合

<!-- リアクティブな要素の統合 -->
<div th:fragment="reactive-content" th:async-supported="true">
    <div th:each="item : ${reactiveData}" 
         th:with="result=${item.block()}">
        <span th:text="${result}">データ</span>
    </div>
</div>

<!-- モダンなWeb機能のサポート -->
<template th:fragment="dynamic-import">
    <script type="module" th:src="@{/js/module.js}"></script>
    <link rel="modulepreload" th:href="@{/js/dependency.js}">
</template>

マイクロサービスアーキテクチャでの活用方法

1. マイクロフロントエンド統合

@Configuration
public class MicroFrontendConfig {

    @Bean
    public FragmentRegistry fragmentRegistry() {
        FragmentRegistry registry = new FragmentRegistry();

        // マイクロフロントエンドの登録
        registry.register("header", "http://ui-service/fragments/header");
        registry.register("footer", "http://ui-service/fragments/footer");

        return registry;
    }
}
<!-- マイクロフロントエンドの統合例 -->
<div th:replace="${@fragmentRegistry.resolve('header')}">
    ヘッダー領域
</div>

<main>
    <!-- メインコンテンツ -->
    <div th:replace="local/content :: content">
        コンテンツ領域
    </div>
</main>

<div th:replace="${@fragmentRegistry.resolve('footer')}">
    フッター領域
</div>

2. APIゲートウェイとの連携

@Service
public class ApiGatewayService {

    private final WebClient webClient;

    public ApiGatewayService(WebClient.Builder webClientBuilder) {
        this.webClient = webClientBuilder
            .baseUrl("http://api-gateway")
            .build();
    }

    public Mono<UserData> getUserData(String userId) {
        return webClient.get()
            .uri("/users/{id}", userId)
            .retrieve()
            .bodyToMono(UserData.class);
    }
}
<!-- APIゲートウェイからのデータ表示 -->
<div th:with="userData=${@apiGatewayService.getUserData(userId).block()}">
    <h2 th:text="${userData.name}">ユーザー名</h2>
    <div th:each="service : ${userData.services}">
        <span th:text="${service.name}">サービス名</span>
    </div>
</div>

3. 将来的な展望

  1. Webコンポーネントとの統合
<!-- カスタムエレメントのサポート -->
<custom-element th:data="${someData}">
    <div slot="content">
        <span th:text="${someData.value}">値</span>
    </div>
</custom-element>

<!-- シャドウDOMサポート -->
<template id="shadow-template" th:fragment="shadow-content">
    <style>
        :host { display: block; }
    </style>
    <div>
        <slot name="content"></slot>
    </div>
</template>
  1. サーバーサイドレンダリングの進化
@Configuration
public class ModernRenderingConfig {

    @Bean
    public HybridTemplateEngine templateEngine() {
        HybridTemplateEngine engine = new HybridTemplateEngine();

        // ハイブリッドレンダリングの設定
        engine.setStreamingEnabled(true);
        engine.setAsyncSupported(true);

        return engine;
    }
}
  1. AIと機械学習の統合
<!-- AI支援型テンプレート生成の例 -->
<div th:fragment="ai-assisted" 
     th:with="suggestion=${@aiService.getSuggestion(content)}">

    <!-- AIが提案するレイアウト -->
    <div th:replace="${suggestion.layout}">
        レイアウト提案
    </div>

    <!-- AI生成コンテンツ -->
    <div th:text="${suggestion.content}">
        最適化されたコンテンツ
    </div>
</div>

移行戦略とベストプラクティス

  1. 段階的な機能更新
  • 既存のテンプレートは維持しながら、新機能を段階的に導入
  • 互換性を保ちながらのアップグレード
  • パフォーマンス改善の継続的な適用
  1. モダン化への対応
  • Web ComponentsやShadow DOMへの対応準備
  • マイクロフロントエンドアーキテクチャへの移行計画
  • リアクティブプログラミングモデルの採用
  1. 将来への備え
  • AIと機械学習の活用準備
  • クラウドネイティブ環境への適応
  • パフォーマンスとスケーラビリティの向上

これらの展望と戦略を意識することで、Thymeleafを使用したアプリケーションを将来にわたって持続可能な形で発展させることができます。