spring
Thymeleaf
Thymeleaf3

Thymeleaf3で独自のDialectを作る

More than 1 year has passed since last update.

今回はSpring Bootの標準テンプレートエンジンであるThymeleafについてお話します。

最新のThymeleaf3で独自のタグを作る方法について解説します。(Thymeleaf2とは大きく実装が異なるので注意!

Thymeleafに機能を追加するDialect(方言)

Thymeleafでは、ナチュラルなHTMLにThymeleaf独自の文法(タグやEL式)を付与してテンプレート化するのはご存知の通りです。

Thymeleaf独自の文法をまとめたものが「Dialect」で、JSPで言うタグライブラリのようなものです。
代表的なDialectとして、以下のようなものがあります。

name description
Thymeleaf Standard Dialect Thymeleafの基本文法をまとめたDialect
Spring Standard Dialect Thymeleafの基本文法に加え、Spring MVCと連携するための文法をまとめたDialect
Spring Security Dialect Spring Securityと連携するための文法をまとめたDialect
Java8 Time Dialect Java 8で提供されるDate Time APIと連携するための文法をまとめたDialect

Thymeleafに開発者が独自の文法を追加するときには、このDialectを作れば良いわけです。

Dialectの種類

ThymeleafのDialectには、その機能に応じて5つのインターフェイスがあります。
が、通常使うのは2つだけだと思うので、それだけ紹介します。

name description
IProcessorDialect タグ(要素や属性)を提供するダイアレクト
IExpressionObjectDialect タグ属性値のEL式に使用できるオブジェクトを提供するダイアレクト

Processorはth:blockみたいな要素やth:textみたいな属性を追加することができて、
ExpressionObjectは#datesみたいなEL式で使うユーティリティを追加することができるわけですね。

ちなみに、IProcessorDialectにはAbstractProcessorDialectという抽象クラスが用意されているので、通常はそちらを継承して使うことになります。

Processorを作る

では続いて、登録するProcessorの作り方を見ていきましょう。
Processorにも、その機能に応じて3つのインターフェイスがあります。

name description
IElementProcessor タグが書かれた要素を受け取って処理するProcessor
IElementModelProcessor タグが書かれた要素と配下の要素をモデルとして受け取って処理するProcessor
ITemplateBoundariesProcessor テンプレートの開始・終了時にのみ呼び出せる特殊なProcessor

IElementProcessorは最も単純で、その要素(またはそのボディ部)を編集するProcessorですね。

例えば、

<span my:userName></span>

のような要素を

<span>Tom</span>

のように編集するわけです。

IElementModelProcessorはもう少し複雑で、その要素より上位/下位の要素の状態を確認して、その要素(またはそのボディ部)を編集するProcessorです。

具体的に言えば、

<div class="user-info">
  <span my:userName></span>
</div>

のように、「my:userNameclass="user-info"が付与されたスペース内で使用しなさい!」と文法を強制できるわけです。もちろん、文法の強制だけでなく、上位/下位の要素の状態に応じてその要素をどう編集するかを変えることも可能です。

ITemplateBoundariesProcessorはここでは解説しませんが、テンプレートの上部に固定で何かを付与する、とかできそうですね。

IElementProcessorを利用してタグを作る

というわけで、まずは最も分かりやすいIElementProcessorを利用してタグを作ってみましょう。
とは言っても、IElementProcessorを直接継承して実装クラスを作ることは滅多にないでしょう。
通常は、サブインターフェイスIElementTagProcessorを継承したAbstractAttributeTagProcessorを継承して実装することになります。

以下に示すのが、AbstractAttributeTagProcessorのシグネチャです。

  • コンストラクタ

    コンストラクタでは、Processorがどのように使われるかを定義します。

public abstract class AbstractAttributeTagProcessor extends AbstractElementTagProcessor {

    protected AbstractAttributeTagProcessor(
            final TemplateMode templateMode, final String dialectPrefix, // (1)(2)
            final String elementName, final boolean prefixElementName, // (3)
            final String attributeName, final boolean prefixAttributeName, // (4)
            final int precedence, final boolean removeAttribute) { // (5)(6)
        super(templateMode, dialectPrefix, elementName, prefixElementName, attributeName, prefixAttributeName, precedence);
        Validate.notEmpty(attributeName, "Attribute name cannot be null or empty in Attribute Tag Processor");
        this.removeAttribute = removeAttribute;
    }
}
no. description
(1) templateModeには、処理するテンプレートの種類をセットします。例えばTemplateMode.HTMLなど。
(2) dialectPrefixには、処理するタグに付与されたプレフィックス(コロン前の部分)をセットします。Dialectから渡されるように実装すると良いでしょう。
(3) elementNameprefixElementNameは、th:blockみたいな要素を作るときにセットします。prefixElementNametrueの場合、要素の完全名にプレフィックスが付与されます。(つまり、プレフィックスを付けずに利用するときはfalseをセットします。)
(4) attributeNameprefixAttributeNameは、th:textみたいな属性を作るときにセットします。プレフィックスについては(3)と同じですね。
(5) precedenceはそのDialect内でのProcessorの優先順位を示し、値が小さい方が早く実行されます。順序性が関係しないのであれば、同じ値をセットしておくと良いでしょう。
(6) removeAttributeはProcessorが処理した後にタグを削除するかどうかを示し、通常はtrue(削除する)をセットすると思います。
  • 抽象メソッド

    doProcessメソッドでは、Processorが要素に行う処理を定義します。

public abstract class AbstractAttributeTagProcessor extends AbstractElementTagProcessor {

    protected abstract void doProcess(
            final ITemplateContext context, // (1)
            final IProcessableElementTag tag, // (2)
            final AttributeName attributeName, // (3)
            final String attributeValue, // (4)
            final IElementTagStructureHandler structureHandler); // (5)
}
no. description
(1) contextでは、リクエスト・レスポンスや変数(SpringだとModel)のような共通的な情報が渡されます。
(2) tagでは、処理する要素の情報が渡されます。例えばタグ名を取得したり、関連する別の属性を取得することができます。ただし、tagはあくまで渡されるだけで、編集はできません。
(3) attributeNameでは、処理する属性の名前が渡されます。
(4) attributeValueでは、処理する属性の値が渡されます。この値は、EL式の処理をしていない値です。${test}をセットしたら、そのまま渡されます。
(5) structureHandlerは、編集を行う対象です。

では次に、IModelFactoryとIElementTagStructureHandlerの使い方を見ていきましょう。
IModelFactoryって何?」と混乱されると思いますが、ThymeleafではHTML以外のテンプレートも扱うため、HTMLのタグ(要素)をモデルという概念に置き換えて操作します。モデルを生成するのがIModelFactoryです。

    @Override
    protected void doProcess(
            final ITemplateContext context,
            final IProcessableElementTag tag,
            final AttributeName attributeName,
            final String attributeValue,
            final IElementTagStructureHandler structureHandler) {

        IModelFactory modelFactory = context.getModelFactory(); // (1)

        String spanTag = "span";
        IModel model = modelFactory.createModel(); // (2)

        // (3)
        model.add(modelFactory.createOpenElementTag(spanTag));
        model.add(modelFactory.createText(attributeValue));
        model.add(modelFactory.createCloseElementTag(spanTag));

        structureHandler.setBody(model, false); // (4)

        // (5)
        String classTag = "class";
        IAttribute classAttr = tag.getAttribute(classTag);
        structureHandler.setAttribute(classTag, (classAttr == null) ? "user-info" : classAttr.getValue() + " user-info");
    }
no. description
(1) モデルを生成するには、まずITemplateContext#getModelFactoryでIModelFactoryを取得します。
(2) IModelFactory#createModelでモデルを作成します。作成したモデルは空なので、これから内容を詰め込んでいきます。
(3) モデルには、ITemplateEventを順番に追加していきます。例では「span要素の開始タグ→テキスト→終了タグ」という順で追加しています。これ以外にも、単一タグ、コメントタグなど様々なイベントが用意されています。
(4) IElementTagStructureHandler#setBodyで、生成したモデルを追加します。なお、元々記述してあった配下の要素は削除されるので注意!
(5) IElementTagStructureHandler#setAttributeで処理する要素に新たな属性を追加できます。例では、元あるclass属性にuser-infoを追加しています。

IElementTagStructureHandler#setBodyの第二引数にtrueをセットした場合、setBodyで追加したボディがProcessorにより再評価されます。
今回のコード例のように決まった要素を追加する場合には再評価する必要はありませんが、例えばth:insertのようにFragment(HTMLテンプレートの断片)を追加するようなProcessorを作る場合は再評価しましょう。

IElementTagStructureHandlerには他にもたくさんのメソッドがありますが、このページ最下部からリンクしている公式リファレンスで詳しく解説されているので、実装する前に調べてみてくださいね!

Standard Dialect提供のProcessorに類似したProcessorを作る

ThymeleafのStandard Dialectでは、AbstractAttributeTagProcessorを継承したサブ抽象クラスを提供しており、
Standard Dialectで提供されるProcessorに類似したProcessorを作るのであれば、これらの利用も選択肢に入ります。

name example
AbstractStandardAssertionTagProcessor th:assert
AbstractStandardConditionalVisibilityTagProcessor th:ifth:case
AbstractStandardExpressionAttributeTagProcessor th:textth:href
AbstractStandardFragmentInsertionTagProcessor th:insertth:replace
AbstractStandardMultipleAttributeModifierTagProcessor th:attrth:attrappend
AbstractStandardTargetSelectionTagProcessor th:object
AbstractStandardTextInlineSettingTagProcessor th:inline

属性値にセットしたEL式を解釈してくれるAbstractStandardExpressionAttributeTagProcessorを使う機会は多いかもしれません。

ExpressionObjectを作る

次に、登録するExpressionObjectを作ってみましょう。
といっても、ExpressionObjectは任意のクラスをnewして登録するだけなので、何も言うことはありません。

例えば以下のようなクラスを作成して

public class Text {

    private final String prefix;

    public Text(String prefix) {
        this.prefix = prefix;
    }

    public String prefix(String text) {
        return prefix + text;
    }

}

以下のように使うイメージになります。

<span th:text="${#text.prefix(userName)}"></span>

Dialectを作る

最後に、Dialectを作成してProcessorやExpressionObjectを追加してみましょう。
必要に応じて、AbstractProcessorDialectとIExpressionObjectDialectを継承したクラスを作成するだけ。簡単ですね。

public class MyDialect extends AbstractProcessorDialect implements IExpressionObjectDialect {

    private static final String DIALECT_NAME = "My Dialect";
    private static final String DIALECT_PREFIX = "my";
    private static final String EXPRESSION_NAME = "text";

    public MyDialect() {
        this(DIALECT_PREFIX);
    }

    public MyDialect(String dialectPrefix) {
        super(DIALECT_NAME, dialectPrefix, StandardDialect.PROCESSOR_PRECEDENCE);
    }
    // (1)
    @Override
    public Set<IProcessor> getProcessors(String dialectPrefix) {
        final Set<IProcessor> processors = new HashSet<IProcessor>();
        processors.add(new MyProcessor(dialectPrefix));
        processors.add(new StandardXmlNsTagProcessor(TemplateMode.HTML, dialectPrefix));
        return processors;
    }
    // (2)
    @Override
    public IExpressionObjectFactory getExpressionObjectFactory() {
        return new IExpressionObjectFactory() {
            // (3)
            @Override
            public Set<String> getAllExpressionObjectNames() {
                return Collections.singleton(EXPRESSION_NAME);
            }
            // (4)
            @Override
            public Object buildObject(IExpressionContext context, String expressionObjectName) {
                return (EXPRESSION_NAME.equals(expressionObjectName)) ? new Text("user-") : null;
            }
            // (5)
            @Override
            public boolean isCacheable(String expressionObjectName) {
                return false;
            }
        };
    }
}
no. description
(1) IProcessorDialect#getProcessorsを実装して、Processorを登録します。StandardXmlNsTagProcessorを登録しているのは、HTMLの最初につけるxmlns:my="http://my.com/"のようなネームスペース表記を削除するためです。
(2) IExpressionObjectDialect#getExpressionObjectFactoryを実装して、IExpressionObjectFactoryを経由してExpressionObjectを登録します。
(3) IExpressionObjectFactory#getAllExpressionObjectNamesでは、ExpressionObjectの名前を登録します。
(4) IExpressionObjectFactory#buildObjectでは、ExpressionObject自体を登録します。getAllExpressionObjectNamesで登録した名前が使われたときに呼び出されます。
(5) IExpressionObjectFactory#isCacheableでは、ExpressionObjectをキャッシュするかどうかを設定します。状態によって異なる結果を返すようなExpressionObjectは、キャッシュしてはいけません。

作成したダイアレクトは、テンプレートエンジンに登録して使いましょう!(Spring MVCならSpringTemplateEngine#additionalDialects

まとめ

主にSpring MVCで独自のデータストア(Bean)を扱う場合や、テンプレートをより簡略化したい場合に独自タグが大活躍してくれると思います。

Thymeleaf3はThymeleaf2に比べ、HTML以外にテキスト形式のテンプレートもサポートしたことによりProcessor周りのアーキテクチャを大幅に見直しており、より複雑な作りになっていてびっくりしました。とはいえ、ここまで見てきたインターフェイス・クラスの構成とProcessorが扱うモデルの概念さえ押さえておけば、比較的簡単に開発することができそうですね。

参考