home ホーム search 検索 -  login ログイン  | reload edit datainfo version cmd icon diff delete  | help ヘルプ

Groovy/TemplateEngine/Hogan.groovy

Groovy/TemplateEngine/Hogan.groovy

Groovy / TemplateEngine / Hogan.groovy
id: 1166 所有者: msakamoto-sf    作成日: 2013-03-18 00:58:01
カテゴリ: Groovy 

mustacheを拡張したHogan.jsをGroovy向けに改良したもの。

mustache:

Hogan.js, Hogan.groovy:

mustacheをJavaおよびGroovyで使うための参考:

実際に自分も練習してみました:

HTML向けのテンプレートエンジンとしてはかなり使いやすく小回りが効く点もポイント高いです。特にデフォルトでHTMLエスケープされる点が素晴らしいと思います。

あとはpartialでマッピング(コンテキスト?)を切り替えられれば、よく使うフォームタグなどを共通部品化出来るのですが、やり方知ってる人いたら教えて欲しいです・・・。Closure使う方式だと、文字列しか渡ってこないので・・・。

Tips

重箱の隅をつつくような細かいTips

Hogan.compile()が返すHoganTemplateのインスタンスはスレッドセーフ「ではない」

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上でアカウント画面など秘密情報をレンダリングするような箇所でこれを使ってしまうと、最悪、他の人向けのレンダリング結果が混入して情報漏えいにつながる可能性も考えられます。

対処策その1 : HoganTemplateのインスタンスについてsynchronizedを使う。

どうしても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

-------------------
対処策その2 : Hogan.compileClass()したのをキャッシュしておき、毎回Hogan.create()でインスタンスを生成する。

HoganTemplateのインスタンスをキャッシュするのを諦め、代わりにHogan.compileClass()したクラスをキャッシュしておき、毎回Hogan.create()でインスタンスを生成します。
Hoganは全体として以下の様な流れになってます。

  1. テンプレートをGroovyソースコード(GroovyHoganTemplateクラスから派生)に変換して、
  2. 動的に↑のコードを読み込んでClassLoaderにロードさせ、(・・・ここまでがHogan.compileClass()の結果)
  3. ↑のクラスのインスタンスを作成・・・ここまでがHogan.compile()の結果。
  4. ↑のインスタンス=HoganTemplateのrender()で実行

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についてもスレッドセーフを考慮する必要が出てきますので、より複雑になってくると思われます。



プレーンテキスト形式でダウンロード
現在のバージョン : 1
更新者: msakamoto-sf
更新日: 2013-04-07 18:39:58
md5:260d9fe53665bdf86838524e137968bf
sha1:ac51a9ad018ef24391c6b1cdacd6aedace109e56
コメント
コメントを投稿するにはログインして下さい。