必要最小限のサンプルでThymeleafを完全マスター
この記事は?
Java EE Advent Calendar 2016 - Qiitaの12日目です。
昨日の記事は@yyYankさんの「どうすんのJava EE - Javaプログラマのはしくれダイアリー」でした。明日は@n_agetsuさんです。
Thymeleafは、Javaで作られたテンプレートエンジンです。JSPの代替技術として近年注目されていて、JJUG CCCなどで話を聞いていても、利用事例が増えているように感じます。
ブログ情報も多く、検索すると「チートシート」のようなブログがいっぱい出てきます。
ただ、Thymeleafは多機能なのでチートシートもボリュームがあり、Thymeleafを初めて学習する人にはちょっと重たいなあ・・・と感じていました。
そこで今回は、JSPから移行したい方が、まず最初に理解すべき必要最低限の項目をまとめました。
アジェンダ
- 環境準備
- 4つの記法
- リンク式(
@{...}
) - メッセージ式(
#{...}
) - 変数式(
${...}
) - 選択変数式(
*{...}
)
- リンク式(
- テンプレートの記述
- 条件分岐(
th:if
、th: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>
TemplateResolver
とTemplateEngine
の生成
今回は、サーブレットが1つ、Thymeleafのテンプレートが1つだけのシンプルなプログラムです。
Thymeleafの中心的なクラスが、TemplateResolver
とTemplateEngine
です。
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定義すればいいですし*1、 Java 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-Language
HTTPリクエストヘッダで指定します。
指定された言語に対応したプロパティファイルがなかった場合は、ロケールなしの「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:if
、th: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
(>=
相当)、を使います。
and
やor
で、複数の条件を指定することも可能です。
実は、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>
他にも色々なメソッドやユーティティオブジェクトがあるので、詳細は公式ドキュメントの下記のページを確認してみてください。
ちなみに、ユーティティオブジェクトは自作も可能です。
多分、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
合わせて、公式ドキュメントも確認しておきましょう。
@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作者が自ら開発されています