mustacheを拡張したHogan.jsをGroovy向けに改良したもの。
mustache:
Hogan.js, Hogan.groovy:
mustacheをJavaおよびGroovyで使うための参考:
実際に自分も練習してみました:
HTML向けのテンプレートエンジンとしてはかなり使いやすく小回りが効く点もポイント高いです。特にデフォルトでHTMLエスケープされる点が素晴らしいと思います。
あとはpartialでマッピング(コンテキスト?)を切り替えられれば、よく使うフォームタグなどを共通部品化出来るのですが、やり方知ってる人いたら教えて欲しいです・・・。Closure使う方式だと、文字列しか渡ってこないので・・・。
重箱の隅をつつくような細かいTips
Hogan.compile()はHoganTemplateインターフェイスの実装クラスを返しますが、デフォルトはGroovyHoganTemplateを継承したクラスのインスタンスが返されます。
そして、GroovyHoganTemplateでは、テンプレートを処理中の結果をインスタンス変数に格納しています。
これによりどのような影響が考えられるかというと、ServletContainer上でHogan.compile()が返すHoganTemplateのインスタンスをキャッシュして、レンダリングに使いまわす場合、同時に同じHoganTemplateのインスタンスのrender()メソッドを呼ぶとレンダリング結果が不正になる可能性があります・・・というか、簡単なスクリプトで実際にそうなります。
hogan_mt1.groovy:
@Grab(group='com.github.plecong', module='hogan-groovy', version='3.0') import com.github.plecong.hogan.* def data = [ m1: "hello1", m2: "hello2", m3: "hello3", 'sleep3': { println Thread.currentThread().getName() sleep(3 * 1000) return { "AWAKEN" } }, ] def template_s = """ {{m1}}{{m1}}{{m1}} {{#sleep3}}sleep now{{/sleep3}} {{m2}}{{m2}}{{m2}} {{#sleep3}}sleep now{{/sleep3}} {{m3}}{{m3}}{{m3}} """ def template = Hogan.compile(template_s) Thread.start { def r = template.render(data) synchronized(template) { println '-------------------' + Thread.currentThread().getName() println r println '-------------------' } } sleep(1000) Thread.start { def r = template.render(data) synchronized(template) { println '-------------------' + Thread.currentThread().getName() println r println '-------------------' } }
レンダリングの途中で、"sleep3"というクロージャで強制的に3秒間スリープさせ、それを2スレッド、1秒空けて並走させてみます。
結果は、以下のように片方のスレッド側に、もう片方の途中までのレンダリング結果が含まれてしまいます。
$ groovy hogan_mt1.groovy: Thread-73 Thread-74 Thread-73 Thread-74 -------------------Thread-73 hello1hello1hello1 hello1hello1hello1 AWAKEN hello2hello2hello2 AWAKEN hello2hello2hello2 AWAKEN hello3hello3hello3 ------------------- -------------------Thread-74 AWAKEN hello3hello3hello3 -------------------
単なるバグで済めばラッキーですが、Web上でアカウント画面など秘密情報をレンダリングするような箇所でこれを使ってしまうと、最悪、他の人向けのレンダリング結果が混入して情報漏えいにつながる可能性も考えられます。
どうしてもHogan.compile()の結果をキャッシュさせたい、となれば、あるスレッドがHoganTemplate.render()を使ってる間は、他のスレッドは待たせる必要があります。
JVM上のマルチスレッドでの排他処理の話題になりますので、色々解法はあると思いますが、単純に思いついたのはHoganTemplateのインスタンスに対してsynchronizedかければ(多分)大丈夫なんじゃないかなーと。
hogan_mt2.groovy:
// 途中まではhogan_mt1.groovyと同じなので省略 Thread.start { synchronized(template) { def r = template.render(data) println '-------------------' + Thread.currentThread().getName() println r println '-------------------' } } sleep(1000) Thread.start { synchronized(template) { def r = template.render(data) println '-------------------' + Thread.currentThread().getName() println r println '-------------------' } }
実行してみると、当たり前ですがHoganTemplate.render()が同期化され、結果が混ざることは無くなりました。
$ groovy hogan_mt2.groovy: Thread-77 Thread-77 -------------------Thread-77 hello1hello1hello1 AWAKEN hello2hello2hello2 AWAKEN hello3hello3hello3 ------------------- Thread-78 Thread-78 -------------------Thread-78 hello1hello1hello1 AWAKEN hello2hello2hello2 AWAKEN hello3hello3hello3 -------------------
HoganTemplateのインスタンスをキャッシュするのを諦め、代わりにHogan.compileClass()したクラスをキャッシュしておき、毎回Hogan.create()でインスタンスを生成します。
Hoganは全体として以下の様な流れになってます。
HoganTemplate自体をキャッシュさせられれば、全体の3/4を最初の1度だけに済ませられるのですが、それですとスレッド排他処理させるときにどうしても同期化が必要になってしまい却ってパフォーマンスが悪くなりそう、ならばHogan.compileClass()までをキャッシュさせ、全体の1/2の工程を最初の1度だけに抑えよう、という方針です。
hogan_mt3.groovy:
// 途中まではhogan_mt1.groovyと同じなので省略 def template_s = """ {{m1}}{{m1}}{{m1}} {{#sleep3}}sleep now{{/sleep3}} {{m2}}{{m2}}{{m2}} {{#sleep3}}sleep now{{/sleep3}} {{m3}}{{m3}}{{m3}} """ Class<HoganTemplate> htclazz = Hogan.compileClass(template_s) def lock = new Object() Thread.start { HoganTemplate t = Hogan.create(htclazz, template_s) def r = t.render(data) synchronized(lock) { println '-------------------' + Thread.currentThread().getName() println r println '-------------------' } } sleep(1000) Thread.start { HoganTemplate t = Hogan.create(htclazz, template_s) def r = t.render(data) synchronized(lock) { println '-------------------' + Thread.currentThread().getName() println r println '-------------------' } }
実行してみると、ひとまずちゃんと分離されてます。
$ groovy hogan_mt3.groovy Thread-83 Thread-84 Thread-83 Thread-84 -------------------Thread-83 hello1hello1hello1 AWAKEN hello2hello2hello2 AWAKEN hello3hello3hello3 ------------------- -------------------Thread-84 hello1hello1hello1 AWAKEN hello2hello2hello2 AWAKEN hello3hello3hello3 -------------------
partialを使いはじめると、使用するpartialについてもスレッドセーフを考慮する必要が出てきますので、より複雑になってくると思われます。