Javaのテンプレートエンジンを使う時はVelocityやFreeMarkerを選択することが多いのですが、他にいい選択肢がないのかなと思ってちょっと調べてみました。
自分が求めているのはHTMLに特化していない、汎用のテンプレートエンジンですね。なので、この文脈だとThymeleafは外れるかなと思っています。
でまあ、このあたりのエントリを見たりして
A Java Template Engine
http://vicox.net/2014/09/02/a-java-template-engine/
今回は、以下のテンプレートエンジンを試してみました。
- Pebble
- Rythm
- Jtwig
- HTTL
詳しく比較したりはしません。まずは試してみて、合うかな?興味が持てるかな?という程度の動機です。
では、順に試してみます。
あ、各テンプレートエンジン中で一部使っている、JavaBeans的なものは以下の定義としています。パッケージは、各Mainクラスと同じパッケージに属しているものとします。
public class Person { private String lastName; private String firstName; public Person(String lastName, String firstName) { this.lastName = lastName; this.firstName = firstName; } public String getLastName() { return lastName; } public String getFirstName() { return firstName; } }
また、各例において、テンプレートはクラスパス上にファイルとして置くものとします。
Pebble
Djangoにインスパイアされたシンタックスを持つテンプレートエンジンらしいですけど、自分はDjangoを使ったことがないのでよくわかりませんが…。
Pebble
http://www.mitchellbosecke.com/pebble/home
エスケープも自動的にかかるらしいです。
では、以下のページを見つつ試してみます。
Installation & Configuration
http://www.mitchellbosecke.com/pebble/documentation/guide/installation
Basic Usage
http://www.mitchellbosecke.com/pebble/documentation/guide/basic-usage
Maven依存関係。
<dependency> <groupId>com.mitchellbosecke</groupId> <artifactId>pebble</artifactId> <version>1.5.1</version> </dependency>
サンプルコード。
src/main/java/org/littlewings/template/pebble/PebbleExample.java
package org.littlewings.template.pebble; import java.io.IOException; import java.io.StringWriter; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import com.mitchellbosecke.pebble.PebbleEngine; import com.mitchellbosecke.pebble.error.PebbleException; import com.mitchellbosecke.pebble.template.PebbleTemplate; public class PebbleExample { public static void main(String... args) throws PebbleException, IOException { PebbleEngine engine = new PebbleEngine(); StringWriter writer = new StringWriter(); Map<String, Object> context = new HashMap<>(); context.put("word", "世界"); context.put("persons", Arrays.asList(new Person("磯野", "カツオ"), new Person("磯野", "ワカメ"))); context.put("containTag", "<script>Hello!!</script>"); PebbleTemplate template = engine.getTemplate("templates/sample.peb"); template.evaluate(writer, context); System.out.println(writer); } }
テンプレート。
src/main/resources/templates/sample.peb
こんにちは、{{ word }}
{% for person in persons %}
姓: {{ person.lastName }} 名: {{ person.firstName }}
{% endfor %}
escape?: {{ containTag }}
raw: {{ containTag | raw }}変数は{{ }}で囲み、forなどは{% %}で囲うみたいです。
実行結果。
こんにちは、世界 姓: 磯野 名: カツオ 姓: 磯野 名: ワカメ escape?: <script>Hello!!</script> raw: <script>Hello!!</script>
HTMLタグは、デフォルトでエスケープされています。
使ってちょっと違和感があったのが、改行の扱いですね…。テンプレートに改行がそこそこ入っている割には、出力結果には含まれていませんが、これを削ると行がくっついたりします。
どういうことでしょう…。
Rythm
オフィシャルサイトが国際化されていますが、なんか日本語が怪しい感じ…。
Rythm
http://rythmengine.org/
また、唯一日本語エントリがありました。
Java の template engine の Rythm を試す
http://tc.hatenablog.com/entry/2015/04/07/213444
では、チュートリアルやテンプレートエンジンガイドにそって試してみます。
Tutorial
http://rythmengine.org/doc/tutorial.md
Template Author's Guide
http://rythmengine.org/doc/template_guide.md
Maven依存関係。
<dependency> <groupId>org.rythmengine</groupId> <artifactId>rythm-engine</artifactId> <version>1.0.1</version> </dependency>
サンプルコード。
src/main/java/org/littlewings/template/rythm/RythmExample.java
package org.littlewings.template.rythm; import java.io.IOException; import java.io.StringWriter; import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.Map; import org.rythmengine.RythmEngine; public class RythmExample { public static void main(String... args) throws URISyntaxException, IOException { RythmEngine rythm = new RythmEngine(); StringWriter writer = new StringWriter(); Path templatePath = Paths.get(Thread.currentThread().getContextClassLoader().getResource("templates/sample.txt").toURI()); Map<String, Object> context = new LinkedHashMap<>(); context.put("word", "世界"); context.put("persons", Arrays.asList(new Person("磯野", "カツオ"), new Person("磯野", "ワカメ"))); context.put("containTag", "<script>Hello!!</script>"); rythm.render(writer, templatePath.toFile(), context); System.out.println(writer); } }
テンプレート。
src/main/resources/templates/sample.txt
@args String word, List persons, String containTag
@import org.littlewings.template.rythm.Person
こんにちは、@word
@for(Person person : persons) {
姓: @person.getLastName() 名: @person.getFirstName()
}
escape?: @containTag最初に、使う変数とその型の宣言が必要です。
なお、テンプレートにバインドする変数を単一のMapとして渡した場合は、展開されてテンプレートに渡されるようです。
https://github.com/greenlaw110/Rythm/blob/rythm-engine-1.0.1/src/main/java/org/rythmengine/RythmEngine.java#L872
これがわからず、けっこうハマりました…。
実行結果。
こんにちは、世界 姓: 磯野 名: カツオ 姓: 磯野 名: ワカメ escape?: <script>Hello!!</script>
こちらは、エスケープされずに出力されます。
なのですが、テンプレートファイルの拡張子を「.html」にすると、なんとエスケープされるようになります。
src/main/resources/templates/sample.html
@args String word, List persons, String containTag
@import org.littlewings.template.rythm.Person
こんにちは、@word
@for(Person person : persons) {
姓: @person.getLastName() 名: @person.getFirstName()
}
escape?: @containTagテンプレートを読むパスも変更。
Path templatePath = Paths.get(Thread.currentThread().getContextClassLoader().getResource("templates/sample.html").toURI());
出力結果。
こんにちは、世界 姓: 磯野 名: カツオ 姓: 磯野 名: ワカメ escape?: <script>Hello!!</script>
あと、ちょっと変わった挙動として、メソッドなどがテンプレート上で宣言された型に対して見つけることができないと、実行時にエラーになります。
例えば、こんな感じにgetLastNameのつづりを間違えると
姓: @person.getLastNam() 名: @person.getFirstName()
こんな感じのエラーになります。
Exception in thread "main" org.rythmengine.exception.CompileException: The method getLastNam() is undefined for the type Person
Template: /xxxxx/rythm-example/target/classes/templates/sample.txt
Relevant template source lines:
-------------------------------------------------
2: @import org.littlewings.template.rythm.Person
3:
4: こんにちは、@word
5:
6: @for(Person person : persons) {
>> 7: 姓: @person.getLastNam() 名: @person.getFirstName()
8: }
9:
10: escape?: @containTag
Relevant Java source lines:
-------------------------------------------------
123: org.rythmengine.internal.LoopUtil person_utils = new org.rythmengine.internal.LoopUtil(person_isFirst, person_isLast); //line: 6
124: org.rythmengine.internal.LoopUtil person__utils = new org.rythmengine.internal.LoopUtil(person_isFirst, person_isLast, person); //line: 6
125: __pushItrVar("person", person); //line: 6
126: p(__v6361); //line: 7
127:
>> 128: try{pe(person.getLastNam());} catch (RuntimeException e) {__handleTemplateExecutionException(e);} //line: 7
129: p(__v14126); //line: 7
130:
131: try{pe(person.getFirstName());} catch (RuntimeException e) {__handleTemplateExecutionException(e);} //line: 7
132: p(__v73678); //line: 7
133:
134: __popItrVar();これは、リフレクションで解決できればOKみたいな挙動ではなく、例えテンプレートにバインドされたオブジェクトが該当のメソッドやフィールドを持っていても、Object型として扱われていたりすると対象のメンバーが解決できずにエラーになります。
へぇ〜って感じでした。
Jtwig
PHPにTwigというテンプレートエンジンがあるみたいなのですが、それの移植版だったりするんでしょうか…?
Jtwig
http://jtwig.org/
シンタックスも似ている気がするので、そんな感じが。
チュートリアルがSpring MVCで書かれているのですが、
Integrating Jtwig
http://jtwig.org/documentation/get-started/
とりあえず普通に使います。
サンプルを参考にしました。
https://github.com/jtwig/jtwig-examples/tree/master/simple-app
Maven依存関係。
<dependency> <groupId>com.lyncode</groupId> <artifactId>jtwig-core</artifactId> <version>3.1.1</version> </dependency>
サンプルコード。
src/main/java/org/littlewings/template/jtwig/JtwigExample.java
package org.littlewings.template.jtwig; import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; import com.lyncode.jtwig.JtwigModelMap; import com.lyncode.jtwig.JtwigTemplate; import com.lyncode.jtwig.configuration.JtwigConfiguration; import com.lyncode.jtwig.exception.CompileException; import com.lyncode.jtwig.exception.ParseException; import com.lyncode.jtwig.exception.RenderException; public class JtwigExample { public static void main(String... args) throws ParseException, CompileException, RenderException, URISyntaxException { Path templatePath = Paths.get(Thread.currentThread().getContextClassLoader().getResource("templates/sample.twig").toURI()); JtwigConfiguration config = new JtwigConfiguration(); JtwigTemplate template = new JtwigTemplate(templatePath.toFile(), config); JtwigModelMap context = new JtwigModelMap(); context.put("word", "世界"); context.put("persons", Arrays.asList(new Person("磯野", "カツオ"), new Person("磯野", "ワカメ"))); context.put("containTag", "<script>Hello!!</script>"); System.out.println(template.output(context)); } }
テンプレートファイル。
src/main/resources/templates/sample.twig
こんにちは、{{ word }}
{% for person in persons %}
姓: {{ person.lastName }} 名: {{ person.firstName }}
{% endfor %}
escape?: {{ containTag }}
escape?: {{ containTag | escape }}Pebbleと同じような構文に見える…。
実行結果。
こんにちは、世界 姓: 磯野 名: カツオ 姓: 磯野 名: ワカメ escape?: <script>Hello!!</script> escape?: <script>Hello!!</script>
これも改行の調整が??
あと、デフォルトでHTMLエスケープされるみたいですね。
HTTL
最後は、HTTL。Hyper-Text Template Languageの略らしく、パフォーマンスが高いと謳っています。あと、構文がVelocityに似ていると。
HTTL
http://httl.github.io/en/
サンプルはこちら。
Example
http://httl.github.io/en/example.html
Syntax
http://httl.github.io/en/syntax.html
Maven依存関係。
<dependency> <groupId>com.github.httl</groupId> <artifactId>httl</artifactId> <version>1.0.11</version> </dependency>
サンプルコード。
src/main/java/org/littlewings/template/httl/HttlExample.java
package org.littlewings.template.httl; import java.io.IOException; import java.io.StringWriter; import java.text.ParseException; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import httl.Engine; import httl.Template; public class HttlExample { public static void main(String... args) throws IOException, ParseException { Engine engine = Engine.getEngine(); Map<String, Object> context = new HashMap<>(); context.put("word", "世界"); context.put("persons", Arrays.asList(new Person("磯野", "カツオ"), new Person("磯野", "ワカメ"))); context.put("containTag", "<script>Hello!!</script>"); Template template = engine.getTemplate("templates/sample.httl", "UTF-8"); StringWriter writer = new StringWriter(); template.render(context, writer); System.out.println(writer); } }
テンプレートファイル。
src/main/resources/templates/sample.httl
#set(String word, List<org.littlewings.template.httl.Person> persons, String containTag)
こんにちは、${word}
#for(org.littlewings.template.httl.Person person : persons)
姓: ${person.lastName} 名: ${person.firstName}
#end
escape?: ${containTag}
raw?: $!{containTag}確かにちょっとVelocityっぽいですが、型の宣言が必要です。
出力結果。
こんにちは、世界 姓: 磯野 名: カツオ 姓: 磯野 名: ワカメ escape?: <script>Hello!!</script> raw?: <script>Hello!!</script>
デフォルトでHTMLエスケープがかかり、$!でエスケープ解除です。
型宣言していることから、前述のRythmの時と同じように、プロパティ名など間違えると
姓: ${person.lastName} 名: ${person.firstName}
エラーになってコケます。
重大: No such property lastNam in class org.littlewings.template.httl.Person, because no such method getLastNam() or method isLastNam() or method lastNam() or filed lastNam.
Occur to offset: 177, line: 5, column: 14, char: ., in:
/templates/sample.httl
========================================
... 姓: ${person.lastNam} 名: ${person...
^-here
========================================
, stack: java.text.ParseException: No such property lastNam in class org.littlewings.template.httl.Person, because no such method getLastNam() or method isLastNam() or method lastNam() or filed lastNam.また、以下のようなプロパティファイルを用意して
src/main/resources/httl-html.properties
comment.left=<!-- comment.right=-->
コードをこのように変更して、設定ファイルを読むようにすると
public static void main(String... args) throws IOException, ParseException { Engine engine = Engine.getEngine("httl-html.properties"); Map<String, Object> context = new HashMap<>(); context.put("word", "世界"); context.put("persons", Arrays.asList(new Person("磯野", "カツオ"), new Person("磯野", "ワカメ"))); context.put("containTag", "<script>Hello!!</script>"); Template template = engine.getTemplate("templates/sample-html.httl", "UTF-8"); StringWriter writer = new StringWriter(); template.render(context, writer); System.out.println(writer); }
各ディレクティブを、HTMLコメントの中に埋め込むことができるようになります。
<!-- #set(String word, List<org.littlewings.template.httl.Person> persons, String containTag) -->
こんにちは、${word}
<!-- #for(org.littlewings.template.httl.Person person : persons) -->
姓: ${person.lastName} 名: ${person.firstName}
<!-- #end -->
escape?: ${containTag}
raw?: $!{containTag}「jericho-html」を使うことで、HTML属性中にディレクティブを書くことも可能みたいです。
で、どうだった?
とりあえず、さらっと「Hello World」的に試してみましたが、個人的にはこの段階では「これは!」みたいな印象は持てなかった気がします。
どれも、そこそこ癖がありますね。VelocityとかFreeMarkerでもいいんじゃないかな、と思ったり…。
※Velocityは開発がほぼ止まっている以外に、Toolsとログまわりがちょっと微妙なんですけど
Pebbleの改行に癖がなかったら、もうちょっと考えたかも。
なお、いずれのテンプレートエンジンも、現在はそこまで活発に開発が行われているわけではなさそうです。Pebbleはぼちぼち続いているみたいですが。
このエントリを読まれた方で、興味を引くところがあれば。