Quarkus 1.1 で追加されたテンプレートエンジン Qute

f:id:Naotsugu:20200108203613p:plain


はじめに

マイクロサービス・アプリケーション・フレームワークである Quarkus の バージョン 1.1 が 2019年12月17日にリリースされました。


主な変更点は以下となります。

  • GraalVM は 19.2.1 をサポート
    • GraalVM 19.3 へのアップグレードはいくつか問題があり Quarkus 1.2 に持ち越し
  • 新しいテンプレートエンジン Qute を experimental(実験的)として追加
  • Configuration フレームワークの更新
    • quarkus-config-yaml 拡張で YAML 形式をサポート
  • Spring API のサポートに Spring Security API を追加
  • Gradle のバージョンを 6系に変更
    • 旧来ユーザは Gradle plugin の宣言を変更する必要あり
  • Logging の改善
    • logging-gelf 拡張で Graylog Extended Log Format(GELF)をサポート
    • logging-json 拡張で JSON 形式のログフォーマットをサポート
    • logging-sentry 拡張で Sentry によるイベント監視をサポート
  • Kogito 0.6 へのアップデート
    • Kogito はルール/プロセスエンジン
  • Quartz 拡張にクラスタ化されたJOBのサポートを追加


細かな変更が多々入っています。

本稿では、Quarkus 1.1 で追加されたテンプレートエンジン Qute の概要を見ていきます。


Qute とは

おそらく、QUarkus TEmplate で Qute という名前になっているのだろうと思います。

Quarkus のアプローチに沿った、新しいテンプレートエンジンで、コードジェネレートによりビルド時に可能なことはビルド時に行い、リフレクションの使用を最小限に抑え、ネイティブイメージのサイズは小さく保つようになっています。

Quarkus 1.1 では experimental(実験的) 機能としてリリースされました。 初期コミットは2019年5月30日であり、現時点では初期の開発段階にあります。


プロジェクトの作成

既にスターターページに反映されています。

code.quarkus.io


Build Tool は Gradle を選択し、以下の拡張を選択してプロジェクトファイルをダウンロードします。

f:id:Naotsugu:20200106213510p:plain

せっかくなので、Quarkus 1.1 で入った quarkus-config-yaml も入れておきました。YAMLでの設定も合わせて見ていきます。


ダウンロードしたファイルを適当なディレクトリに解凍し、中身を覗いてみましょう。

build.gradle は以下のようになっています。

plugins {
    id 'java'
    id 'io.quarkus'
}

repositories {
     mavenLocal()
     mavenCentral()
}

dependencies {
    implementation 'io.quarkus:quarkus-resteasy-qute'
    implementation 'io.quarkus:quarkus-config-yaml'
    implementation enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}")
    implementation 'io.quarkus:quarkus-resteasy'

    testImplementation 'io.quarkus:quarkus-junit5'
    testImplementation 'io.rest-assured:rest-assured'
}

group 'org.acme'
version '1.0.0-SNAPSHOT'

compileJava {
    options.compilerArgs << '-parameters'
}

java {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
}


gradle.properties は以下のようになっています。

quarkusPluginVersion=1.1.0.Final
quarkusPlatformArtifactId=quarkus-universe-bom
quarkusPlatformVersion=1.1.0.Final
quarkusPlatformGroupId=io.quarkus


Hello Qute

src/main/java/org/acme/ExampleResource.java を以下のように変更します。

package org.acme;

import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import io.quarkus.qute.TemplateInstance;
import io.quarkus.qute.Template;

@Path("/hello")
public class ExampleResource {

    @Inject
    Template hello; 

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public TemplateInstance get(@QueryParam("name") String name) {
        return hello.data("name", name);  
    }
}

Template hello とすることで、フィールド名と同名のテンプレートファイルが src/main/resources/templates/ から選択されます。

@ResourcePath アノテーションでリソースパスを指定することもできます。 たとえば以下のようにするとsrc/main/resources/templates/example/items.html がテンプレートファイルとして選択されます。

@ResourcePath("example/items.html")
Template hello; 


テンプレートファイル自体を作成します。

src/main/resources/templates/hello.txt に以下の内容でファイルを作成します。

Hello {name}! 

hello.data("name", name); のようにして値の埋め込みができます。


YAML サポートも試しておきます。

src/main/resources/application.propertiessrc/main/resources/application.yaml にリネームして以下のように編集します(application.properties が優先されるためリネームしておく必要があります)。

quarkus:
  http:
    port: 9090

ここでは YAML 形式でポートを指定しました。


実行してみましょう。

$ ./gradlew quarkusDev


http://localhost:9090/hello?name=Bob にアクセスすれば以下のような出力が得られます。

f:id:Naotsugu:20200106213425p:plain

簡単ですね。


Qute テンプレートの文法

式はブランケットで括ります。

{name} 
{item.name} 

現在のコンテキストから名前で解決され、解決できない場合は(もしあれば)親のコンテキストオブジェクトで解決が試みられます。


メソッド呼び出しは以下のようになります。

{item.get(name)}
{name or 'John'},

2つ目の例は、暗黙的に name.or('John') というメソッド呼び出しに変換されます。


条件分岐は以下のようになります。

{#if item.name is 'sword'}
  It's a sword!
{#else if item.name is 'shield'}
  It's a shield!
{#else}
  Item is neither a sword nor a shield.
{/if}

iseq== としても同じです。その他、ne, !=, gt, >, ge, >=, lt, <, le, <= が利用できます。


each ループは以下のように書きます。

{#each items}
  {count}. {it.name} 
{/each}

count は1始まりのカウンタとして使用できます。

it はループ対象のエイリアスとして使用できます。


for ループは以下のように書きます。

{#for item in items}
  {item.name}
{/for}


with で現在のコンテキストオブジェクトを定義できます。

{#with item.parent}
  {name}  
{/with}

この例では、item.parent.name として解決されます。


include にて共通のテンプレートに値を流し込むことができます。

共通テンプレートとして base を以下のように準備し、

<html>
<head>
<meta charset="UTF-8">
<title>{#insert title}Default Title{/}</title> 
</head>
<body>
  {#insert body}No body!{/} 
</body>
</html>

それぞれのテンプレートで以下のようにすれば、

{#include base} 
  {#title}My Title{/title} 
  {#body}
    <div>
      My body.
    </div>
  {/body}
{/include}

以下のような出力が得られます。

<html>
<head>
<meta charset="UTF-8">
<title>My Title</title> 
</head>
<body>
    <div>
      My body.
    </div>
</body>
</html>


Template Extension Methods

Qute には Template Extension Methods という便利な仕組みが用意されています。

Hello World より少し複雑な例の中で動作を見ていきます。


最初にテンプレートに出力するための Item を以下のように用意します。

package org.acme;

import java.math.BigDecimal;

public class Item {
    public String name;
    public BigDecimal price;

    public static Item of(String name, BigDecimal price) {
        Item item = new Item();
        item.name = name;
        item.price = price;
        return item;
    }
}


YAML 設定の例も含めたいので application.yaml を以下のように変更します。

quarkus:
  http:
    port: 9090

app:
  items:
    title: Item list

Item 一覧のタイトルを定義しました。


ItemResource を以下のように作成します。

package org.acme;

import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateExtension;
import io.quarkus.qute.TemplateInstance;
import org.eclipse.microprofile.config.inject.ConfigProperty;

import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.math.BigDecimal;
import java.util.Arrays;

@Path("items")
public class ItemResource {

    @ConfigProperty(name = "app.items.title", defaultValue = "Title")
    String title;

    @Inject
    Template items;

    @GET
    @Path("/")
    @Produces(MediaType.TEXT_HTML)
    public TemplateInstance list() {
        return items
            .data("title", title)
            .data("items", Arrays.asList(
                Item.of("item1", BigDecimal.valueOf(100)),
                Item.of("item2", BigDecimal.valueOf(200)),
                Item.of("item3", BigDecimal.valueOf(300)))
        );
    }

    @TemplateExtension
    static BigDecimal discountedPrice(Item item) {
        return item.price.multiply(new BigDecimal("0.9"));
    }
}

@ConfigProperty で YAML 定義内容を取り込んでいます。

@Produces(MediaType.TEXT_HTML) で HTML を返却するメソッドを定義し、Item のインスタンスをリストでテンプレートに流しています。

@TemplateExtension で指定したメソッドが Template Extension Methods で、テンプレートからこのメソッドを呼び出すことができます。 この例では、Item の割引した価格を計算する static メソッドとなります。


最後にテンプレート items.html です。

<!DOCTYPE html>
<html>
<head>
  <title>{title}</title>
</head>
<body>
{#for item in items}
  <h5>{item.name}</h5>
  <p>
    Price: {item.price}
    <br/>
    {#if item.price > 100}
      Discounted Price: {item.discountedPrice}
    {/if}
  </p>
{/for}
</body>
</html>

Item の内容を for ループで繰り返しています。

{item.discountedPrice} で先程定義した Template Extension Methods の呼び出しとなります。

item がベースオブジェクトとなり discountedPrice という名前のメソッドに渡されることになります。これにより Item オブジェクトに discountedPrice という computed properties を定義したように動きます。


実行してみましょう。

$ ./gradlew quarkusDev


http://localhost:9090/items にアクセスすれば以下のような出力が得られます。

f:id:Naotsugu:20200107230505p:plain


まとめ

Quarkus 1.1 で追加されたテンプレートエンジンである Qute について見てみました。

まだ初期の開発段階ではありますが、シンプルで心地よいテンプレートエンジンでした。



マイクロサービスアーキテクチャ

マイクロサービスアーキテクチャ

  • 作者:Sam Newman
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2016/02/26
  • メディア: 単行本(ソフトカバー)

Kubernetes完全ガイド impress top gearシリーズ

Kubernetes完全ガイド impress top gearシリーズ

  • 作者:青山 真也
  • 出版社/メーカー: インプレス
  • 発売日: 2018/09/21
  • メディア: Kindle版

Microservices for the Enterprise: Designing, Developing, and Deploying (English Edition)

Microservices for the Enterprise: Designing, Developing, and Deploying (English Edition)