[Java, JavaFX] JavaFX Integration Strategies

原文はこちら。
http://www.oracle.com/technetwork/articles/java/javafxinteg-2062777.html

With lambda式と非同期通信のサポートにより、JavaFXはバックエンドサービスの新たな連携の可能性をもたらします。

分断されたアプリケーションを企業内で見つけることはそうそうないでしょう。エンタープライズデスクトップアプリケーションは、アプリケーションサーバが公開する一つ以上のバックエンドサービスのデータをレンダリングし、操作します。古いSwingとJ2EEの時台は、通信は、一方向かつ同期型でした。 JavaFXとJava EE 6およびJava EE 7では、新たな同期、非同期、プッシュ、プル型といった様々なの連係戦略が導入されています。この記事では、JavaFXアプリケーションを使用してJava EEサービスを統合することに焦点を当てます。

JavaFX Is Java

JavaFXはJavaです。そのため、Swingアプリケーションのために使っているベストプラクティスはJavaFXに適用できます。バックエンドサービスとの統合はプロトコルにも技術にも依存しません。
サービスには様々な設定(IPアドレス、ポート番号、プロパティファイル)が含まれています。APIのメソッドは、 java.rmi.RemoteException のようなプロトコル固有の例外をスローするため、無関係な詳細情報でプレゼンテーションロジックを汚すことがあります。プロプライエタリなサービスを囲む薄いラッパーは実装の詳細を隠蔽し、より意味のあるインターフェースを公開します。これは古典的なGoF(Gang of Four)のAdapterパターンです。
Adapterパターン
http://en.wikipedia.org/wiki/Adapter_pattern
http://ja.wikipedia.org/wiki/Adapter_%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3

Revival of the Business Delegate

J2EEクライアントはバックエンドとの通信において、その昔はRMI-IIOP、その後はJAX-RPCやJAX-WSに大きく依存していました。どちらのAPIとも、厳密にチェックされた例外を使い、特定のテクノロジーに結びついています。プレゼンテーションロジックをプロトコルから分離する上で、Business Delegateパターンが必要でした。
Core J2EE Patterns - Business Delegate
http://www.oracle.com/technetwork/java/businessdelegate-137562.html
"Use a Business Delegate to reduce coupling between presentation-tier clients and business services. The Business Delegate hides the underlying implementation details of the business service, such as lookup and access details of the EJB architecture."
(Business Delegateを使って、プレゼンテーション層クライアントとビジネスサービス間の結合を減らします。Business Delegateは、EJBアーキテクチャの詳細の検索やアクセスのような、基盤となるビジネスサービスの実装の詳細を隠蔽します)
Business Delegateはデフォルトケースでは実際のプロキシ、テスト環境ではMockオブジェクトを作成するファクトリを用いてよく拡張されました。最近のMockライブラリ、例えばMockitoを使うと、Business Delegateを直接Mockにすることができます。
mockito - simpler & better mocking
http://code.google.com/p/mockito/
JavaFXおよびJava EEコンテキストにおけるBusiness Delegateを、実装の詳細を隠蔽し、JavaFXに対し使い勝手のよいインターフェースを公開するアダプタPOJOとして実装することができます。
Figure 1
Figure 1

First Request, Then Response

ブロッキングリクエストをアプリケーションサーバーに送信し、データの到着を待つというのは、最もシンプルなバックエンドとの統合方法です。Business Delegateはバックエンドと通信するサービスになります(コード1)。
コード1
import javax.json.JsonObject;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

public class MethodMonitoring {
 
    private Client client;

    public void init() {
        this.client = ClientBuilder.newClient();
    }
    public MethodsStatistics getMethodStatistics(String application, String ejbName) {
        final String uri = getUri();
        WebTarget target = this.client.target(uri);
        Response response = target.
                resolveTemplate("application", application).
                resolveTemplate("ejb", ejbName).
                request(MediaType.APPLICATION_JSON).get(Response.class);
        if (response.getStatus() == 204) {
            return null;
        }
        return new MethodsStatistics(response.readEntity(JsonObject.class));
    }
 }
MethodMonitoring クラスは実装、テストが簡単で、プレゼンテーション層と統合が可能です。 getMethodStatistics メソッドは潜在的に無制限の時間ブロックすることが可能であるため、UIリスナー・メソッドからの同期呼び出しを使ってUIが応答しないようになります。

Asynchronous Integration

幸いにして、JAX-RS 2.0 APIでは非同期・コールバックベースの通信モデルもサポートしています。ブロッキングの代わりに、 getMethodStatistics メソッドはリクエストを開始し、コールバックを登録します(コード2)
コード2
import javax.json.JsonObject;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.InvocationCallback;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MediaType;
import java.util.function.Consumer;

public class MethodMonitoring {
    private Client client;

    @PostConstruct
    public void init() {
        this.client = ClientBuilder.newClient();
    }

    public void getMethodStatistics(Consumer<MethodsStatistics> consumer, Consumer<Throwable> error, 
    String application, String ejbName) {
        final String uri = getUri();
        WebTarget target = this.client.target(uri);
        target.
                resolveTemplate("application", application).
                resolveTemplate("ejb", ejbName).
                request(MediaType.APPLICATION_JSON).async().get(new InvocationCallback<JsonObject>() {
            @Override
            public void completed(JsonObject jsonObject) {
                consumer.accept(new MethodsStatistics(jsonObject));
            }

            @Override
            public void failed(Throwable throwable) {
                error.accept(throwable);
            }
        });
    }
}
コールバックは返ってくる JsonObject をドメインオブジェクトに変換し、それを java.util.function.Consumer の実装に渡します。Business Delegateの実装はまだJavaFX APIから独立しており、Java 8の java.util.function.Consumer をコールバックとして利用します。Java 7では、任意のカスタムインターフェースやクラスをコールバックとして使うこことができますが、Java 8ではJavaFXのpresenterを劇的に簡素化することができます(コード3)。
コード3
...
      this.methodMonitoring.getMethodStatistics(s -> onArrival(s), 
        t -> System.err.println(t), this.monitoredApplication, ejb);
...
    void onArrival(MethodsStatistics statistics) {
        Platform.runLater(() -> {
            this.methodStatistics.clear();
            this.methodStatistics.addAll(statistics.all());
        }
        );
    }
 java.util.function.Consumer をlambda式として実装することができます。これにより、コード量を大きく削減できます。JavaFXはシングルスレッドのUIツールキットゆえ、非同期でマルチスレッドからアクセスすることはできません。
 java.lang.Runnable インターフェースのlambda実装を Platform.runLater メソッドに渡し、後の実行のためにイベントキューに追加しています。Javadocには以下のような記述があります。
"Run the specified Runnable on the JavaFX Application Thread at some unspecified time in the future. This method, which may be called from any thread, will post the Runnable to an event queue and then return immediately to the caller. The Runnables are executed in the order they are posted. A runnable passed into the runLater method will be executed before any Runnable passed into a subsequent call to runLater."
(将来のある時間に、JavaFXアプリケーションのスレッドで指定されたRunnableを実行します。任意のスレッドから呼び出される可能性があるこのメソッドは、イベントキューにRunnableをエンキューし、呼び出し元にすぐに戻ります。RunnableはQueueに入った順序で実行されます。runLaterメソッドに渡されたRunnableは、runLaterへの後続の呼び出しに渡されたRunnableの前に実行されます)
Platform#runLater メソッドは長時間実行するタスクには適切ではありません。非同期スレッドからのJavaFX UIコンポーネントのアップデートのためだけに使うべきです。

Tasks for Real Work

Platform.runLater は「重労働」の実装を目的としているのではなく、JavaFXノードの高速なアップデートを目的としています。長時間実行されるメソッドの非同期呼び出しは、スレッドの作成が必要で、これをJavaFXの javafx.concurrent.Task クラスでネイティブにサポートしています。 TaskWorker および EventTarget インターフェースを実体化し、 java.util.concurrent.FutureTask クラスから継承するため、JavaのスレッドとJavaFXのイベント機構の間のブリッジと見なすことができます。 Task を直接 普通の Runnable として Thread が利用することも、 Callable として ExecutorService に渡すこともできます。どちらの場合も、非同期実行の機能を持たない同期Java EE API、例えばIIOPをBusiness Delegteで最初にラップできます。
コード4
public class SnapshotFetcher{ 
...
    public Snapshot getSnapshot() {
        return fetchFromServerSynchronously();
    }
...
次に、ブロッキングBusiness Delegateメソッドを Task でラップすると、ようやく非同期実行が可能になります。
コード5
Task<Snapshot> task = new Task<Snapshot>() {
            @Override
            protected Snapshot call() throws Exception {
                SnapshotFetcher fetcher = new SnapshotFetcher();
                return fetcher.getSnapshot();
        };
TaskRunnable であり Future なので、直接 Thread が実行することも、 Executor に渡すこともできます。JavaFXはシームレスにバインド可能なプロパティを使用してUIとスレッドを統合する javafx.concurrent.Service クラスが付属しています。
Service は、実際には Task ファクトリです。
コード6
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import org.lightview.model.Snapshot;

public class SnapshotProvider extends Service<Snapshot> {
  @Override
    protected Task<Snapshot> createTask() {
        return new Task<Snapshot>() {
            @Override
            protected Snapshot call() throws Exception {
                SnapshotFetcher fetcher = new SnapshotFetcher();
                return fetcher.getSnapshot();
            };
        };
    }
}
Service の状態だけでなく Task の実行結果も、バインド可能なJavaFXプロパティとして利用可能です。
コード7
this.service = new SnapshotProvider();
        service.start();
        service.valueProperty().addListener(
                (observable,old,newValue) ->{
                //process new value
                    });
Task クラスは、同期型レガシーリソースの非同期統合、または単にクライアント側で長時間実行プロセスを起動するための便利な手段です。

Blocking for Asynchronicity

Cometとロング・ポーリングはブラウザを用いてHTTPベースのプッシュ通信のシミュレーションのための美しくないHackです。
Comet
http://en.wikipedia.org/wiki/Comet_%28programming%29
HTTPはリクエストーレスポンスプロトコルゆえ、レスポンスはリクエストに対する回答として送信することができます。そのため、最初のリクエストがないのにHTTPを介してブラウザにデータをプッシュすることはできません。ロングポーリングの通信スタイルは実装が簡単です。ブラウザは、サーバーがブロックしている接続を開始します。サーバはブロッキング通信を使ってデータをブラウザに送信し、直ちに接続を閉じます。ブラウザはデータを処理し、サーバを使って後続のブロッキング接続を再開します。何も伝えることがない場合、サーバはブラウザに対しリクエスト204を送信します。
それゆえ、HTTP通信に限定されずに、JavaFXアプリケーションはスタンドアロンJavaアプリケーションとして企業内に展開されますが、多くの場合、RESTエンドポイントは、HTML5のクライアント用に利用可能であり、JavaFXアプリケーションが直接再利用することも可能です。RESTおよびJSONは、HTML5クライアントやJavaアプリケーション、そして低レベルデバイスとの通信のための新しい最小公分母になっています。
JavaFXアプリケーションは直接ロングポーリングに参加することができ、HTML5クライアントと同じ方法で通知することができます。同期通信とロング・ポーリングの唯一の違いは、ブロッキング呼び出しを繰り返し開始する点です。定期的なポーリングを直接 javafx.concurrent.Service で実装できます。実行の成功・失敗にかかわらず、サービスをリセットし、その後再起動します。
コード8
javafx.concurrent.Service service = ...;
    void registerRestarting() {
        service.stateProperty().addListener((observable,oldState,newState) -> {
                if (newState.equals(Worker.State.SUCCEEDED) ||
                        newState.equals(Worker.State.FAILED)) {
                    service.reset();
                    service.start();
            }
        });
    }

Push Integration

プッシュ通信は、リクエスト部分のないリクエスト・レスポンス型の通信スタイルです。アプリケーション·サーバーは、いつでもデータをプッシュすることができます。Java Message Service(JMS)やWebSocket、メモリ·グリッドは、fire-and-forgetスタイルの通知メカニズムを備えており、簡単にJavaFXと統合することができます。
JSR 356はJava EE 7に含まれるWebSocketプロトコルを実装しており、JavaクライアントAPIを備えています。WebSocket仕様では双方向バイナリプロトコルを導入し、UIクライアントとの統合にぴったり適しています。
JSR 356: JavaTM API for WebSocket
http://www.jcp.org/en/jsr/detail?id=356
WebSocketメッセージはバイナリもしくはテキストをとることができ、コード9のようにEndpoint サブクラスを使ってメッセージを受信します。
コード9
import javafx.application.Platform;
import javafx.beans.property.SimpleObjectProperty;
import javax.websocket.Endpoint;
import javax.websocket.EndpointConfig;
import javax.websocket.MessageHandler;
import javax.websocket.Session;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import java.io.StringReader;

public class SnapshotEndpoint extends Endpoint {

    private SimpleObjectProperty<Snapshot> snapshot;
    private final Unmarshaller unmarshaller;
    private JAXBContext jaxb;

    public SnapshotEndpoint() {
        try {
            this.jaxb = JAXBContext.newInstance(Snapshot.class);
            this.unmarshaller = jaxb.createUnmarshaller();

        } catch (JAXBException e) {}
        this.snapshot = new SimpleObjectProperty<>();
    }

    @Override
    public void onOpen(Session session, EndpointConfig ec) {
        session.addMessageHandler(new MessageHandler.Whole<String>() {
            @Override
            public void onMessage(String message) {
                final Snapshot current = deserialize(message);
                Platform.runLater(() ->
                        snapshot.set(current));
            }
        });
    }
    Snapshot deserialize(String message) {
        try {
            return (Snapshot) unmarshaller.unmarshal(new StringReader(message));
        } catch (JAXBException e) {}
    }
}
SnapshotEndpoint クラスは文字列のメッセージを受け取り、Java Architecture for XML Binding (JAXB) APIを使って変換します。
JSR 222: JavaTM Architecture for XML Binding (JAXB) 2.0
http://jcp.org/en/jsr/detail?id=222
Snapshot ドメインオブジェクトはアノテーションの付いたPOJOです。
コード10
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Snapshot {
 //...arbitrary fields
    private long id;
}
JSR 356 APIは拡張機能をサポートしているため、シリアル化と逆シリアル化を専用のクラスに分解することができます。また、我々は、JAXBに限定していないので、JSONやシリアライズといった任意の利用可能なオブジェクト表現を利用できます。 SnapshotEndpoint はWebSocketの専用スレッドによってクライアント側で実行されるので、メッセージを使ってUIだけを更新することはできません。Platform.runLater メソッドを使い、メッセージを適切にWebSocketからJavaFXのスレッドに渡します。
Endpoint は、実際の通信のみを担当しています。さらに、WebSocketのコンテナは、専用クラスに実装されているものに対する初期設定と初期化が必要です。
コード11
public class SnapshotSocketListener {
    private SnapshotEndpoint endpoint;
    private Session session;

    public SnapshotSocketListener() {
        this.endpoint = new SnapshotEndpoint();
        WebSocketContainer container = ContainerProvider.getWebSocketContainer();
        ClientEndpointConfig config = ClientEndpointConfig.Builder.create().build();
        String uri = "ws://localhost:8080/lightfish/snapshots/";
        try {
            session = container.
            connectToServer(this.endpoint, config, URI.create(uri));
        } catch (DeploymentException | IOException e) {
            throw new IllegalStateException("Cannot connect to WebSocket: ", e);
        }
    }

    public ReadOnlyObjectProperty<Snapshot> snapshotProperty() {
        return endpoint.snapshotProperty();
    }

    @PreDestroy
    public void disconnect() {
        try {
            session.close();
        } catch (IOException e) {
        }
    }
}
これまでのところ、我々はほとんどJavaFXの統合機能を使用していません。その代わりに、様々な統合のスタイルに焦点を当ててきました。しかし、JavaFXのプロパティを使うと、同期および非同期の統合が、特に興味深いものになります。

Integration the JavaFX Way

javafx.beans.property.ObjectPropertyObject インスタンスをラップしており、バインド可能です。関心のあるクライアントがリスナーとして登録したり、直接プロパティにバインドしたりすると、ラップされたインスタンスが置き換わった場合に通知を受け取ることができます(Java Magazineの記事"JavaFX Data Binding for Enterprise Applications"の"Binding for Narrow Interfaces"の章を参照してください)。
JavaFX Data Binding for Enterprise Applications
http://www.oraclejavamagazine-digital.com/javamagazine/20130910?pg=53&pm=1&u1=texterity&linkImageSrc=/javamagazine/20130910/data/imgpages/tn/0053_rzzace.gif/%20-%20pg53#pg53
通信プロトコルや同期・非同期に関係なく、レスポンスは、UIが表示するドメインオブジェクトを伝達します。ObjectPropertyを使って、UIは直接値とバインドすることができ、データ到着時には自動的に通知を受け取ることができます。プレゼンターは直接ObjectPropertyにバインドするので、追加の管理手法は不要です。
コード12
this.snapshotSocketListener.snapshotProperty().
        addListener((o, oldValue, newValue) -> {
            snapshots.add(newValue);
            onSnapshotArrival(newValue);
        });
バインド可能な ObjectProperty を使うと、大幅にシンプルになり、インターフェイスが顕著に「狭く」なります。バインディングをアウトバウンド通信に適用することもできます。プレゼンターは、ドメインオブジェクトもしくはモデルの状態を変更すると、サービス(Business Delegate)の通知を引き起こします。アウトバウンド通信は、UIスレッドとの同期は不要です。非同期操作も直接UIスレッドから実行することができ、長時間実行する操作を javafx.concurrent.Service でラップすることも、Business Delegate内で非同期実行することもできます。しかし、すべてのUI操作がドメインオブジェクトの状態を変更するわけではありません。「保存」や「リフレッシュ」といった簡単なユーザーアクションをBusiness delegateメソッド呼び出しに直接変換することができます。

One Step Further for Responsiveness and Simplicity

JavaFXのUIはイベント駆動で非同期です。また、JAX-RS 2.0や、JMS、WebSocketなどのJava EE 7 APIは非同期機能を有しています。JavaFXを非同期のJava EE 7 APIと一緒に使うと、大幅にコードがシンプルになります。すべてのBusiness Delegateの操作は、UIあるいはBusiness Delegate自体をブロックせずに非同期に実行することができます。インタラクションパターンは、通信プロトコル非依存であり、すべての非同期Java EE 7 APIで一貫して適用できます。

リクエストは、 "fire-and-forget"方式でサーバーに送信されます。応答を待つのではなく、コールバックメソッドを応答処理のために登録します。コールバックは、データを受信し、ドメインオブジェクトを移入し、 Platform.runLater メソッド内の ObjectProperty#set を使って現在のドメインオブジェクトを置き換えます。ドメインオブジェクトへの変更は、プレゼンターに伝達されます。このとき、任意の関心のあるビューに対する変更をマルチキャストで通知します。
完全非同期通信にすると、大幅に必要なコードの量を減らすことができます。データバインディングを伴う双方向fire-and-forgetアプローチを使用すると、サーバ側での一時的なモデルとマスタ状態との間でデータの同期が不要になります。すべてのアクションは、直接Business Delegateに渡され、アプリケーションサーバからのすべてのレスポンスによって直接UIが更新されます。
また、完全非同期のインタラクションにすると、ユーザーエクスペリエンス(UX)を大幅に改善することができます。サーバー側のインタラクションがどれほど高コストであったとしても、UIがブロックされることはありません。

結論

一見すると、バックエンドサービスとのJavaFXの統合はSwingに非常に似ています。 Platform#runLaterjavax.swing.SwingUtilities#invokeLater と同等であり、 javafx.concurrent.Service の目的と javax.swing.SwingWorker は類似しています。

近代的なJava EE7 APIやJavaFXデータ・バインディング("JavaFX Data Binding for Enterprise Applications"をご覧下さい)、FXMLとScene Builderを使った制御機能の反転("Integrating JavaFX Scene Builder into Enterprise Applications"をご覧下さい)を使うと、大幅にプレゼンテーションロジックを簡素化し、マルチビューデスクトップアプリケーションを実装するための一貫したアプローチを導入することができます。
JavaFX Data Binding for Enterprise Applications
http://www.oraclejavamagazine-digital.com/javamagazine/20130910?pg=53&pm=1&u1=texterity&linkImageSrc=/javamagazine/20130910/data/imgpages/tn/0053_rzzace.gif/#pg53
Integrating JavaFX Scene Builder into Enterprise Applications
http://www.oraclejavamagazine-digital.com/javamagazine/20130506/?pg=75&pm=1&u1=friend%20-%20pg75#pg75 
Java EE 6およびJava EE 7ベースのバックエンドシステムであれば、プレゼンテーション層だけでなく、サーバーサイドでも継続して非同期通信のスタイルを利用することができます。

参考資料

著者について

コンサルタントである著者のAdam Bienは、Java EE 6、Java EE 7、EJB 3.x、JAX-RS、JPA 2.x JSRのExpert Groupメンバーです。
Adam Bien's Blog
http://www.adam-bien.com/roller/abien/ 
JDK 1.0の頃からJavaテクノロジーに関わり、Servlet/EJB 1.0を経て、現在はJava SEおよびJava EEプロジェクトのアーキテクトであり、開発者です。Java FX、J2EE、Java EEに関する書籍を執筆しており、Real World Java EE Patterns - Rethinking Best PracticesとReal World Java EE Night Hacks - Dissecting the Business Tierの著者でもあります。
Real World Java EE Patterns—Rethinking Best Practices
http://realworldpatterns.com/
Real World Java EE Night Hacks—Dissecting the Business Tier
http://press.adam-bien.com/real-world-java-ee-night-hacks-dissecting-the-business-tier.htm
AdamはJava Championにして、Top Java Ambassador 2012、JavaOne 2009、2011、2012 Rock Starでもあります。Adamはミュンヘン空港でのJava EEワークショップを時々開催しています。
Java EE Workshops with Adam Bien at MUC Airport
http://airhacks.com

0 件のコメント:

コメントを投稿