必要最小限のサンプルでThymeleafを完全マスター

この記事は?

Java EE Advent Calendar 2016 - Qiitaの12日目です。

昨日の記事は@yyYankさんの「どうすんのJava EE - Javaプログラマのはしくれダイアリー」でした。明日は@n_agetsuさんです。

Thymeleafは、Javaで作られたテンプレートエンジンです。JSPの代替技術として近年注目されていて、JJUG CCCなどで話を聞いていても、利用事例が増えているように感じます。

ブログ情報も多く、検索すると「チートシート」のようなブログがいっぱい出てきます。

ただ、Thymeleafは多機能なのでチートシートもボリュームがあり、Thymeleafを初めて学習する人にはちょっと重たいなあ・・・と感じていました。

そこで今回は、JSPから移行したい方が、まず最初に理解すべき必要最低限の項目をまとめました。

アジェンダ

  • 環境準備
  • 4つの記法
    • リンク式(@{...}
    • メッセージ式(#{...}
    • 変数式(${...}
    • 選択変数式(*{...}
  • テンプレートの記述
    • 条件分岐(th:ifth:unless
    • 繰り返し(th:each
  • その他
    • ユーテリティオブジェクト(#listsなど)
    • コメント(<!--/* */-->

サンプルコードはGitHubに公開しています。

GitHub - MasatoshiTada/beginning-thymeleaf

環境準備

プロジェクトの作成

MavenまたはGradleを使います。(下記はMavenの例です)

    <dependencies>
        ...
        <dependency>
            <groupId>org.thymeleaf</groupId>
            <artifactId>thymeleaf</artifactId>
            <version>3.0.2.RELEASE</version>
            <scope>compile</scope>
        </dependency>
        ...
    </dependencies>

TemplateResolverTemplateEngineの生成

今回は、サーブレットが1つ、Thymeleafのテンプレートが1つだけのシンプルなプログラムです。

Thymeleafの中心的なクラスが、TemplateResolverTemplateEngineです。

TemplateResolverクラスは、指定された論理的なビュー名(例:"hello")から、物理的なビュー名(例:"/WEB-INF/views/hello.html")を解決する役割を担います。

TemplateEngineクラスは、作成されたテンプレートを元に、実際にレスポンスするHTMLを出力する役割を担います。

まずは、上記2つのインスタンスを生成しておく必要があります。これらのインスタンスは1つずつ生成して、以降の処理では使い回せばOKです。

今回は、サーブレットクラスの初期化処理の中で生成します。

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {

    // Thymeleafの出力を書き出すクラス
    private TemplateEngine templateEngine;

    /**
     * サーブレットの初期化処理。
     * TemplateResolverおよびTemplateEngineを生成する。
     */
    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);

        // TemplateResolverの生成
        ServletContext servletContext = config.getServletContext();
        ServletContextTemplateResolver templateResolver =
                new ServletContextTemplateResolver(servletContext);
        templateResolver.setPrefix("/WEB-INF/views/"); // ビューの保存フォルダ
        templateResolver.setSuffix(".html"); // ビューの拡張子
        templateResolver.setTemplateMode(TemplateMode.HTML);

        // TemplateEngineの生成
        templateEngine = new TemplateEngine();
        templateEngine.setTemplateResolver(templateResolver);
    }
    
    // その他のメソッド省略
}

今回はサーブレットが1つなので、サーブレットの初期化処理で書きましたが、本来のアプリケーション開発であれば、何らかの方法でシングルトンにしておくと良いでしょう。

SpringだったらBean定義すればいいですし*1Java EEならば@ApplicationScopedなプロデューサーメソッドとして作ってもいいでしょう。(詳細は最後の参考資料をご覧ください)

4つの記法

さて、ここから具体的なThymeleafによるテンプレート記述に入ります。

Thymeleafには、大きく分けて4つの記法があります。

リンク式(@{...}

HTMLとかJSPを書いていてけっこう面倒なのが、リンク/CSS/JS/画像などの「パスの記述」ではないでしょうか。

絶対パスで指定するためにコンテキストパスを毎回書いたり、相対パスで書くとプロジェクトのフォルダ構造と、サーバー上で実行される時のパスが違っていて、うまくパスを指定するのが難しかったりします。

こういった問題の対策として、Thymeleafにはリンク式という記法があります。

  • テンプレート
    <link rel="stylesheet" href="../../css/style.css" th:href="@{/css/style.css}">

th:href属性の@{...}の部分がリンク式です。/で始めると、レスポンスされるHTMLには、なんとコンテキストパスが補完されます!

href属性は、サーバーを通さずに直接ブラウザでHTMLを開いたときに利用されます。そして、サーバー上で実行したときには、href属性の値はth:href属性の値で上書きされます。

Thymeleafには、th:xxxという形式の属性がいっぱいあるのですが、基本的にはxxxという属性の値を上書きするものです。

  • レスポンスされるHTML
    <link rel="stylesheet" href="/beginning-thymeleaf/css/style.css">

なぜ、上書きされて消えてしまうhref属性を書くのかというと、直接ブラウザでHTMLを開いてもCSSが適用されるようにするためです。

こうすると、テンプレートをそのままブラウザで開いて、お客様に見せて打ち合わせなどができます。

本番用のソースコードがそのまま画面モックになるのが、Thymeleaf最大の特徴です。

メッセージ式(#{...}

Thymeleafでは、画面上のメッセージをプロパティファイルなどに記述し、国際化ができます。

やり方は簡単で、テンプレートの.htmlファイルと同じフォルダに、「テンプレート名.properties」というファイルを作るだけです。

例えば、hello.htmlに対するプロパティファイルは「hello.properties」「hello_ja.properties」などです。

言語は、クライアントからのAccept-LanguageHTTPリクエストヘッダで指定します。

指定された言語に対応したプロパティファイルがなかった場合は、ロケールなしの「hello.properties」が使われます。

  • hello.properties
backToTop=Back to top
nothingToShow=No users to show.
  • hello_ja.properties
backToTop=トップページに戻る
nothingToShow=表示するユーザーがありません。
  • hello.html(テンプレート)
<p th:text="#{nothingToShow}">表示するユーザーがありません。</p>

<a href="../../index.html" th:href="@{/}" th:text="#{backToTop}">トップページに戻る</a>

#{...}の部分がメッセージ式です。「nothingToShow」や「backToTop」は、プロパティファイルのキーを指定しています。

th:text属性は、画面にメッセージを表示するために使われる属性です。

サーバーで実行した時は、th:text属性を指定したタグに挟まれた部分が置き換えられます。

テンプレートに記述している「表示するユーザーがありません。」などのメッセージは、ブラウザで直接開いたときのための仮のメッセージで、サーバーで実行時はプロパティファイルのメッセージで上書きされます。

変数式(${...}

サーブレット側で、テンプレートに値を渡す処理は、こんな感じで書きます。

List<User> userList = ...;

// ビューに渡す値を保存するマップ
HashMap<String, Object> map = new HashMap<>();
map.put("userList", userList);

// レスポンスするHTMLを書き出す
WebContext webContext = new WebContext(request, response,
        getServletContext(), request.getLocale());
webContext.setVariables(map); // 値をビューに渡す
Writer writer = new OutputStreamWriter(
        response.getOutputStream(), StandardCharsets.UTF_8);
templateEngine.process("hello", webContext, writer); // /WEB-INF/views/hello.htmlを書き出す

少し面倒なコードに見えますが、このあたりはフレームワーク化すれば、毎回書く必要は無くなります。

この値をテンプレートで表示するには、変数式を使います。

  • テンプレート
<tr th:each="user : ${userList}">
    <td th:text="${user.id}">111</td>
    <td th:text="${user.name}">Yumi Wakatsuki</td>
</tr>

th:eachは繰り返しを記述するもので、後述します)

${...}の部分が変数式です。ほぼJSPのELと同じですね。

ここもth:textを使っているので、タグで挟まれた部分(「111」など)は、サーバー上で実行されたときには置き換えられます。

  • レスポンスされるHTML
<tr>
    <td>1</td>
    <td>User1</td>
</tr>
<tr>
    <td>2</td>
    <td>User2</td>
</tr>
<tr>
    <td>3</td>
    <td>User3</td>
</tr>

選択変数式(*{...}

先ほどのユーザーの表示の部分ですが、userというのをIDと名前で2回書いています。

これを簡略化できるのが、選択変数式です。

  • テンプレート
<tr th:each="user : ${userList}" th:object="${user}">
    <td th:text="*{id}">111</td>
    <td th:text="*{name}">Yumi Wakatsuki</td>
</tr>

上記の例だと、<tr>開始タグにth:object=${user}と書いています。

こうすると、<tr>で挟んでいる部分では、${user.id}ではなく*{id}と書くことができます。これが選択変数式です。

レスポンスされるHTMLは、変数式の場合とまったく同じになります。

  • レスポンスされるHTML
<tr>
    <td>1</td>
    <td>User1</td>
</tr>
<tr>
    <td>2</td>
    <td>User2</td>
</tr>
<tr>
    <td>3</td>
    <td>User3</td>
</tr>

テンプレートの記述

テンプレートでは欠かせない、条件分岐・繰り返しについて説明します。

条件分岐(th:ifth:unless

JSPで言うところの<c:if>に相当します。

<c:choose>に相当するものはありません。つまり、ifに対するelseみたいな記述はできません。

  • テンプレート
<table border="1" th:unless="${#lists.isEmpty(userList)}">
    ...
</table>

<p th:if="${#lists.isEmpty(userList)}">表示するユーザーがありません。</p>

#listsの部分はユーティリティオブジェクトというもの(後述)なのですが、ざっくり言うと、userListが空でなかったら<table>要素の部分のみが、空だったら<p>要素の部分のみが出力されます。

th:unlessは変数式で指定した条件がfalseの時に出力され、th:ifは変数式で指定した条件がtrueの時に出力されます。

  • レスポンスされるHTML(userListが空でない場合)
<table border="1">
    ...
</table>
  • レスポンスされるHTML(userListが空の場合)
<p>表示するユーザーがありません。</p>

条件を指定するには、普通のJavaのように==!=などを使うことができます。

不等号については、<>ではなく、lt<相当)、gt>相当)、le<=相当)、ge>=相当)、を使います。

andorで、複数の条件を指定することも可能です。

実は、true/falseだけでなく、下記のものはfalseと判断されます。逆に言うと、falseと下記以外は全てtrueと判断されます。

  • null
  • 0
  • "0"
  • “false”
  • “off”
  • “no”

これは、検索結果などを表示する時に非常に便利です。null比較をしなくて済みますね!

  • テンプレートの例(このコードはGitHubにはありません)
<!--/* th:if="${user != null}" と同じ */-->
<table border="1" th:if="${user}">
    <tr>
        <td th:text="*{id}">111</td>
        <td th:text="*{name}">Yumi Wakatsuki</td>
    </tr>
</table>

繰り返し(th:each

前にも少し出てきましたが、繰り返しはth:eachで表します。

下記の場合だと、th:each属性を記述している<tr>要素自体が繰り返し出力されます。

構文はth:each="コレクションから取り出した要素の変数名 : ${コレクションの変数名}">です。

  • テンプレート
<tr th:each="user : ${userList}" th:object="${user}">
    <td th:text="*{id}">111</td>
    <td th:text="*{name}">Yumi Wakatsuki</td>
</tr>
  • レスポンスされるHTML
<tr>
    <td>1</td>
    <td>User1</td>
</tr>
<tr>
    <td>2</td>
    <td>User2</td>
</tr>
<tr>
    <td>3</td>
    <td>User3</td>
</tr>

僕もたまにやってしまうのですが、<c:forEach>の感覚で使っていると、こんな書き方をしちゃいます。

  • テンプレート
<table th:each="user : ${userList}">
    <tr th:object="${user}">
        <td th:text="*{id}">111</td>
        <td th:text="*{name}">Yumi Wakatsuki</td>
    </tr>
</table>

こう書いてしまうと、<table>要素自体が繰り返されるので注意してください。th:eachは、あくまで繰り返したい要素自身に付加します。

その他の記法

ユーテリティオブジェクト(#listsなど)

変数式などの中で、ユーティリティオブジェクトを呼ばれるものを使うことができます。

ユーテリティオブジェクトは、変数式などの中で#オブジェクト名.メソッド名(引数)で利用します。

Thymeleafには、あらかじめいくつかのユーティティオブジェクトが定義されています。

#listsはその1つで、リストに関するメソッドをいくつか持っています。

#lists.isEmpty()メソッドは、引数で渡されたリストが、nullまたは要素数ゼロの場合にtrueを返します。

下記の場合は、th:unlessと組み合わせて、userListが「nullまたは要素数ゼロ」でない場合に<table>要素を出力しています。

  • テンプレート
<table border="1" th:unless="${#lists.isEmpty(userList)}">
    ...
</table>
  • レスポンスされるHTML
<table border="1">
    ...
</table>

他にも色々なメソッドやユーティティオブジェクトがあるので、詳細は公式ドキュメントの下記のページを確認してみてください。

http://www.thymeleaf.org/doc/tutorials/2.1/usingthymeleaf_ja.html#appendix-b-expression-utility-objects

ちなみに、ユーティティオブジェクトは自作も可能です。

多分、SpringでThymeleafを使っていると、#fieldsというユーティティオブジェクトが見つかると思います。

これは素のThymeleafのものではなく、thymeleaf-spring4*2というライブラリ側が提供しているユーティティオブジェクトです。

コメント(<!--/* */-->

普通のHTMLコメント(<!-- -->)とすると、レスポンスされるHTMLにこのコメントが含まれてしまいます。

レスポンスされるHTMLに含めたくないコメントは、<!--/* */-->で書きます。

Thymeleafは、<!--/*から*/-->までをコメントとして認識し、テンプレートを解釈する時にこれらで挟まれた部分を無視します。

これを応用すると、こんなことが可能です。

<tr th:each="user : ${userList}" th:object="${user}">
    <td th:text="*{id}">111</td>
    <td th:text="*{name}">Yumi Wakatsuki</td>
</tr>
<!--/*-->
<tr>
    <td>222</td>
    <td>Reika Sakurai</td>
</tr>
<tr>
    <td>333</td>
    <td>Erika Ikuta</td>
</tr>
<!--*/-->

th:eachで繰り返しを書く時、ブラウザで直接開いた時も、複数件のデータが見えるようにします。

上記で言うと、222番や333番の人は、ブラウザで直接開いたときは見えるのですが、サーバー上で実行したときは、<!--/**/-->で挟まれているので、この部分は無視されます。

さらに勉強するための参考資料

以上、最低限必要な機能を紹介していきました。

Thymeleafに興味を持って、さらに勉強されたい方は、下記の資料を読んでみましょう。

まずは、@bufferingsさんの資料を読んで、一通りの機能を把握しましょう。

Welcome Thymeleaf 3.0! #jjug_ccc #ccc_f2 // Speaker Deck

合わせて、公式ドキュメントも確認しておきましょう。

Tutorial: Using Thymeleaf

@bufferingsさんが翻訳された日本語版もあります。Thymeleaf 2.xの頃のドキュメントですが、あまり大きくは変わっていません。

Tutorial: Using Thymeleaf (ja)

SpringでThymeleafを使うという人は、「Spring徹底入門」を読みましょう。

Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発(株式会社NTTデータ) | 翔泳社の本

Java EEで Thymeleafを使うという人は、僕のスライドを読んでみてください。

Java EEアクションベースMVC入門 #jjug_ccc #ccc_cd4 // Speaker Deck

それでは、Enjoy Thymeleaf!!

*1:正確には、Thymeleaf-Spring4というライブラリが提供しているクラスをBean定義します。詳しくはhttps://github.com/MasatoshiTada/spring4-thymeleaf/blob/master/src/main/java/com/example/web/WebMvcConfig.java

*2:ちなみに、thymeleaf-spring4はThymeleaf作者が自ら開発されています