QuarkusでHTMLを返す

Quarkusとresteasy-html + Thymeleafで、HTMLレスポンスを返すサンプルを作りました。

Quarkusは、主にJava EE APIのサブセットからなるフレームワークで、GraalVMにより実行可能バイナリを生成することができます。高速に起動し、省メモリでバイナリサイズも小さいので、コンテナやサーバレスのようにアプリをバラ撒いては上げ下げするような環境に適しています。

JSONを返すQuarkusサンプルは山ほどあるので、今回はHTML画面を返してみます(作ったもの)。

プロジェクトの生成は、Mavenプラグインを使います。

$ mvn io.quarkus:quarkus-maven-plugin:0.21.2:create \
  -DprojectGroupId=com.natswell.examples \
  -DprojectArtifactId=quarkus-webui \
  -DclassName="com.natswell.examples.quarkus.GreetingResource" \
  -Dpath="/greeting"

以下のコマンドで起動します。

$ mvn clean compile quarkus:dev
$ curl -s localhost:8080/greeting
hello

文字列でHTMLを返すのは辛いのでRESTEasyの text/html 向けプロバイダであるresteasy-htmlを使います。resteasy-htmlはJSPにフォワードすることでHTMLを返しますが、QuarkusではJSPはサポートされないのでThymeleafを使うことにします(当初、Freemarkerのサーブレットにフォワードさせてみたのですが、JVMでは動作したもののネイティブイメージのビルドができなかったため断念しました)。pom.xmlにresteasy-htmlとThymeleafを追加します。

<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>

ThymeleafによるHTML応答処理を追加します。resteasy-htmlでは、Renderableインタフェースの実装クラスとして、HTML生成処理を用意します(Servlet/JSPベースのものがorg.jboss.resteasy.plugins.providers.html.Viewクラス)。

public static class ThymeleafView implements Renderable {
    ...
    @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));
    }
}

JAX-RSでThymeleafViewを返すと、HTML応答が返ります。

@Path("/greeting")
public class GreetingResource {
    ...
    @GET
    @Produces(MediaType.TEXT_HTML)
    public Renderable helloHtml(@QueryParam("to") String someone) {
        String message = String.format("hello %s!", Objects.toString(someone, "WORLD"));

        return views.view("/templates/greeting.html").with("greeting", message);
    }
}

ビルドして、まずはJVMで動かしてみます。JVMでも1秒未満で起動します。

$ mvn clean compile quarkus:dev
$ curl -s -H 'Accept: text/html' localhost:8080/greeting?to=John%20Doe
<!doctype html>
 <html lang="ja">
     <head>
         <meta charset="UTF-8"/>
         <title>Quarkus - Thymeleaf Web UI</title>
     </head>
     <body>
         <p >hello John Doe!</p>
         <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
     </body>
 </html>

次にネイティブイメージで動かします。事前にGraalVMをインストールし、native-imageコマンドを入れておく必要があります。ビルドにはかなり時間がかかります(使用したMacBook Pro Early 2015で2分強)が、バイナリのサイズは23MB(ちなみにJARファイルの方は119KBですが、別途JVMが必要です)、起動は0.006秒でした。

$ mvn -Pnative clean package
$ ./target/quarkus-webui-1.0-SNAPSHOT-runner
$ curl -s -H 'Accept: text/html' localhost:8080/greeting?to=John%20Doe
> 結果は同じなので省略

バイナリをビルドして実行する際にエラーが出たため、native-imageコマンドの引数をpom.xmlに追加しています。ビルドエラー回避のため、--initialize-at-run-time--allow-incomplete-classpathを指定し、Thymeleafテンプレートをバイナリに入れるために-H:IncludeResourcesを指定しました。

<plugin>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-maven-plugin</artifactId>
  <version>${quarkus.version}</version>
  <executions>
    <execution>
      <goals>
        <goal>native-image</goal>
      </goals>
      <configuration>
        <enableHttpUrlHandler>true</enableHttpUrlHandler>
        <additionalBuildArgs>
          <additionalBuildArg>--initialize-at-run-time=org.thymeleaf.standard.serializer.StandardJavaScriptSerializer$JacksonStandardJavaScriptSerializer</additionalBuildArg>
          <additionalBuildArg>--allow-incomplete-classpath</additionalBuildArg>
          <additionalBuildArg>-H:IncludeResources=.*\.html$</additionalBuildArg>
        </additionalBuildArgs>
      </configuration>
    </execution>
  </executions>
</plugin>

起動が一瞬なことや小さなメモリフットプリントはコンテナでなくとも魅力的です。一方で、GraalVMによるバイナリ生成はかなり制約があり、既存のJava資産をそのまま持ち込めるといった性質のものではないようで、使用できるライブラリをかなり選びます。IDEでコンパイルエラーも出ずテストも通っているのにネイティブイメージの失敗でコケる、といったことがあるのが辛いところです。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です