※本記事は、Andrew Oliverによる”Java in the Browser with TeaVM“を翻訳したものです。


Javaをフロントエンドとバックエンドの両方に使ってWebアプリを構築する

著者:Andrew Oliver

2019年10月8日

Javaがブラウザの中で実行されていた頃を覚えている方もいるかもしれません。その頃は、Webページから「Pure Java」のUIを簡単に起動できました。ユーザー・インタフェースとバックエンドを強く型付けされた同じ言語で開発することや、データ・クラスや検証ロジックを共有することもできました。また、コード品質ツールをフロントエンドとバックエンドの両方のコードに対して適用することもできました。いい時代でした。

しかし最近、ブラウザ・ベンダーはJavaScriptに重きを置くようになり、Javaのサポートをだんだん減らしてきました。クライアント側のWebアプリに支配された世の中では、Javaはサーバー側のみのテクノロジーに格下げされてしまっているようです。フルスタックJavaエクスペリエンスのメリットは、失われています。開発者は複数の言語やツールセットを行き来しなければならないため、開発の生産性は急降下します。サーバー側のリファクタリングがクライアント側で見逃されることや、その逆のことが起きた場合、エラーが紛れ込むことになります。コードの品質を保つための優れたJavaツールセットも、クライアント側で再発明せざるを得なくなります。これは大きな後退であるように感じられます。

 

解決策

うれしいことに、解決策があります。オープンソース・プロジェクトTeaVMは、受け取ったJavaコードをビルド時にコンパクトで高速なJavaScriptに変換するものです。多くのWeb APIのラッパーが提供されているため、DOMの操作からユーザーの位置情報の取得まで、コードでブラウザを完全に操作できます。また、Flavourという、Webページ・テンプレート用の軽量フレームワークも含まれています。Flavourには、JSONバインディングを含め、JAX-RS Webサービスを簡単に呼び出す仕組みが組み込まれています。

TeaVMの考え方はGoogle Web Toolkit(GWT)に似ています。いずれのプロダクトでもJavaでコードを書くことができ、ブラウザと親和性があるJavaScriptが生成されます。しかし、柔軟性、パフォーマンス、Webネイティブ性という領域では、TeaVMがGWTを上回っています。TeaVMではKotlinやScalaなどのすべてのJVM言語をサポートしていますが、GWTでサポートしているのはJavaのみです。TeaVMでは、GWTよりコードのビルドが高速であるだけでなく、GWTよりコンパクトでブラウザでのパフォーマンスもよいJavaScriptが生成されます。さらに、おそらくもっとも重要なポイントは、TeaVMのFlavourライブラリにより、アプリのコンテンツやコンポーネントをHTMLとCSSで構築できるため、開発者とデザイナーが連携して作業できることです。

フロントエンドのTeaVMとバックエンドの従来のJavaサービスとを組み合わせれば、フルスタックのJavaを取り戻すことになります。すべてをJavaでコーディングすることができます。UIとサーバーでクラスを共有することができ、サーバーで名前をリファクタリングすればUIもリファクタリングされます。すべてのビジネス・ロジックの単体テストをJUnitで書くこともできます。PITミューテーション・テストでテスト品質を計測することや、PMDCheckstyleなどのユーティリティでコード品質を確認することも可能です。

 

使ってみる

TeaVM Flavourアプリは、標準Maven Webプロジェクトにいくつかの追加を行ったものです。アプリケーションのメイン・ページは、おなじみのwebapp/index.htmlです(慣例的に、CSSはwebapp/css/app.cssにあります)。ただし、メイン・ページは簡単なものであることが多く、通常はresources/templatesフォルダからHTMLテンプレート(ページ断片)を読み込みます。このHTMLテンプレートは、Javaのビュー・クラスと1対1になるようにリンクされています。ページのビジネス・ロジックを提供するのは、このビュー・クラス(src/main/javaにあります)です。ビュー・クラスには、ビジネス・ロジック、テンプレートで使われるプロパティ、バインディングが含まれています。さらに具体的に言えば、ビュー・クラスでは、イベントへの応答、RESTサービスとの通信、表示されるテンプレートの変更など、何でも行うことができます。

このセクションでは、単純なTeaVMアプリケーションをゼロから立ち上げる方法について説明します。その中で、Javaベースのビジネス・ロジックにリンクしたHTMLテンプレートを作成します。このビジネス・ロジックが、ブラウザの中だけでユーザーのアクションに応答します。

使用を開始するもっとも早い方法は、Mavenアーキタイプを使ってTeaVM Flavourプロジェクトを作成することです。

mvn archetype:generate \
    -DarchetypeGroupId=org.teavm.flavour \
    -DarchetypeArtifactId=teavm-flavour-application \
    -DarchetypeVersion=0.2.0

グループとパッケージにcom.mycompanyを、アーティファクトIDにflavourを使った場合、主なファイルは次のようになります。

  • src/main/java/com/mycompany/Client.java:このファイルには、アプリのJavaロジック(ビューと呼ばれます)が含まれます。また、テンプレート(次の項目で説明します)で使われるプロパティ(名前)も含まれます。
  • src/main/resources/templates/client.html:アプリのHTMLテンプレートです。nameフィールドにバインドされる、入力フィールドとHTMLテキストが含まれます。入力を変更すると、HTMLが変わります。後ほどルーティングとコンポーネントのセクションで説明しますが、HTMLテンプレートとビューの2つは、Flavourのあらゆる場面で使用される基本要素です。
  • src/main/webapp/css/app.css:アプリのCSSです。
  • src/main/webapp/index.html:アプリケーションのラッパーHTMLページです。ここでは大したことはしておらず、実際のアクションはテンプレートで行われます。

packageゴールを使ってプロジェクトをビルドします。

mvn clean package

Webアプリのファイルで、パッケージが指定されていないものは、target/flavour-1.0-SNAPSHOT/に格納されることになります。ブラウザからindex.htmlファイルを開いて、アプリが動作していることをすぐに確認できます。 プラグインやダウンロードは必要なく、Javaアプリがブラウザで直接実行されます。

アプリをTomcatにデプロイする場合は、target/flavour-1.0-SNAPSHOT.warを使います。または、${TOMCAT_HOME}/webapps/flavourflavour-1.0-SNAPSHOTフォルダへのシンボリック・リンクにすることもできます。シンボリック・リンクの方法を使った場合は、http://localhost:8080/flavourからアプリにアクセスすることができます(Tomcatがデフォルト・ポートの8080で動作するように構成している場合)。

 

Flavourを使ったリスト表示

UIに項目のリストを表示したいことはよくあります。Flavour要素<std:foreach>を使うことで、短いリストや中規模のリストを表示できます。このFlavour要素には、Javaでの通常のforeachループと同じように、反復処理するコレクションと、ループ本体をスコープとするコレクション要素の変数名を渡します。

コレクションはビュー・クラスから読み取られます。public List<Song> getSongs()というgetterで歌のリストを定義している場合、次のようにして単純なリストを作成することができます(本記事で紹介しているすべてのコードは、ダウンロード・サイトで公開しています)。次に示すのは、listプロジェクトのclient.htmlのコードです。

<ol>
  <std:foreach var="song" in="songs">
    <li>
      <html:text value="song.name"> by
      <strong><html:text value="song.artist"></strong>
    </li>
  </std:foreach>
</ol>

Flavourで生成される順序付きリスト(ol)には、getSongs()が返すすべてのSongに対応するリスト項目(li)が存在しています。リストの各項目には、歌の名前とアーティストが含まれます。<html:text>要素では、value式が評価され、その結果がページに挿入されます。Flavourの式言語を使うことで、明示的にgetterやsetterを呼び出さずに、JavaBeansスタイルのプロパティ(getX/setX)に名前を使ってアクセスすることができます。そのため、song.getName()と書く代わりに、song.nameと簡略化して書くことができます。

次は、それぞれの機能について詳しく見ていきます。

 

その他の標準コンポーネント

Flavourでは、std:foreachの他にも、ページのコンテンツを制御する多くの標準コンポーネントをサポートしています。

  • std:ifを使うことで、ブール条件に基づいてページにコンテンツを追加できます。
  • attr:xyzを使うことで、式を使って属性xyzを設定できます。たとえば、attr:classにより、要素のクラスを動的に設定できます。

標準コンポーネントは、次に紹介するFlavourの式言語と密接に連携しています。

 

式言語

Flavourコンポーネントの属性は、式言語(EL)を使って指定します。ELはJavaに似ていますが、いくつかのシンタックス・シュガーが追加されており、HTMLページでうまく動作させるための若干の調整も行われています。

以下を含め、すべての標準プリミティブがサポートされています。

  • 文字列(HTML内で簡単に使えるように、一重引用符を使用)
  • 数値(整数と浮動小数点数)
  • ブール値(trueとfalse)

ビュー・クラスのメソッドとプロパティは、名前を使って呼び出すことができます。Javaと同様に、メソッド呼出しにはパラメータが必要です。ただし、プロパティは名前で直接参照できます。先ほど紹介したように、titleと指定するだけで、FlavourがgetTitle()を呼び出してくれます。

次に示すのは、ELの使い方を説明したいくつかの標準コンポーネントの例です(standard-components-elclient.html)。

<!-- ブール値プロパティshowHeadingを使用 -->
<std:if condition="showHeading">
  <h1>Flavour Messenger</h1>
</std:if>
 
<!-- messageが入力されたときにボタンを有効化 -->
<!-- message.emptyはELの省略記法で、message.isEmpty()を表す -->
<button html:enabled="!message.empty">Check Spelling</button>

 

イベントへの応答

UIは、ユーザーの操作に応答できなければ完成とは言えません。Flavourでevent属性を使うことにより、DOMイベントが発生したときにJavaメソッドを呼び出すことができます。一般的に使われるオプションには、以下のものがあります。

  • event:click:クリック・イベントのハンドリング
  • event:change:コンポーネントの値変更のハンドリング

テンプレートで、ボタンをクリックしたときにsendメソッドを呼び出したい場合を考えてみます。次のようにして、単純にevent:click属性をボタンに追加します。

<button event:click="send()">Send</button>

バインディング

バインディングを行うことで、ビュー・クラスのプロパティを表示コンポーネントにリンクすることができます。バインディングには、いくつかの種類があります。ビュー・クラスのプロパティが変更されたときにUIが更新されるようにするものもあれば、ユーザーがUIコンポーネントを操作したときにビュー・クラスのプロパティが更新されるようにするものもあります。双方向バインディングは、この2つを組み合わせて、ビュー・クラスのプロパティ、またはUIのいずれが変更された場合でも両方を同期させるものです。特によく使われるものを紹介します。

  • html:textでは、ビュー・クラスのプロパティに基づいてテンプレートにHTMLが出力されます。
  • html:valueでは、ビュー・クラスのプロパティの変更に基づいて入力コンポーネントが更新されます。
  • html:changeでは、入力値が変更されたときにビュー・クラスのメソッドが呼び出されます。
  • html:bidir-valueはもっとも強力です。html:valuehtml:changeを組み合わせて、UIの入力フィールド、またはビュー・クラスのプロパティのいずれが変更されても両者を同期させます。

先ほどのファイルには、次の内容が含まれています。

<form>
<!-- 値が変更されると、messageプロパティからの読取りまたはmessageプロパティへの書込みを行う -->
  <input type="text" html:bidir-value="message">
</form>

 

ルーティング

単一ページ・アプリケーション(SPA)には複数の画面があり、サーバーにリクエストせずにブラウザ内で切り替わるようになっています。Flavourでは、ルーティング機能によってSPAを完全にサポートしています。ルーティングには、いくつかの要素が関連します。

  • ルート・インタフェース:画面とそのURLを定義する
  • ルート実装:オンデマンドで画面のインスタンスを生成する
  • 画面のHTMLテンプレート:1画面につき1つ存在し、レイアウトとコンポーネントを含む
  • ビュー・クラス:1つのテンプレートにつき1つ存在し、テンプレートのデータとイベント・ハンドラを提供する

レストランの一覧を表示する画面と、それぞれのレストランの詳細画面があるものとします。その場合、レストラン一覧とレストラン詳細のテンプレートを作成できます。それぞれのテンプレートにビュー・ページが存在することになります。ルート・インタフェース(サンプル・コードではApplicationRoute.java)は次のようになります。

@PathSet
interface ApplicationRoute extends Route {
  @Path("/restaurants")
  public void restaurantList();
 
  @Path("/restaurant/{id}")
  public void restaurantDetails(@PathParameter("id") int id);
}

レストランの詳細を表示するrestaurantDetailsページに、レストランID用のパラメータがどのように含まれているかに注意してください。

Routeの実装には、ページを切り替えるロジックが含まれています。慣例的に、RouteClient.javaのメイン・ページで実装しています。

@BindTemplate("templates/client.html")
public class Client extends ApplicationTemplate 
  implements ApplicationRoute {
    RestaurantSource source = new RestaurantSource();

    public static void main(String[] args) {
        Client client = new Client();
        new RouteBinder()
            .withDefault(ApplicationRoute.class,
                         route -> route.restaurantList())
            .add(client)
            .update();
        client.bind("application-content");
    }

    @Override
    public void restaurantList() {
        setView(new RestaurantListView(source));
    }

    @Override
    public void restaurantDetails(int id) {
        setView(new RestaurantDetailsView(source, id));
    }
}

レストラン一覧ページ(routingrestaurant-list.html)では、std:foreachを使ってレストランの一覧を表示します。各レストランには、対応するレストラン詳細ページへのリンクが含まれます。

<div>
  <h1>Restaurants</h1>
  <ol>
    <std:foreach var="restaurant" in="restaurants">
      <li>
        <button type="button"
                event:click="showRestaurant(restaurant.id)">
          <html:text value="restaurant.name"/>
        </button>
      </li>
    </std:foreach>
  </ol>
</div>

レストラン詳細ページ(restaurant-detail.html)では、1つのレストランについての情報を表示します。ここでは、<html:text>を使ってレストラン・オブジェクトのフィールドを表示しています。

<div>
  <h1><html:text value="restaurant.name"/></h1>
  <h3>In business for 
    <html:text value="restaurant.yearsInBusiness"/> years!</h3>
  <p></p>
  <p><button type="button" event:click="showList()">
  Return to the restaurant list</button></p>
</div>

 

RESTful API

ほとんどのWebアプリケーションではサーバーと通信します。よく使われるのは、リモート・サービスの起動、データの保存、アップデートの取得などです。

Flavourでは、簡単にWebサービスにアクセスする方法を提供しています。RESTClientクラスにより、JAX-RSサービスとして宣言したJSONベースのWebサービス用のクライアントを構築できます。JSONとJAX-RSの人気を考えれば、皆さんのWebサービスがこのクラスに対応している可能性も高いはずです。UIモジュールのPOMファイルにJAX-RSインタフェースを含めるだけで、1行のコードを書くことによりFlavourからインスタンスを生成できます。

YourService service =
    RESTClient.factory(YourService.class)
              .createResource("api");

レストランの例の続きとして、サービスにレストランの一覧を取得するメソッドがあるものとします。

List getRestaurants();

このメソッドは、次のようにして呼び出すことができます。

List restaurants = service.getRestaurants();

以上です。クライアント側でレストランの一覧を入手できたことから、先ほどのセクションのようにしてビューで使用できます。

近日中に予定されているTeaVMバージョン0.6リリースでは、REST関係の小さな変更が行われることが発表されています。コードを0.6で動作させるために必要なのは、2箇所にマーカー・アノテーションを付加することだけです。

  • RESTサービス・インタフェースに@Resource(org.teavm.flavour.rest.Resource)アノテーション
  • サービス・メソッドに渡す、またはサービス・メソッドから受け取るカスタム・クラスに@JsonPersistable (org.teavm.flavour.json.JsonPersistable)アノテーション(StringLongなどの標準クラスには不要)

 

カスタム・コンポーネント

ページやビューのクラスを設計する際には、再利用したいHTMLやコードが数多くあることに気づくでしょう。筆者がよく見るケースは2つあります。

  • ページの一部を2つのページで繰り返して使う必要がある
  • リストやテーブルなど、ページ内に繰り返される部分がある

コンポーネントを定義する最初の手順は、Flavourアプリで通常のページを定義する操作によく似ています。ビュー・クラスにバインドされたHTMLテンプレートを作成します。このテンプレート(慣例的に、src/main/componentsに配置します)には、コンポーネントのHTMLを格納します。ビュー・クラスにはテンプレートで使用するビジネス・ロジックとプロパティを含めます。この点は先ほどと同じですが、バインディングを追加します。バインディングはコンポーネントに対して一意で、属性やその他の値がどのようにテンプレートにバインドされるかを指定します。属性は、コンポーネントをカスタマイズするために使われます。

それでは、リスト内部や複数のページで再利用できるレストラン・コンポーネントの作り方を見てみます。

まず、components/restaurant.htmlでテンプレートを定義します。

<div style="background-color: #99e">
  <div>
    <h1><html:text value="restaurant.name"/></h1>
  </div>
  <div>
    <h3><html:text value="restaurant.yearsInBusiness"/>
     years in business!</h3>
  </div>
</div>

次に、src/main/java/com/app/component/Restaurant.javaでビュー・クラスを定義します。コンポーネントに表示するレストランをrestaurant属性にバインドしている点に注意してください。

@BindTemplate("components/restaurant.html")
@BindElement(name = "restaurant")
public class RestaurantComponent extends AbstractWidget {
    private Supplier<Restaurant> restaurantSupplier;
 

    public RestaurantComponent(Slot slot) {
      super(slot);
    }
 

    @BindAttribute(name = "restaurant")
    public void setNameSupplier(
                    Supplier<Restaurant> supplier) {
        this.restaurantSupplier = supplier;
    }
 

    public Restaurant getRestaurant() {
        return restaurantSupplier.get();
    }
}

さらに、コンポーネントを使う前に、ビュー・クラスの名前を次のファイルに追加して登録を行う必要があります。

META-INF/flavour/component-packages/com.mycompany

コンポーネントが別のパッケージにある場合は、com.mycompanyの部分をパッケージと一致するように変更します。

以上の手順が完了すれば、他のテンプレートでレストラン・コンポーネントを使えるようになります。先ほどのレストラン一覧ページを書き換えて、各レストランの一覧が表示されるページを作成してみます。std:foreachループの中にコンポーネントを配置し、各レストランに対応する、コンポーネントのインスタンスを1つずつ作成します(restaurant-list.html)。

<?use comp:com.mycompany?>
<div>
  <h1>Restaurants</h1>
  <ol>
    <std:foreach var="restaurant" in="restaurants">
      <li>
        <comp:restaurant restaurant="restaurant"/>
      </li>
    </std:foreach>
  </ol>
</div>

テンプレートの最初に、処理命令useがある点に注意してください。この命令では、コンポーネントを使用することをFlavourに対して伝えるとともに、後ほどファイル内で使用する接頭辞を指定しています。ここでは、接頭辞をcompとして宣言しています。後ほどコンポーネントを使う際に、comp:restaurant-detailという形で接頭辞が再登場しています。

 

位置情報APIの使用

位置情報APIを使ってユーザーの位置を照会することができます。サンプル・アプリのビュー・クラス(ここでは、geolocationClient.java)に次のコードを追加します。

public void locate() {
    if (Navigator.isGeolocationAvailable()) {
       Navigator.getGeolocation().getCurrentPosition(
           (Position pos) -> {
               final Coordinates coords = pos.getCoords();
               location = "Lat/Lon: " + coords.getLatitude()
                        + "/" + coords.getLongitude();
               Templates.update();
              }, 
           (PositionError positionError) -> {
              switch (positionError.getCode()) {
                  case PositionError.PERMISSION_DENIED:
                    location = "The user blocked location access.";
                    break;
                  case PositionError.POSITION_UNAVAILABLE:
                    location = "Location could not be determined.";
                    break;
                 case PositionError.TIMEOUT:
                    location = "A timeout occurred while attempting"
                            + " to determine your location."; break;
              }
              Templates.update();
           });
    } else {
          location = "This browser doesn't support geolocation";
          Templates.update();
    }
}
public String getLocation() {     return location; }

位置情報の検索を呼び出すボタンと、結果を表示する別のテキスト・フィールドを追加します。

<div>
  <div>
    <button event:click="locate()">Locate</button>
  </div>

  <div>
    Your location is: <html:text value="location"/>
  </div>
</div>

位置情報は非同期的に発生することから、Templates.update()を使って表示を更新しています。Templates.update()は軽量であるため、必要なときにはためらわずに呼び出すことができます。

 

まとめ

以上で、TeaVMで独自のJava Webアプリを構築する準備が整いました。レストラン一覧のWebサイトでも、新しいソーシャル・ネットワークでも、画期的なオンライン・ゲームでも作ることができます。

他の考え方や情報を探したい方、本記事の範囲を超える質問がある方は、TeaVMのWebサイトにアクセスしてみることをお勧めします。フォーラムが設置されているほか、詳しいドキュメントや例、問題リストなどが掲載されています。

Alexey Andreev氏は、TeaVMとFlavourの作者です。本記事の原稿を確認していただいたことに感謝いたします。

 

Java Magazine 日本版Vol.47の他の記事

Java 13のswitch式と再実装されたSocket APIの内側
Javaにテキスト・ブロックが登場
言語の内側:シールド型
ツールをよく知る
クイズに挑戦:1次元配列(中級者向け)
クイズに挑戦:カスタム例外(上級者向け)
クイズに挑戦:ロケールの読取りと設定(上級者向け)
クイズに挑戦:関数型インタフェース(上級者向け)


Andrew Oliver

20年以上にわたり、Javaのコーディング、講演、執筆に携わる。O’Reilly Conferenceでは、エンタープライズJavaについて講演を行った。AWT、Swing、JavaFX、Codename OneなどのさまざまなJavaツールキットでUIを構築した経験を持ち、現在はいくつかのTeaVM/Flavourベースのプロジェクトに従事している。