Skip to content

freemarker - template engine

freemarker は人気がある Java の template engine です。 以下は spring と連携して利用する前提で記述していますが、spring 利用しない場合でも利用できる知見があると思います。

freemarker を spring-boot で利用するには以下のように依存を追加します。

kotlin
plugins {
    id("java")
    id("org.springframework.boot") version "2.7.18"
}

dependencies {
    implementation("com.google.guava:guava")
    implementation("org.springframework.boot:spring-boot-starter-freemarker")
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.webjars.npm:bootstrap")

    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

基本的な設定

src/main/resources/config/application.yml に profile 切り替え用の設定を記述します。 ここではデフォルトでは以下の 2 つの profile を設定しています。

  • local: local での開発に使用。デバッグしやすく設定。
  • release: 本番環境用の設定。パフォーマンスが出るように設定。
yaml
# ---------------------------------------------------------------------------------------------------------------------
#
# 以下、profile ごとの設定
#
# ---------------------------------------------------------------------------------------------------------------------
---
spring.profiles: default
spring.profiles.active: local
---
spring.profiles: local
spring.profiles.include:
- freemarker-devel
- freemarker-common
---
spring.profiles: release
spring.profiles.include:
- freemarker-release
- freemarker-common

src/main/resources/config/application-freemarker-common.yml に以下のように設定します。 このファイルには環境によらない fremarker の共通の設定を記述します。

yaml
spring.freemarker:
  charset: UTF-8
  settings:
    # default charset for string?url()
    url_escaping_charset: UTF-8
    # no auto commify
    number_format: 0.#######
    # Enable auto escape
    # http://freemarker.org/docs/dgui_misc_autoescaping.html
    output_format: HTMLOutputFormat
    lazy_auto_imports: true

src/main/resources/config/application-freemarker-devel.yml を以下のように設定します。 キャッシュをオフにして、テンプレートファイルを変更したときにをファイルシステムから随時リロードできるようにして開発効率をあげます。 例外発生時にはエラーを上げるようにします。

yaml
spring.freemarker:
  # テンプレートのキャッシュ。
  # 開発時は false。本番では true。
  cache: false
  # 開発時は source directory を指定することにより、自動的にリロードされます。
  template-loader-path:
    - file:src/main/resources/templates/
    - classpath:/templates/
  settings:
    # 例外の処理モード。
    # 開発時は html_debug 本番は rethrow を指定する。
    template_exception_handler: html_debug

Java 8 Date & Time API (JSR 310) 対応

Java 8 Date & Time API (JSR 310) のオブジェクトは、現状の freemarker ではサポートされていません。つまり、Freemarker に LocalDateTime などの JSR 310 で導入された Date/Time 系のオブジェクトを渡しても Date/Time 処理用のフィルタなどを利用することができません。

freemarker-java-8 を利用すればできるようですが、このライブラリは maven central にリリースされておらず、使いづらいです。 https://github.com/amedia/freemarker-java-8

それほど多くないコード量で実現可能なので、以下のように設定するのが良いでしょう。

java
import freemarker.template.DefaultObjectWrapper;
import freemarker.template.SimpleDate;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfig;

import java.sql.Time;
import java.sql.Timestamp;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;

@Configuration
public class FreemarkerConfig {
    @Autowired
    void configurefreeMarkerConfigurer(FreeMarkerConfig configurer) {
        configurer.getConfiguration().setObjectWrapper(new CustomObjectWrapper());
    }

    private static class CustomObjectWrapper extends DefaultObjectWrapper {
        public CustomObjectWrapper() {
            super(freemarker.template.Configuration.VERSION_2_3_25);
        }

        @Override
        public TemplateModel wrap(Object obj) throws TemplateModelException {
            if (obj instanceof LocalDateTime) {
                Timestamp timestamp = Timestamp.valueOf((LocalDateTime) obj);
                return new SimpleDate(timestamp);
            } else if (obj instanceof LocalDate) {
                java.sql.Date date = java.sql.Date.valueOf((LocalDate) obj);
                return new SimpleDate(date);
            } else if (obj instanceof LocalTime) {
                Time time = Time.valueOf((LocalTime) obj);
                return new SimpleDate(time);
            } else {
                return super.wrap(obj);
            }
        }
    }

}

Note: ( https://issues.apache.org/jira/browse/FREEMARKER-35 そもそもコアで対応してほしいので issue 上げてみました。 )

Wrapper を使う。

header/footer をちまちま設定するよりもマクロでラッパーしたほうが便利です。

src/main/resources/templates/__wrapper.ftlh というファイル名で以下の内容を置きます。

ftl
<#ftl strip_whitespace=true>
<#macro main subtitle="-">

<!doctype html>
<html>
<head>
    <title>${subtitle} - QND</title>
    <link rel="stylesheet" href="/webjars/bootstrap/3.3.5/dist/css/bootstrap.min.css">
</head>
<body>

    <#nested/>

</body>
</html>
</#macro>

利用側では以下のようにします。<@wrapper.main> から </@wrapper.main> までの間に書いた内容は __wrapper.ftl<#nested/> の中に展開されます。

ftl
<#import "/__wrapper.ftlh" as wrapper>
<@wrapper.main>

<h1>List of items</h1>

...

</@wrapper.main>

テンプレートファイルのパスを補完できるようにする

src/main/resources/freemarker_implicit.ftlh を以下のように設定すると、src/main/resources/templates/ をルートディレクトリとしてルート相対でテンプレートファイルのパスを IntelliJ で補完できるので便利です(<#import "/__wrapper.ftl" as wrapper>/__wrapper.ftl の部分で補完がきくようになります)。

ftl
[#ftl]
[#-- @implicitly included --]
[#-- @ftlroot "./templates" --]

Freemarker cheat sheet

良く使う基本的な構文をいくつかここで例にあげます。他にも載せといたほうが便利なやつあれば随時追記しましょう。

ftl

loop:

  <#list ["foo", "bar", "baz"] as x>
  ${x}
  </#list>

if:

  <#assign user="Big Joe">
  <#if user == "Big Joe">
    It is Big Joe
  </#if>

include:

  <#include "/footer/${company}.html">

switch:

  <#assign size="small">
  <#switch size>
    <#case "small">
       This will be processed if it is small
       <#break>
    <#case "medium">
       This will be processed if it is medium
       <#break>
    <#case "large">
       This will be processed if it is large
       <#break>
    <#default>
       This will be processed if it is neither
  </#switch>

文字列系の処理:

  HTML自動エスケープモードのときに、自動エスケープ対象外にする。
  ${"<b>hello</b>"?no_esc}

  文字列置換
  ${"this is a car acarus"?replace("car", "bulldozer")}

  前方一致
  ${"redirect"?starts_with("red")?c}

  文字列を含む?
  <#if "piceous"?contains("ice")>It contains "ice"</#if>

  後方一致
  <#if "piceous"?ends_with("ice")>It ends with "ice"</#if>

  文字列スライス
  ${"hoge"[0]}
  ${"hoge"[1..]}
  ${"hoge"[1..2]}
  ${"hoge"[1..*2]} ← str[from..*maxLength]

  大文字に
  ${"GrEeN MoUsE"?upper_case}

  Capitalize
  ${"GreEN mouse"?capitalize}

  URL escape:
  ${'a/b c'?url}

  文字列の長さ:
  ${"GreEN mouse"?length}

数字系の処理:

  絶対値
  ${-5?abs}
  ${5?abs}

  整数化
  <#assign testlist=[
    0, 1, -1, 0.5, 1.5, -0.5,
    -1.5, 0.25, -0.25, 1.75, -1.75]>
  <#list testlist as result>
      ${result} ?floor=${result?floor} ?ceiling=${result?ceiling} ?round=${result?round}
  </#list>

  フォーマット
  ${3.14?string["0.#"]}

日時系の処理:

  現在の日時を得る
  ${.now}

  SimpleDateFormat のパターンでフォーマットする
  ${.now?string["yyyy-MM-dd(EEE) HH:mm"]}

boolean 系の処理:

  文字列化
  ${true?c}

  分岐しての文字列化
  ${true?string("yes", "no")}

リスト系の処理:

  join
  ${["red", "green", "blue"]?join(", ")}

  reverse
  ${["red", "green", "blue"]?reverse?join(", ")}

  size
  ${["red", "green", "blue"]?size}

  sort
  ${["red", "green", "blue"]?sort?join(", ")}

Map 系の処理:

  get
  ${{ "name": "mouse", "price": 50 }['name']}

  keys
  ${{ "name": "mouse", "price": 50 }?keys?join(", ")}

  values
  ${{ "name": "mouse", "price": 50 }?values?join(", ")}

ループ内変数

  <#list ['a', 'b', 'c'] as x>
    ${x?index}
    ${x?has_next?c}
    ${x?is_even_item?c}
    ${x?is_first?c}
    ${x?is_last?c}
    ${x?is_odd_item?c}
  </#list>