今回は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:userName
はclass="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) |
elementName とprefixElementName は、th:block みたいな要素を作るときにセットします。prefixElementName がtrue の場合、要素の完全名にプレフィックスが付与されます。(つまり、プレフィックスを付けずに利用するときはfalse をセットします。) |
(4) |
attributeName とprefixAttributeName は、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:if 、th:case
|
AbstractStandardExpressionAttributeTagProcessor |
th:text 、th:href
|
AbstractStandardFragmentInsertionTagProcessor |
th:insert 、th:replace
|
AbstractStandardMultipleAttributeModifierTagProcessor |
th:attr 、th: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が扱うモデルの概念さえ押さえておけば、比較的簡単に開発することができそうですね。