【2019年11月版】QuarkusでThymeleafのネイティブ化に挑戦！→テンプレートでメソッド呼び出しはちょっと待った！

# 今回のネイティブ化は、テンプレートエンジンの Thymeleaf に挑戦です。

GraalVM でどこまでネイティブ化できるかチャレンジ、今回はテンプレートエンジン **Thymeleaf** のネイティブ化に挑戦です。

と、チャレンジを始めてからそのまんまのページを見つけてしまいました。。。

- [QuarkusでHTMLを返す | ナッツウェル 技術研究室](http://www.natswell.com/techcolumn/2019/09/13/quarkus-html-response/)
- 上記の記事の成果物です → [quarkus-webui-example](https://github.com/takami-h/quarkus-webui-example)

正直、Resteasy でどうやって Thymeleaf 使えば良いのか見当がつかなかったので有り難かったですw

それでは今回もモリモリの構成でまいりたいと思います!

## 1. プロジェクトの作成

例によって以下のコマンドでプロジェクト作成と、プラグインてんこ盛りをインストールします。

```shell
$ mvn io.quarkus:quarkus-maven-plugin:1.0.0.CR1:create
...
$ cd thymeleaf-quarkus-project
$ mvn quarkus:add-extension -Dextensions="quarkus-resteasy,quarkus-jdbc-postgresql,quarkus-smallrye-metrics,quarkus-smallrye-openapi,quarkus-smallrye-opentracing,quarkus-hibernate-orm-panache,quarkus-resteasy-jsonb"
```
今回は `thymeleaf-quarkus-project` という名前で作成してみました。そしてもう好い加減、HelloResource は作成しなくてもいいでしょうね。。。

そして、pom.xmlから依存関係の追加です。

```xml:pom.xml
...
    <dependency>
      <groupId>io.opentracing.contrib</groupId>
      <artifactId>opentracing-jdbc</artifactId>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.datatype</groupId>
      <artifactId>jackson-datatype-jsr310</artifactId>
    </dependency>
    <dependency>
      <groupId>org.jboss.resteasy</groupId>
      <artifactId>resteasy-html</artifactId>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.thymeleaf</groupId>
      <artifactId>thymeleaf</artifactId>
      <version>3.0.11.RELEASE</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.thymeleaf.extras</groupId>
      <artifactId>thymeleaf-extras-java8time</artifactId>
      <version>3.0.1.RELEASE</version>
    </dependency>
...
```
はい、いつものメンツに加えて今回は`resteasy-html`と`thymeleaf`を追加してます。
そして今回の台風の目、`thymeleaf-extras-java8time` を追加いたしました。

Thymeleaf ではそのままでは Java 8 から追加された `java.time.*` に対応できていないのですね。というわけで、`thymeleaf-extras-java8time`を追加いたしました。

## 2. モデルクラスとREST API インタフェースの作成

それでは実際のコーディングに入っていきます。

### 2-1. モデルクラス

ここはもう慣れたものでしょう。いつもの通り、Personモデルを定義いたします。

```java:src/main/java/org/acme/quarkus/sample/model/Person.java
package org.acme.quarkus.sample.model;

import javax.persistence.Entity;
import java.time.LocalDate;
import java.util.List;

import io.quarkus.hibernate.orm.panache.PanacheEntity;

import com.fasterxml.jackson.annotation.JsonFormat;

@Entity
public class Person extends PanacheEntity {
  public enum Status {
    Alive, DECEASED
  }
  
  public String name;
  @JsonFormat(pattern = "yyyy-MM-dd")
  public LocalDate birth;
  public Status status;

  public static Person findByName(String name) {
    return find("name", name).firstResult();
  }

  public static List<Person> findAlive() {
    return list("status", Status.Alive);
  }

  public static void deleteStefs() {
    delete("name", "Stef");
  }
}
```
いつもながら、モデルクラスの記述量の少なさは圧巻ですね。。

### 2-2. REST API インタフェースの実装

さて、REST API側です。

```java:
package org.acme.quarkus.sample;

...
import org.jboss.resteasy.plugins.providers.html.Renderable;

/**
 * PersonResource
 */
@Path("/person")
public class PersonResource {

    ThymeleafRenderer views;

    @Inject
    public PersonResource(ThymeleafRenderer views) {
        this.views = views;
    }

    @POST
    @Transactional
    @Produces(MediaType.APPLICATION_JSON)
    @Counted(name = "performed_create", description = "How many it have been called.")
    @Timed(name = "checksTimer_create", description = "A measure of how long it takes to perform creating person.", unit = MetricUnits.MILLISECONDS)
    public Person create(Person person) {
        person.persist();
        return person;
    }

    @GET
    @Path("/{id}")
    @Transactional
    @Produces(MediaType.TEXT_HTML)
    @Counted(name = "performed_html", description = "How many it have been called.")
    @Timed(name = "checksTimer_html", description = "A measure of how long it takes to perform getting person.", unit = MetricUnits.MILLISECONDS)
    public Renderable getHTML(@PathParam("id") Long id) {
        return views
            .view("person.html")
            .with("p", Person.findById(id));
    }    
}
```
コンストラクタで、`ThymeleafRenderer` を受け取るようにしましたが、これは下の手順で作成するThymeleafテンプレートエンジンのラッパーです。

そして今回は `create` と、`getHTML` の２つをご用意いたしました。
`create` はレコード追加用のAPIで、`getHTML`が HTML を返すAPIとなります。

`getHTML`の中身は Thymeleafというよりラッパークラスでの処理となっておりますので[上記でご紹介したサンプル](https://github.com/takami-h/quarkus-webui-example)を作ってくれた方の実装スタイルです。大変、美しいですね！

`getHTML` の戻り値ですが、Renderableを返却すると HTML として返してくれるようです。これは RestEasyHTML の機能ですね。

Rest API の実装はこのように非常にシンプルなものとなっております。

## 3. Thymeleaf テンプレートエンジンの使用

### 3-1. エンジンラッパーの実装

それでは本丸、Thymeleaf エンジンのラッパークラスの紹介です。・・・と言っても実装は[例のサンプルそのまんま](https://github.com/takami-h/quarkus-webui-example/blob/master/src/main/java/com/natswell/examples/quarkus/ThymeleafViews.java)なんですけれども・・・ありがとうございます。

```java:src/main/java/org/acme/quarkus/sample/ThymeleafRenderer.java
package org.acme.quarkus.sample;

...

import org.jboss.resteasy.plugins.providers.html.Renderable;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.WebContext;
import org.thymeleaf.extras.java8time.dialect.Java8TimeDialect;
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;

@ApplicationScoped
public class ThymeleafRenderer {

    private TemplateEngine templateEngine = new TemplateEngine() {{
        addDialect(new Java8TimeDialect());
        setTemplateResolver(new ClassLoaderTemplateResolver());
    }};

    public Renderer view(String path) {
        return new Renderer(templateEngine, "META-INF/resources/"+path);
    }

    public static class Renderer implements Renderable {
        private TemplateEngine templateEngine;
        private String path;
        private Map<String, Object> variables;
        public Renderer(TemplateEngine templateEngine, String path) {
            this.templateEngine = templateEngine;
            this.path = path;
            this.variables = new HashMap<>();
        }
        
        public Renderer with(String key, Object variable) {
            this.variables.put(key, variable);
            return this;
        }

        @Override
        public void render(HttpServletRequest request, HttpServletResponse response)
                throws IOException, ServletException, WebApplicationException {
            WebContext context = new WebContext(request, response, request.getServletContext());

            context.setVariables(variables);
            
            templateEngine.process(
                    path, context,
                    new OutputStreamWriter(response.getOutputStream(), StandardCharsets.UTF_8));
        }
    }
}
```
元の実装と違うのは `TemplateEngine` の初期化処理オーバーライドで `addDialect()` を追加しているところですね。ここで例の `Java8TimeDialect` を追加しております。

あとはそのままですがちょっと解説をいたしますと、内部クラスで `Renderable` を実装しておりまして、このResteasyから呼ばれるRenderableのrenderメソッドではしっかり、`HttpServletRequest`と`HttpServletResponse`が渡ってきております。`PersonResource#getHTML` の方では Servletの処理としっかり切り離されておりまして『テンプレートエンジン、どうすんだよ？』ってところでしたが、こうなれば普通のサーブレットでテンプレートエンジン使うのと全く一緒ですね。contextに設定した値を詰めて、`process`!! でOKです。

また、ファイルのパスについてですが、ネイティブ化の際に`META-INF/resources/`の中身は自動でインクルードしてくれるのに、パスを指定する場合はちゃんと`META-INF/resources/`をつけないとダメなのね。。。
というわけで、暗黙で`META-INF/resources/`を追加するようにいたしました。

Javaのコーディングは以上です！

### 3-2. テンプレートHTMLの記述

さて、HTMLの方ですが、Thymeleaf はデザイン時もレイアウトが崩れにくいのが特徴、というわけで無理やり Bootstrap を突っ込んでみました。

```html:src/main/resources/META-INF/resources/person.html
<!doctype html>
<html lang="ja">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">

    <title>Hello, world!</title>
  </head>
    <h2>Thymeleaf Template Example</h2>
        <ul>
            <li>Id: <span th:text="${p.id}" class="btn btn-primary">仮 ID</span></li>
            <li>Name: <span th:text="${p.name}" class="btn">仮名</span></li>
            <li>Birth: <span th:text="${#temporals.format(p.birth, 'yyyy/MM/dd')}" class="btn btn-primary">1990/01/01</span></li>
            <li>Status: <span th:text="${p.status}" class="btn">Alive</span></li>
        </ul>
    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
  </body>
</html>
```
本質的な部分は、`<span th:text="${p.name}">` のように Thymeleaf は独自属性で埋め込む値を指定しておくことで、HTMLとして破綻しないようにしている、という点ですね。

また、`<span th:text="${#temporals.format(p.birth, 'yyyy/MM/dd')}">` のように、`LocalDate` などを使う箇所は`#temporals`で定義されたメソッドを使って日付処理を行います。
あとは表示されてからのお楽しみ、ということですね！

### 3-3. reflectionconfig.json の作成

これは結果的にわかったことでございますが、ネイティブ化に当たってこの手順が必須でした。
`mvn quarkus:dev` で JVM 上で Thymeleaf を動かすだけであれば上までの手順でOKです。

ですが、テンプレートエンジンが**値を評価する**というのはコンパイラが知ったこっちゃない事柄なのです。
つまり、テンプレート上で呼び出される`temporals.format`のメソッド呼び出しなんて、ネイティブ化の時点ではわからないわけで、ネイティブ化の際には`temporals.format`メソッドなんてバイナリに含まれていないらしいのです。

以下の記事、見つけてよかった。。。

- [Building native Java Applications with GraalVM](http://dplatz.de/blog/2018/graal-native-app.html)

'reflectionconfig.json'というファイルを作成して"`temporals.format`が実行時には使用されますよ〜"ということをコンパイラにお知らせしておく必要がありました。

まず、以下のような json を用意します。

```json:src/main/resources/reflectionconfig.json
[
    {
        "name" : "java.time.LocalDate",
        "methods" : [
            { "name" : "format", "parameterTypes" : ["java.time.format.DateTimeFormatter"] }
        ]
    },
    {
        "name" : "org.thymeleaf.extras.java8time.expression.Temporals",
        "methods" : [
            { "name" : "format", "parameterTypes" : ["java.time.temporal.Temporal", "java.lang.String"] }
        ]
    }
]
```
このようにクラス名とメソッド名、そのシグネチャーをjson形式で書いておきます。~~`DateTimeFormatter`を試してみた記憶も残しておきます。~~

続いてこのファイルをビルド時の`-H:ReflectionConfigurationFiles`に指定します。

```properties:src/main/resources/application.properties
...
quarkus.native.additional-build-args=-H:ReflectionConfigurationFiles=../../src/main/resources/reflectionconfig.json
```
毎度、わかりにくいですが、Dockerでビルドさせるとmvn の環境変数がうまくハマってくれないので、パスの起点が `./target/xxxx-xxxx-xxxxx-jar/` (`/`終わりに注目) であるというのが注意点です。
Dockerfile で ENV しちゃってもいいんですけどね〜。。。

今回はこれだけでOKです。

が、つまり・・・Javaのコードには一切出てこないがテンプレート上で呼び出すメソッドに関しては逐一、このjsonにシグネチャー書いてかないとダメってことです。
まぁ、テンプレートで呼び出す処理というのはプログラマーがコーディングするんだから"ついでに設定ファイルにも使うメソッドメモっといてね"というのはしょうがないといえばしょうがないですが…テンプレート側で翻訳入れるとか単位の計算するとか、いわゆる`パイプ`っぽい処理をモリモリしようとするとこれは**けっこう厳しい**ということになりますね。せっかく${...}でガッツリ`式`が書けるというのにぃ！

## 4. ビルド用のコンテナ、DB、Jaeger などなど

ここは特に毎度のことなので割愛したいとおもいます。

github の方で、Dockerfile と docker-compose.yaml をチェックしてください。

## 5. ネイティブ化

こちらも上記で用意したdocker-compose.yml をビルドでOKです！

```shell
$ docker-compose build
...
$ docker-compose up
```
~~これ、ビルドに毎度、10分程度かかるのでトライ＆エラーもなかなか暇なんすよね・・・~~

## 6. 実行！！

それでは早速動作をみてみましょう。
まず、ターミナルの方から curl で jsonをPOSTします。

```shell
$ curl -H 'Content-Type:application/json' -d '{"name":"Alice","birth":"2010-10-11","status":"Alive"}' http://localhost:8082/person
{"id":1,"birth":"2010-10-11","name":"Alice","status":"Alive"}
```
ということで、IDに1が振られて、レコードがちゃんと生成されていることが確認できます。

続いてブラウザから`http://localhost:8082/person/1`を開いてみましょう。
![Hello world By Panache.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/75a2a205-6961-48f8-85dd-53a728000a23.png)
はい、ちゃんとLocalDateが`yyyy/MM/dd`で整形されております。
そして、このボタン。**これがBootstrapだ！！**

で、quarkusは静的なコンテンツもそのまま表示が可能です。
`http://localhost:8082/`では `src/main/resources/META-INF/resources/index.html`が表示されています。
というわけで、`http://localhost:8082/person.html` をブラウザに打ち込んで、すっぴんの`person.html`を確認してみましょう。
![Hello world By HTML.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/ee365325-0717-f4a5-a369-831456c0f4f7.png)
た、確かに・・・そのまんまだ。。。いや、そのまんますぎてなんと思わないですが、テンプレートで式が埋め込まれているのにデザインが崩れてないのはすごいことなんですよ？！
今回は非常に簡単な値の埋め込みだけでしたが、すっぴんのJSPやFreeMarkerはまともに表示できないですからね。。。
ブラウザでちゃんとCSSが効いた状態が確認できる、というのは大きいでしょう！！

## まとめ

とりあえず、動いてよかったです。いや、無理だと思ってました。。。
ちょっと設定ファイルに追記が必要なのはボトルネックですが、RuntimeErrorは解消できたのでよかったです。

今回の成果物も Github にあげておきました。

- [thymeleaf ブランチ | quarkus-examples](https://github.com/Yoshinori-Koide-PRO/quarkus-examples/tree/thymeleaf)

日本語フォント付きのPDFboxに続いての２連敗は避けたかったのでホッ。