🍔

Java用WebフレームワークHillaって知ってる?

2022/05/17に公開

始めに

Javaバックエンド実装すると自動で型を生成してくれて、TypeScriptフロントサイドでそのまま型を使える。さらにバックエンド側で実装した関数をTypeScriptフロントサイドで呼び出してサーバーへのリクエストもうまくやってくれる... おもろい...

TypeScript少し触ったことあるし、Javaやってるし、、面白そう。と思ったのでHillaチュートリアルに取り組んでどんなものか調査してみました。
「理解した」わけではないのでこういうのあるんだぁくらいで見ていただけると助かります。
Javaは2022/05より学習始めた素人です...

参考にしたのは以下Youtube動画
https://youtu.be/kykdX-cUv1I

公式Docsにもチュートリアルあります
https://hilla.dev/

本記事のソースコード
https://github.com/developeeeer/hilla-youtube-tutorial

Hillaって?

Hillaは、JavaバックエンドでリアクティブなWebアプリを構築するためのフレームワーク。
Lit TypeScriptフロントエンドとSpringBootバックエンドをシームレスに統合する。

REST: REST API作る → 型情報を記述 → フロントよりAPI叩く(fetch, axios...)
Hilla: Endpointの実装 → Hillaが自動で型情報&関数の生成 → フロントは生成された関数を呼び出すだけ。生成された関数を呼び出しているだけだがサーバーとの通信も勝手にやってくれる。すばら。

所感

  1. 個人で開発してみた感じ便利そう。
    Javaでバックエンド実装 → フロントサイドへ自動的に型情報がジェネレートされておりシームレスに開発ができるところより個人で開発している分には型安全ですごく開発しやすい印象でした。

  2. 学習コストかかりそう
    全くの未経験だと、TypeScript, Java, SpringFramework, Vaadin Component, Lit...もちろん僕は「Hilla理解した」とは言えないレベルですが、結構勉強することは多そうですね。

  3. まだ普及していない?
    あまりHilla(旧Vaadin Fusion)について話を聞かないので、まだ知らない方も多い。そのため資料や記事等も少ない。会社で使われることも多くないはず。コアだねぇと言われたい方は触ってみましょう。

コア「Javaやってます」
おじ「なんのフレームワーク使ってる?」
コア「Hillaですね」
おじ「...」

1. プロジェクトの作成

  1. 以下のコマンドを入力してプロジェクトを作成します。
npx @vaadin/cli init --preset hilla-quickstart-tutorial hilla-grocery-app
  1. Maven Wrapperを使用してMavenをインストール & プロジェクトを実行する
# プロジェクト内で実行
./mvnw
  1. localhost:8080より起動確認
    以下のような画面になっているかと思います。(プロジェクト名により表記が異なる部分もあります)

2. Java Endpointクラスの追加

  1. Endpointクラスを新規作成
src/main/java/com/example/application/GroceryEndpoint.java
package com.example.application;
import com.vaadin.flow.server.auth.AnonymousAllowed;
import dev.hilla.Endpoint;

@Endpoint
@AnonymousAllowed
public class GroceryEndpoint {
    public String greet(String message) {
        return "Server says: " + message;
    }
}

@Endpoint: クライアント側のViewのサービスとして公開される。エンドポイントの全てのパブリックメソッドがTypeScriptから呼び出せるようになる。(JavaなのにTypescriptから呼び出せる!!)
@AnonymouseAllowed: デフォルトで認証済みユーザーしかアクセスできないが誰でもアクセスできうように変更する。

3. JavaパブリックメソッドをTypeScriptから呼び出す。

  1. 以下のファイルに追記します。
frontend/index.ts
import { Router } from '@vaadin/router';
import { routes } from './routes';
import { appStore } from './stores/app-store';

export const router = new Router(document.querySelector('#outlet'));

router.setRoutes(routes);

window.addEventListener('vaadin-router-location-changed', (e) => {
  appStore.setLocation((e as CustomEvent).detail.location);
  const title = appStore.currentViewTitle;
  if (title) {
    document.title = title + ' | ' + appStore.applicationName;
  } else {
    document.title = appStore.applicationName;
  }
});

+ GroceryEndpoint.greet('Hello World!').then((msg) => console.log(msg));
  1. ブラウザのDevToolよりConsole出力を確認してみてください。出力が確認できれば成功です。

4. Dependenciesの追加

  1. h2の追加
pom.xml
+ <dependency>
+     <groupId>com.h2database</groupId>
+     <artifactId>h2</artifactId>
+ </dependency>
  1. spring-boot-starter-data-jpaの追加
pom.xml
+ <dependency>
+     <groupId>org.springframework.boot</groupId>
+     <artifactId>spring-boot-starter-data-jpa</artifactId>
+     <version>2.6.7</version>
+ </dependency>

VisualStudioCodeを使用している方は各種拡張機能を入れて、画面左下のMaven > Dependencies > + > artifactIdで検索して追記することができます。

4. Entity Repositoryの追加, Endpointの修正

Endpointは前回以下のものに置き換える形で問題ありません。

  1. Entityクラスの作成
src/main/java/com/example/application/GroceryItem.java
package com.example.application;

import java.util.UUID;

import javax.persistence.Id;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;


@Entity
public class GroceryItem {
    @Id
    @GeneratedValue
    private UUID id;
    
    public UUID getId() {
        return id;
    }
    public void setId(UUID id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Integer getQuantity() {
        return quantity;
    }
    public void setQuantity(Integer quantity) {
        this.quantity = quantity;
    }
    @NotBlank
    private String name;
    @Min(value = 1)
    private Integer quantity;
    
}

  1. Repositoryインターフェイスの追加
src/main/java/com/example/application/GroceryRepository.java
package com.example.application;

import java.util.UUID;

import org.springframework.data.jpa.repository.JpaRepository;

public interface GroceryRepository extends JpaRepository<GroceryItem, UUID> {
    
}

  1. Endpointの修正
src/main/java/com/example/application/GroceryEndpoint.java
package com.example.application;

import java.util.List;

import javax.annotation.Nonnull;

import com.vaadin.flow.server.auth.AnonymousAllowed;

import dev.hilla.Endpoint;

@Endpoint
@AnonymousAllowed
public class GroceryEndpoint {

    private GroceryRepository repository;

    public GroceryEndpoint(GroceryRepository repository) {
        this.repository = repository;
    }

    public @Nonnull List<@Nonnull GroceryItem> findAll() {
        return repository.findAll();
    }

    public GroceryItem save(GroceryItem item) {
        return repository.save(item);
    }
}

5. TypeScriptでフロントの実装

  1. viewの編集
frontend/views/grocery/grocery-view.ts
import { html } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { View } from '../../views/view';
import '@vaadin/text-field';
import '@vaadin/number-field';
import '@vaadin/grid';
import GroceryItem from '../../generated/com/example/application/GroceryItem';
import { GroceryEndpoint } from 'Frontend/generated/endpoints';
import { Binder, field } from '@hilla/form';
import GroceryItemModel from '../../generated/com/example/application/GroceryItemModel';

@customElement('grocery-view')
export class GroceryView extends View {
  @state() groceryItems: GroceryItem[] = [];
  private binder = new Binder(this, GroceryItemModel);

  render() {
    const { model } = this.binder;

    return html`
      <div>
        <vaadin-text-field label="Item" ${field(model.name)}></vaadin-text-field>
        <vaadin-number-field label="Quantity" ${field(model.quantity)}></vaadin-number-field>
        <vaadin-button theme="primary" @click=${this.addItem}>Add</vaadin-button>
      </div>
      <vaadin-grid .items=${this.groceryItems}>
        <vaadin-grid-column path="name"></vaadin-grid-column>
        <vaadin-grid-column path="quantity"></vaadin-grid-column>
      </vaadin-grid>
    `;
  }

  async addItem() {
    const saved = await this.binder.submitTo(GroceryEndpoint.save);
    if (saved) {
      this.groceryItems.push(saved);
      this.binder.clear();
    }
  }

  async connectedCallback() {
    super.connectedCallback();
    this.classList.add('flex', 'flex-col', 'h-full', 'p-l', 'box-border');

    this.groceryItems = await GroceryEndpoint.findAll();
  }
}

6. その他

  1. Javaで実装したメソッドがなぜTypeScriptで呼び出せるのか。型安全なのか。
    詳細は置いておいて、、、
    importの際に気づいた方もいるかと思いますが、frontend/generatedフォルダ内に自動生成されています。

Java -> TypeScript (GroceryEndpoint.java -> GroceryEndpoint.ts)

Java(GroceryEndpoint.java)
package com.example.application;

import java.util.List;

import javax.annotation.Nonnull;

import com.vaadin.flow.server.auth.AnonymousAllowed;

import dev.hilla.Endpoint;

@Endpoint
@AnonymousAllowed
public class GroceryEndpoint {

    private GroceryRepository repository;

    public GroceryEndpoint(GroceryRepository repository) {
        this.repository = repository;
    }

    public @Nonnull List<@Nonnull GroceryItem> findAll() {
        return repository.findAll();
    }

    public GroceryItem save(GroceryItem item) {
        return repository.save(item);
    }
}

TypeScript(GroceryEndpoint.ts)
/**
 * This module is generated from GroceryEndpoint.java
 * All changes to this file are overridden. Please consider to make changes in the corresponding Java file if necessary.
 * @see {@link file:///Users/ym/Documents/source/hilla-grocery-app/src/main/java/com/example/application/GroceryEndpoint.java}
 * @module GroceryEndpoint
 */

// @ts-ignore
import client from './connect-client.default';
import type GroceryItem from './com/example/application/GroceryItem';

function _findAll(): Promise<Array<GroceryItem>> {
 return client.call('GroceryEndpoint', 'findAll');
}

function _save(
 item: GroceryItem | undefined
): Promise<GroceryItem | undefined> {
 return client.call('GroceryEndpoint', 'save', {item});
}
export {
  _findAll as findAll,
  _save as save,
};

Discussion