🍇

話題騒然!に全然なっていない Java x TypeScript な Web フレームワーク "Hilla" の紹介

2022/05/01に公開

はじめに

https://hilla.dev/

あまり話題になっていない Web フレームワーク "Hilla"[1] を紹介したいと思います(もう少し話題になってほしい)。

BFF を Java で作りたい、または作らざるを得ない人向けなので該当しない方は、ここで exit です。Hilla は、SSR とかないので SEO が不要なシステムが対象になるかと思います。

Hilla の最大の特徴は、TypeScript クライアントのコード自動生成により、BFF(Java)とフロントエンド(TypeScript)でタイプセーフな通信を実現している点だと思います。

概要を掴むためには最初に以下の記事を読んでもらうと良いかもしれません。。
https://www.infoq.com/jp/news/2022/04/vaadin-introduces-hilla/

フレームワーク構成

フレームワークの構成は、以下のようになっています。

  • BFF
    • Spring Boot
  • Frontend
    • Lit
    • Vaadin Web UI Components

かなりフルスタックな構成です。フレームワーク、ライブラリの構成に悩むこと少ないでしょう。開発時のビルド、リリース時のビルドも一気通貫で考慮されており、そこでも悩むことが少ないでしょう。

Spring Boot

https://spring.io/projects/spring-boot

Spring Boot は、Java サーバサイドの Web フレームワークです。
定番すぎるので多くは語りません。

Lit

https://lit.dev/

Lit は、Web Component を扱うライブラリです。
HTMLテンプレートに、以下のように Tagged Template Literals が利用されています。

const header = (title: string) => html`<h1>${title}</h1>`;

React などの JSX に比べタイプセーフさがないのではないかと思うのですが、それについては後述したいと思います。

Vaadin Web UI Components

https://vaadin.com/components

Vaadin の豊富な UI コンポーネントが利用できます。

Quickstart

Quickstart を解説したいと思います。 最終的な完成品は、こちらです。

まずは、以下のコマンドでプロジェクトを作成します。

$ npx @vaadin/cli init --preset hilla-quickstart-tutorial hilla-grocery-app
$ cd hilla-grocery-app

BFF モデルクラス

BFF のリクエスト、レスポンスで使用するモデルクラスsrc/main/java/com/example/application/GroceryItem.javaを作成します。

package com.example.application;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

public class GroceryItem {

    @NotBlank 
    private String name;

    @NotNull
    @Min(value = 1) 
    private Integer quantity;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getQuantity() {
        return quantity;
    }

    public void setQuantity(int quantity) {
        this.quantity = quantity;
    }
}

Java 16 で導入された record が使えるのは未検証です。

BFF Endpoint クラス

リクエストを処理する Endpoint クラスsrc/main/java/com/example/application/GroceryEndpoint.javaを作成します。

package com.example.application;

import java.util.ArrayList;
import java.util.List;
import com.vaadin.flow.server.auth.AnonymousAllowed;
import dev.hilla.Endpoint;
import dev.hilla.Nonnull;

@Endpoint 
@AnonymousAllowed 
public class GroceryEndpoint {

  private final List<GroceryItem> groceryList = new ArrayList<>();

  public @Nonnull List<@Nonnull GroceryItem> getGroceries() { 
    return groceryList;
  }

  public GroceryItem save(GroceryItem item) {
    groceryList.add(item);
    return item;
  }
}

通常の Spring Boot アプリならば @Controller,@RestController を使うと思いますが、Hilla では @Endpoint を使います。
getGroceriesがデータを取得するメソッド、saveがデータを登録するメソッドとなります。

パッケージ違いの同名の Endpoint クラスは作成できないようです。以下のようなエラーが出力されます。

Caused by: org.springframework.context.annotation.ConflictingBeanDefinitionException: Annotation-specified bean name 'groceryEndpoint' for bean class [com.example.application.foo.GroceryEndpoint] conflicts with existing, non-compatible bean definition of same name and class [com.example.application.GroceryEndpoint]

エラーで教えてくれるので親切かと思います。
それに対してパッケージ違いの同名モデルクラスは作成可能です。Endpoint クラスの内部クラスとしてモデルクラスを作成しても問題なく動くようです。

TypeScript のクライアントコードの自動生成

以下のコマンドでアプリケーションを起動します。

./mvnw

開発起動時またはリリースビルド時に TypeScript のクライアントコードが自動生成されます。
以下のようなコードが生成されます。このコードを利用することによりタイプセーフな通信が可能になります。

$ tree frontend/generated/
frontend/generated/
├── GroceryEndpoint.ts
├── com
│   └── example
│       └── application
│           ├── GroceryItem.ts
│           └── GroceryItemModel.ts
├── connect-client.default.ts
├── endpoints.ts
├── theme-hilla-grocery-app.generated.js
├── theme.d.ts
├── theme.js
├── vaadin-featureflags.ts
├── vaadin.ts
└── vite-devmode.ts

この記事で言いたいことのすべては、ここです。
このコードを生成する仕組みがフレームワークとして非常に綺麗に提供されています。

自動生成されたコードを利用する

frontend/views/grocery/grocery-view.ts が存在していますので以下の内容で上書きます。

import '@vaadin/button';
import '@vaadin/text-field';
import '@vaadin/number-field';
import '@vaadin/grid/vaadin-grid';
import { html } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { View } from 'Frontend/views/view';
import { Binder, field } from '@hilla/form';
import { getGroceries, save } from 'Frontend/generated/GroceryEndpoint';
import GroceryItem from 'Frontend/generated/com/example/application/GroceryItem';
import GroceryItemModel from 'Frontend/generated/com/example/application/GroceryItemModel';

@customElement('grocery-view') 
export class GroceryView extends View { 

  @state()
  private groceries: GroceryItem[] = []; 
  private binder = new Binder(this, GroceryItemModel); 

  render() {
    return html`
      <div class="p-m">
        <div>
          <vaadin-text-field
            ${field(this.binder.model.name)}
            label="Item"> </vaadin-text-field> 
          <vaadin-number-field
            ${field(this.binder.model.quantity)}
            has-controls
            label="Quantity"></vaadin-number-field> 
          <vaadin-button
            theme="primary"
            @click=${this.addItem}
            ?disabled=${this.binder.invalid}>Add</vaadin-button> 
        </div>

        <h3>Grocery List</h3>
        <vaadin-grid .items="${this.groceries}" theme="row-stripes" style="max-width: 400px">
          
          <vaadin-grid-column path="name"></vaadin-grid-column>
          <vaadin-grid-column path="quantity"></vaadin-grid-column>
        </vaadin-grid>
      </div>
    `;
  }

  async addItem() {
    const groceryItem = await this.binder.submitTo(save); 
    if (groceryItem) { 
      this.groceries = [...this.groceries, groceryItem];
      this.binder.clear();
    }
  }

  async firstUpdated() { 
    const groceries = await getGroceries();
    this.groceries = groceries;
  }
}

これで Quickstart アプリの完成です。

Lit の HTML テンプレートの静的解析について

VS Code で開発する場合は、 プロジェクトに lit-plugin が最初から組み込まれていてテンプレート部分の静的解析を行ってくれます。しかしながら、リリースビルド時の処理にはこの静的解析が含まれていません。以下の手順で CLI の lit-analyzer を実行するようにします。

以下のコマンドで lit-analyzer をインストールします。

npm install lit-analyzer -D

package.json に以下の内容を追加します。

  "scripts": {
    "lint:lit-analyzer": "lit-analyzer frontend/views --strict"
  },

以下のコマンドで実行できます。

npm run lint:lit-analyzer

このコマンドの実行をリリース時のビルドの中に組み込みます。

Gradle Plugin

現在(2022-05-01)は、maven 用の plugin しか提供されておらず、 Gradle 用の plugin はありません。以下の Issue の状況を Watch しています。
https://github.com/vaadin/hilla/issues/141

終わりに

以前、私は以下の記事を書きました。
https://qiita.com/chibato/items/e4a748db12409b40c02f

Springfox[2]OpenAPI Generator を組み合わせ、TypeScript のコードを生成していました。
Hilla は、このコード生成する仕組みをフレームワークとして非常に綺麗に提供しています(2回目)。

とても良いフレームワークだと思いますぜひ使ってみていただければと思います。[3]

余談

  • リリースビルド後の jar の中に swagger-codegen も含まれていて、本番実行時に不要なのではないかと思った。
  • swagger-codegen ではなく、openapi-generator ではダメだったのだろうか。
  • 内部的な OpenAPI の Spec 情報の生成には、springfox, springdoc-openapi を使っていない。JavaParser というのを使っている。
  • 開発時の起動でフロントエンドと BFF で別にサーバ上げて request forward みたいなことはしてない。

以上です。

脚注
  1. もともと Hilla は、古くからある Vaadin というフレームワークのサブプロジェクト的に "Vaadin Fusion" という名前で開発されていました。2022年にリネームされました。 ↩︎

  2. 今ならば springdoc-openapi を使うのでしょう。 ↩︎

  3. そういう私もまだ実案件では使っていない。 ↩︎

Discussion