Javaでテンプレートエンジンを使おうとしてFreeMarkerにたどり着いたものの、カンマが1つ足りないだけで容赦なくエラー画面が表示され、指定した変数が存在しないだけで処理が止まってしまう。そんな経験をした人は少なくないはずです。
「なんとなく雰囲気で書けば動く」という甘えがまったく通用しないのがFreeMarkerの特徴です。裏を返せば、構文さえきちんと押さえてしまえば非常に予測可能で、意図しない挙動に悩まされにくいテンプレートエンジンでもあります。
この記事では、FreeMarkerの基本構文を、変数展開・条件分岐・ループ・マクロ・エラー対処までできるだけ網羅的に、実際のコードとあわせて解説していきます。長い記事になりますが、リファレンスとしてブックマークしておける内容を目指しています。
この記事のポイント
- 変数展開・条件分岐・ループなど、プログラミングで必要な基本構文をひと通り実例付きで紹介する
- FreeMarkerは構文に非常に厳格で、存在しない変数や記号の抜けを容赦なくエラーにする点を踏まえて解説する
- 実際によく遭遇するエラーメッセージとその原因・対処法もあわせてまとめる
FreeMarkerとは何か
FreeMarkerは、Javaで動作するテンプレートエンジンです。HTMLメールの本文、Webページ、設定ファイル、コード生成など、「決まった雛形(テンプレート)」に「動的なデータ」を流し込んで最終的な出力を作る場面で使われます。Javaのプログラム側でデータを用意し、そのデータをテンプレートファイル(拡張子は一般的に .ftl)に渡すことで、変数の値が埋め込まれた文書が生成されます。
Java側の基本的な組み込みコード
テンプレートを実際に処理するJava側のコードは、おおよそ次のような形になります。Configurationでテンプレートの読み込み元を設定し、Templateオブジェクトを取得して、データモデル(Map)と一緒に処理を実行します。
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateExceptionHandler;
import java.io.File;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;
public class FreemarkerSample {
public static void main(String[] args) throws Exception {
// Configurationはアプリ全体で1つ使い回すのが推奨
Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
cfg.setDirectoryForTemplateLoading(new File("src/main/resources/templates"));
cfg.setDefaultEncoding("UTF-8");
cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
// テンプレートファイルの取得(hello.ftl)
Template template = cfg.getTemplate("hello.ftl");
// データモデル(テンプレートに渡す変数の集まり)
Map<String, Object> dataModel = new HashMap<>();
dataModel.put("userName", "田中");
dataModel.put("itemCount", 3);
// テンプレートを処理して結果を出力
StringWriter writer = new StringWriter();
template.process(dataModel, writer);
System.out.println(writer.toString());
}
}
ここでいうデータモデルとは、テンプレート内で使える変数をまとめたMapのことです。テンプレート側では、このデータモデルに存在するキーだけを変数として参照できます。存在しないキーを参照しようとすると、これから説明する通りエラーになります。
テンプレート内の基本ルール:3つの記法
FreeMarkerのテンプレートファイルの中には、大きく分けて3種類の書き方が登場します。
| 記法 | 書き方 | 用途 |
|---|---|---|
| 補間(変数展開) | ${変数名} |
変数の値をそのまま出力に埋め込む |
| FTLディレクティブ | <#if>、<#list> など |
条件分岐やループなど、処理の制御を行う |
| コメント | <#-- コメント --> |
出力には一切含まれないメモ書き |
この3つを混同すると構文エラーの原因になります。「値を表示したいだけなら${ }」「処理を制御したいなら<# >」という役割分担をまず押さえておきましょう。
変数展開:${...}の基本
もっとも基本的な使い方は、データモデルに入れた変数をそのまま出力することです。
<p>ようこそ、${userName}さん。</p>
<p>ご注文数:${itemCount}件</p>
先ほどのJavaコードのデータモデルと組み合わせると、次のように出力されます。
<p>ようこそ、田中さん。</p>
<p>ご注文数:3件</p>
ネストしたオブジェクト(Mapの中にMapがある場合)は、ドット記法でたどることができます。
<p>郵便番号:${user.address.zipCode}</p>
<p>都道府県:${user.address.prefecture}</p>
条件分岐:<#if> <#elseif> <#else>
条件分岐は<#if>で開始し、必ず</#if>で閉じます。閉じタグを忘れることが初心者の定番のミスなので注意してください。
<#if itemCount > 10>
<p>大口注文です。</p>
<#elseif itemCount > 0>
<p>通常の注文です。</p>
<#else>
<p>注文がありません。</p>
</#if>
比較演算子や論理演算子は、一般的なプログラミング言語とほぼ同じ感覚で使えます。
| 演算子 | 意味 | 例 |
|---|---|---|
== / != |
等しい/等しくない | status == "OK" |
> / >= / < / <= |
大小比較 | price >= 1000 |
&& / || / ! |
論理積・論理和・否定 | isActive && !isDeleted |
なお、<を条件式の中でそのまま使うとFreeMarkerがFTLディレクティブの開始と誤解することがあるため、山括弧を使う比較には<#if price lt 1000>のように、lt(less than)、gt(greater than)、lte、gteといった英字表記の演算子を使うのが安全です。
<#if price lt 1000>
<p>お手頃価格です。</p>
</#if>
ループ処理:<#list>
配列やリストを繰り返し処理する場合は<#list>を使います。基本形は「リスト as 各要素の変数名」です。
<ul>
<#list itemList as item>
<li>${item.name}(${item.price}円)</li>
</#list>
</ul>
ループの中では、item_index(0始まりのインデックス)やitem_has_next(次の要素があるかどうか)といった特殊なループ変数が自動的に使えます。要素名の末尾に_indexや_has_nextを付けるだけです。
<ol>
<#list itemList as item>
<li>
No.${item_index + 1}:${item.name}
<#if item_has_next>(次があります)</#if>
</li>
</#list>
</ol>
リストが空の場合の表示も<#else>で指定できます。
<#list itemList as item>
<p>${item.name}</p>
<#else>
<p>商品がありません。</p>
</#list>
ハッシュ(Map)のループ
MapのキーとValueをまとめてループしたい場合は、?keysや?valuesを使ってシーケンス(配列のようなもの)に変換します。
<#list priceMap?keys as key>
<p>${key}:${priceMap[key]}円</p>
</#list>
Null・存在しない変数への対策
ここがFreeMarkerを使う上で最も重要なポイントです。データモデルに存在しない変数を${ }でそのまま参照すると、テンプレート処理は例外を投げて停止します。「値が空文字として表示される」といった甘い挙動は基本的にありません。
<#-- middleNameがデータモデルに存在しない場合 -->
<p>${middleName}</p>
<#-- 実行結果はエラー -->
<#-- freemarker.core.InvalidReferenceException: -->
<#-- The following has evaluated to null or missing: -->
<#-- ==> middleName [in template "hello.ftl" at line 3, column 5] -->
この挙動を回避するために、FreeMarkerには「欠損値演算子」が用意されています。代表的なのが!(デフォルト値指定)と??(存在チェック)です。
| 記法 | 意味 |
|---|---|
${middleName!""} |
値がnull・存在しない場合に空文字を代わりに使う |
${middleName!"未設定"} |
値がない場合に「未設定」という文字列を表示する |
<#if middleName??> |
値が存在するかどうかだけを判定する |
<#if middleName??>
<p>ミドルネーム:${middleName}</p>
<#else>
<p>ミドルネームは登録されていません。</p>
</#if>
<#-- 1行で済ませたい場合 -->
<p>ミドルネーム:${middleName!"未登録"}</p>
ネストした階層で欠損の可能性がある場合は、途中経路にも!や??を挟む必要があります。user.address.zipCodeのうちaddress自体が存在しない可能性があるなら、次のように書きます。
<p>郵便番号:${(user.address.zipCode)!"未登録"}</p>
よく使うビルトイン関数(?で始まる機能)
FreeMarkerには変数の後ろに?関数名を付けることで、値を加工できるビルトイン関数が豊富に用意されています。代表的なものを分野別にまとめます。
文字列操作
| 記法 | 説明 |
|---|---|
${name?upper_case} |
大文字に変換 |
${name?lower_case} |
小文字に変換 |
${name?trim} |
前後の空白を除去 |
${name?length} |
文字数を取得 |
${text?replace("旧", "新")} |
文字列の置換 |
${text?html} |
HTML特殊文字をエスケープ |
数値・日付のフォーマット
<#-- 数値のフォーマット -->
<p>価格:${price?string("#,##0")}円</p>
<p>小数:${rate?string("0.00")}</p>
<#-- 日付のフォーマット(データモデル側でjava.util.Dateなどを渡している前提) -->
<p>登録日:${registeredAt?string("yyyy年MM月dd日")}</p>
シーケンス(配列・リスト)操作
<p>件数:${itemList?size}</p>
<#-- 先頭のみ / 並び替え -->
<#list itemList?sort_by("price") as item>
<p>${item.name}:${item.price}円</p>
</#list>
変数の代入:<#assign>
テンプレート内で新しい変数を定義したり、既存の値を加工して一時変数に入れたりする場合は<#assign>を使います。
<#assign totalPrice = itemList?size * 100>
<p>合計目安:${totalPrice}円</p>
<#assign greeting>
こんにちは、${userName}さん。本日もご利用ありがとうございます。
</#assign>
<p>${greeting}</p>
なお、マクロやループの内部で定義した変数のスコープを外側に漏らしたくない場合は<#local>、逆にどこからでも参照できるグローバル変数にしたい場合は<#global>を使い分けます。
マクロ:処理の再利用
同じような表示パターンを何度も繰り返す場合は、マクロとして定義しておくと便利です。プログラミング言語における関数に近い役割を持ちます。
<#macro itemCard name price>
<div class="item-card">
<h3>${name}</h3>
<p>${price}円</p>
</div>
</#macro>
<#-- 呼び出し側 -->
<#list itemList as item>
<@itemCard name=item.name price=item.price />
</#list>
マクロの中に囲み要素を渡したい場合は<#nested>を使います。
<#macro panel title>
<div class="panel">
<h2>${title}</h2>
<#nested>
</div>
</#macro>
<@panel title="注意事項">
<p>こちらの内容は必ずお読みください。</p>
</@panel>
関数:戻り値が必要な場合
表示ではなく「値を計算して返す」ことだけを目的とするなら、マクロではなく<#function>を使います。
<#function calcTax price rate>
<#return price * rate>
</#function>
<p>税額:${calcTax(1000, 0.1)}円</p>
テンプレートの分割:<#include>と<#import>
共通のヘッダーやフッターなど、複数のテンプレートで使い回したい部分は別ファイルに切り出せます。
<#-- header.ftlの中身をそのまま展開する -->
<#include "common/header.ftl">
<p>本文の内容</p>
<#include "common/footer.ftl">
マクロや関数を定義したファイルを名前空間として読み込みたい場合は<#import>を使います。
<#import "common/macros.ftl" as m>
<@m.itemCard name="サンプル商品" price=1200 />
空白・改行の制御
ディレクティブの行を書くと、そのままでは不要な空行や余分な空白が出力に混ざることがあります。行末に<#t>(トリム)を付けると、その行の前後の空白を取り除けます。
<#list itemList as item><#t>
${item.name}<#t>
</#list>
出力される見た目を細かく調整したい場合に覚えておくと役立つテクニックです。
よくあるエラーと原因・対処法
冒頭で触れた通り、FreeMarkerは些細な記述ミスも容赦なくエラーにします。代表的なエラーパターンを一覧にまとめました。
| エラーの種類 | 主な原因 | 対処法 |
|---|---|---|
| InvalidReferenceException | データモデルに存在しない変数を${ }で参照した |
!や??で欠損対策を行う、またはJava側でデータモデルを見直す |
| ParseException | タグの閉じ忘れ、カンマや引用符の抜け、対応していない構文 | エラーメッセージに表示される行番号・列番号を確認し、該当箇所の括弧や引用符の対応を1つずつ確認する |
| NonStringException | 文字列用のビルトイン関数(?upper_caseなど)を数値や日付に使った |
値の型を確認し、対応するビルトイン関数(数値なら?stringなど)に変更する |
| TemplateNotFoundException | テンプレートファイルのパスやファイル名の指定ミス | Configurationに設定したテンプレート読み込みディレクトリと、実際のファイルパスの一致を確認する |
| NonSequenceException | 配列やリストではない値に<#list>を使おうとした |
データモデル側の値がList・配列・シーケンスになっているか確認する |
エラー対処の基本チェックリスト
エラーが出たときに確認すること
- エラーメッセージに表示される行番号・列番号を最初に確認する
- 参照している変数名が、Java側のデータモデルのキー名と1文字違わず一致しているか確認する(大文字小文字も区別される)
- タグやディレクティブが、開始と終了で正しくペアになっているか確認する(
<#if>と</#if>など) - カンマ・引用符・括弧の数が対応しているか、1つずつ数えて確認する
- 存在しない可能性がある変数には、あらかじめ
!や??を付けているか確認する
まとめ
FreeMarkerは、変数展開・条件分岐・ループ・マクロといった一般的なプログラミングの要素をひと通り備えたテンプレートエンジンです。その一方で、存在しない変数の参照や構文のわずかなミスに対して非常に厳格で、あいまいな記述を汲み取ってくれることはほとんどありません。
裏を返せば、この記事で紹介した!や??による欠損値対策、タグの開閉ルール、そしてよくあるエラーパターンさえ押さえてしまえば、意図しない挙動に振り回されにくい安定したテンプレート運用が可能になります。まずは小さなテンプレートで変数展開とループを試し、慣れてきたらマクロやテンプレート分割で共通化を進めていくとよいでしょう。


0 件のコメント:
コメントを投稿