JavaFX から Payara Micro を呼び出す際の注意点

この記事は、Payara Advent Calendar 2016JavaFX Advent Calendar 2016Java EE Advent Calendar 2016 の 15 日目です。昨日は、

です。

Payara Micro は Java EE の埋め込みサーバーとして使用することが可能です。Java EE の埋め込みサーバーは、Java SE アプリケーションに対して Java EE API を提供するもので、別途 Java EE サーバーを構築しなくても Java SE アプリケーション上で Web アプリケーションをはじめとする Java EE アプリケーションを実行させることができるようになります。また、Java EE サーバーを準備しなくて良いことから、Java EE アプリケーションの単体テストを実行するための組み込みコンテナとしても活用することができます。

埋め込みサーバーとしての Payara Micro は、Embedded GlassFish Web Profile を使いやすくラップしたものであり、API としては Web Profile に加えて JBatch、Concurrency Utilities、JCache が追加されています (ただし、クラスタリングを無効にすると Hazelcast が使われなくなるため、それによって提供される JCache も同時に使用できなくなります)。

Payara Micro を埋め込みサーバーとして使用する場合、通常は Java SE のアプリケーションに組み込んで使うことになりますが、JavaFX のアプリケーションに組み込むことも可能です。ただし、いくつか制約事項がありますので、以下に示します。

1. スタートアップ

PayaraMicro のスタートアップ・コードは、javafx.application.Application#init メソッドに記述します。この場所でないと Payara Micro を初期化することができません。

private PayaraMicroRuntime runtime;

@Override
public void init() throws Exception {
    runtime = PayaraMicro.getInstance().setNoCluster(true).bootStrap();
}

2. アプリケーション実行時

このセクションは適当に流すつもりだったのですが、3 つの Advent Calendar に対する共通エントリとしたため気が引けて、当日未明になってから 5 時間がかりで全面的に書き直しました。何らかの参考になれば幸いです。

FXML と Controller クラスを使用しているのであれば、Java SE から Web アプリケーションに対してリクエストを送信し、そのレスポンスを受信して UI に反映させる処理となります。ここでは JAX-RS Client と WebSocket の使用例を重点的に示します。

2.1. JAX-RS Client を使用する場合

以下に JAX-RS のクライアント/サーバーを利用した例を示します。

Step 1 - JAX-RS のエンドポイントを定義する

まず、Web アプリケーション側に JAX-RS のエンドポイントを定義します。サンプルコードを以下に示します。

// ApplicationPath クラス
@ApplicationPath("api")
public class ApplicationConfig extends Application { }

// SearchResource クラス
@Path("search")
@RequestScoped
public class SearchResource {
  @GET
  @Path
  @Produces("application/xml")
  public List<List> search(@NotNull @QueryParam("q") String query) {
    List result = new ArrayList<>();
    
    // (ここで検索処理を行い、結果の文字列を result へ格納する)
    
    // JAX-RS は List<String> でも適切な XML にアンマーシャリングする
    return result;
  }
}

Step 2 - JavaFX から JAX-RS エンドポイントへのアクセスを実装する

JavaFX の Controller に対して JAX-RS エンドポイントにアクセスするコードを記述します。JAX-RS に関しては、これだけで JavaFX からデータを取得して ListView などへの表示を行えるようになります。


public class SearchController implements Initializable {
  
  //////////////// 共通処理 ////////////////
  
  /**
   * 検索条件を入力するテキストフィールドです。
   */
  @FXML
  private TextField queryField;
  
  /**
   * 検索結果の文字列を格納するリストです。
   */
  private List results = new ArrayList<>();
  
  //////////////// 検索処理 (非同期) の定義 ////////////////

  /**
   * 検索実行可能な場合に true となるプロパティです。
   */
  private BooleanProperty ready = new SimpleBooleanProperty(true);
  
  /**
   * JAX-RS エンドポイントを呼び出すタスクを作成します。
   * <p>
   * ボタン・クリックのイベントが発生したときに実行することを想定しているため、
   * {@link Service} による非同期処理として実装する必要があります。
   */
  Service<List> searchService = new Service<List>() {
    @Override
    protected Task<List> createTask() {
      return new Task<List>() {
        @Override
        protected List call() throws Exception {
          // 検索条件は queryField に入力される
          String query = queryField.getText();
          
          // 検索結果は listView に設定する
          listView.getItems().clear();
          
          // Web サービスを呼び出し、検索結果を取得する
          List results = client.target(CONTEXT_ROOT).path("api").path("search")
                                       .queryParam("q", query)
                                       .request()
                                       .accept("application/xml")
                                       .get(new GenericType<List>() { });
          // (追加の処理が必要な場合は、ここで実装する)
        
          // 検索結果を設定する
          // このクラス内では getValue() で参照可能になる
          return result;
        }
      };
    }
    
    /**
     * タスク実行時の処理を記述します。
     */
    @Override
    protected void running() {
      // 再検索を行う時は、前回の検索結果を消去する
      tweets.clear();
      
      // 検索中なので ready プロパティの状態を false に変更する
      // これを行わないと検索リクエストが無制限に発生して面倒なことになる
      ready.set(false);
    }
    
    /**
     * タスク成功時の処理を記述します。
     */
    @Override
    protected void succeeded() {
      // 検索結果を取得する
      results.addAll(getValue());
      
      // 検索結果を listView に設定する (重複排除とソートは実施する)
      listView.setItems(FXCollections.observableList(
          getValue().stream().distinct().sort().collect(toList());
      
      // (検索結果のダイアログ表示などは、この場所に記述すること)
    
      // ready プロパティの状態を true に変更して、検索可能な状態に戻す
      ready.set(true);
    }
    
    /**
     * タスクキャンセル時の処理を記述します。
     */
    @Override
    protected void cancelled() {
      // (検索キャンセルのダイアログ表示などは、この場所に記述すること)
      
      // ready プロパティの状態を true に変更して、検索可能な状態に戻す
      ready.set(true);
    }
    
    /**
     * 検索失敗時の処理を記述します。
     */
    @Override
    protected void failed() {
      // (検索失敗のダイアログ表示などは、この場所に記述すること)
      
      // ready プロパティの状態を true に変更して、検索可能な状態に戻す
      ready.set(true);
    }
  };
  
  //////////////// ボタン・イベント処理の定義 ////////////////
  
  /**
   * 検索ボタンをクリックした時の処理を記述します。
   * 
   * @param event アクション実行時のイベント
   */
  @FXML
  public void onSearchAction(ActionEvent event) {
    // searchService による非同期呼び出し
    // もし仮に同期呼び出しを使用すると、JavaFX の UI が硬直するので注意
    searchService.reset();
    searchService.start();
  }
  
  @Override
  public void initialize(URL location, ResourceBundle resources) {
    // 省略
  }  
}

2.2. WebSocket

以下に WebSocket のクライアント/サーバーを利用した例を示します。

Step 1 - WebSocket のエンドポイントを定義する

Web アプリケーション側で WebSocket のエンドポイントを Singleton として実装します。Singleton 以外でも実装可能のはずですが、私の環境では Singleton が最も安定して動作します。

@Singleton
@ServerEndpoint(value = "/api/twitter/publish")
public class TwitterPublisher {
  
  /**
   * WebSocket のセッションを管理するオブジェクトです。
   */
  private Set sessions = new CopyOnWriteArraySet<>();
  
  /**
   * 新たな WebSocket 接続があったときに対応するセッションを追加します。
   *
   * @param 追加されるセッション
   */
  @OnOpen
  public void onOpen(Session session) {
    sessions.add(session);
  }
  
  /**
   * 既存の WebSocket 接続が切断されたときに対応するセッションを削除します。
   *
   * @param 削除されるセッション
   */
  @OnClose
  public void onClose(Session session) {
    sessions.remove(session);
  }
  
  /**
   * エラー発生時の処理を定義します。
   *
   * throws t エラー内容を表す例外オブジェクト
   */
  @OnError
  public void onError(Throwable t) {
    // ログ出力などを行う (省略)
  }
  
  /**
   * 接続済みのすべてのセッションに対してメッセージを配信します。
   * <p>
   * このメソッドは、メッセージ配信のタイミングで外部から呼ばれます。
   * このサンプルでは、外部の EJB タイマーから定期的に呼び出します。
   */
  public void send(String message) {
    sessions.forEach(session -> {
      try {
        session.getBasicRemote().sendText(message);
      } catch (IOException) {
        e.printStackTrace();
      }
    });
  }
  
  @Override
  public void initialize(URL location, ResourceBundle resources) {
    // 省略
  }  
}

Step 2 - Web アプリケーション側に定期配信の仕組み (EJB Timer) を実装する。

WebSocket は定期的に配信するところに強みがあります。エンドポイント自身には定期配信を行うための仕組みがないため、エンドポイントを呼び出して定期配信するためのコードを実装する必要があります。ここでは Web アプリケーション側に EJB Timer を追加して、10 秒おきにエンドポイントからデータを取得するようなコードを用意しました。

@Singleton
public class Scheduler {
  
  /**
   * ポーリング実行時の絞り込み条件を表します。
   */
  private String query;
  
  /**
   * ポーリングの開始・停止を制御します。
   *
   * @pamam flag {@code true} の場合はポーリング実行、{@code false} の場合は停止
   */
  public setPolling(boolean flag) { /* 省略 */ }
  
  /**
   * ポーリングの開始・停止の状態を取得します。
   *
   * @return {@code true} の場合はポーリング実行中、{@code false} の場合は停止済
   */
  public isPolling() { /* 省略 */ }
  
  /**
  * ポーリングを開始します。
  *
  * @param query ポーリング対象の絞り込み条件
  */
  public void startPolling(String query) {
    // ポーリング停止中のみ処理対象 (既に実行中の場合は何もしない)
    if (!isPolling()) {
      this.query = query;
      setPolling(true);
    }
  }
  
  /**
   * ポーリングを停止します。
   */
  public void stopPolling() {
    // ポーリング実行中のみ処理対象 (既に停止中の場合は何もしない)
    if (isPolling()) {
      setPolling(false);
      query = null;
    }
  }
  
  /**
   * Publisher クラス (WebSocket エンドポイント) のインスタンスです。
   */
  @Inject
  private Publisher publisher;
  
  /**
   * 定期的に {@link Publisher#send(String)} を呼び出してデータを配信します。
   * このメソッドは EJB Timer として呼び出されることが補償されているため、
   * {@code private} スコープとしています。
   * <
   * なお、このメソッドは 10 秒おきに EJB Timer として呼び出されます。
   *
   * @see javax.ejb.Scledule
   */
  @Schedule(hour = "*", minute = "*", second = "*/10", persistent = false)
  private void polling() {
    if (isPolling()) {
      // ポーリング実行中: データ配信
      // ここでは getNewResult メソッドで新着メッセージを取得すると仮定
      List result = getNewResult(query);
      result.forEach(s -> publisher.send(s));
    }
  }
}

Step 3 - JavaFX から WebSocket エンドポイントへのアクセスを実装する

JavaFX の Controller に対して WebSocket エンドポイントにアクセスするコードを記述します。WebSocket に関しては、これだけで JavaFX からデータを取得して ListView などへの表示を行えるようになります。

public static class PollingController implements Initializable {

//////////////// 共通処理 ////////////////

/**
 * ポーリング対象条件を入力するテキストフィールドです。
 */
@FXML
private TextField queryField;

/**
 * ポーリングの開始・終了を制御するトグルボタンです。
 */
@FXML
private ToggleButton pollingButton;

/**
 * ポーリング結果の文字列を格納するリストです。
 */
@FXML
private ListView<String> listView;

//////////////// 検索処理 (非同期) の定義 ////////////////

/**
 * ポーリング実行可能な場合に true となるプロパティです。
 */
private BooleanProperty ready = new SimpleBooleanProperty(true);

/**
 * WebSocket エンドポイントを呼び出すタスクを作成します。
 * <p>
 * ボタン・クリックのイベントが発生したときに実行することを想定しているため、
 * {@link Service} による非同期処理として実装する必要があります。
 */
Service<Void> pollingService = new Service<Void>() {
  @Override
  protected Task<Void> createTask() {
    return new Task<Void>() {
      /**
       * This latch works the thread keep awaiting because of accepting cancel request.
       */
      private CountDownLatch messageLatch = new CountDownLatch(1);
      
      /**
       * WebSocket セッションへの参照を表します。
       */
      private Session session;
      
      @Override
      protected Void call() throws Exception {
        // WebSocket のエンドポイント
        final wsEndpoint = URI.create("/api/polling/publish");
        
        // このあたりは WebSocket クライアント API を使うときのほぼ定型コード
        WebSocketContainer container = ContainerProvider.getWebSocketContainer();
        session = container.connectToServer(new Subscriber(listView), wsEndpoint);
        
        messageLatch.await();
        
        return null;
      }
      
      /**
       * ポーリング実行時の処理を記述します。
       */
      @Override
      protected void cancelled() {
        if (session != null) {
          try {
            session.close();
          } catch (IOException e) {
            // ログ出力などを行う (省略)
          }
        }
        
	// pollingButton をクリック可能にする
        pollingButton.setSelected(false);
        
        // ポーリング実行を受け付ける
        ready.set(true);
      }

      /**
       * ポーリングキャンセル時の処理を記述します。
       */
      @Override
      protected void failed() {
        
	// pollingButton をクリック可能にする
        pollingButton.setSelected(false);
        
        // ポーリング実行を受け付ける
        ready.set(true);
      }
    };
  }

  /**
   * タスク失敗時の処理を記述します。
   */
  @Override
  protected void cancelled() {
    // pollingButton をクリック可能にする
    pollingButton.setSelected(false);
    
    // ポーリング実行を受け付ける
    ready.set(true);
  }

  /**
   * タスク失敗時の処理を記述します。
   */
  @Override
  protected void failed() {
    // pollingButton をクリック可能にする
    pollingButton.setSelected(false);
    
    // ポーリング実行を受け付ける
    ready.set(true);
  }
};

/**
 * ポーリング開始・停止ボタンをクリックした時の処理を記述します。
 * 
 * @param event アクション実行時のイベント
 */
@FXML
public void onPollingAction(ActionEvent event) {
  if (pollingButton.isSelected()) {
    pollingButton.setSelected(false);
    
    // 開始要求の GET リクエストを送信するだけ: レスポンスは無視
    client.target(CONTEXT_PATH).path("api").path("polling").path("start")
          .queryParam("q", queryField.getText()).request().get();

    listView.getItems().clear();
    
    // pollingService による非同期呼び出し
    // もし仮に同期呼び出しを使用すると、JavaFX の UI が硬直するので注意
    pollingService.reset();
    pollingService.start();
  } else {
    pollingService.cancel();
    
    // 停止要求の GET リクエストを送信するだけ: レスポンスは無視する
    client.target(CONTEXT_PATH).path("api").path("polling").path("stop")
          .request().get();
  }
}

2.3. その他の手段

JavaServer Faces、Servlet、JSP などに対しては、画面遷移を伴うことから、通常は JavaFX の WebView を経由したアクセスになります。ただし、ユースケースによっては HTTP Client で直接アクセスすることもあり得ます。

JAX-WS については、Java SE に JAX-WS Client が付属しているため、これを使用することができます。

RMI-IIOP などのレガシーな分散処理プロトコルについても、検証こそしていませんが同様の要領で使用できるのではないかと考えています。もっとも、今どき RMI-IIOP なんて (一部の例外を除き) 使用することはないと思いますが。

その他にも Java EE 7 には様々な API が用意されています。JavaFX のアプリケーションに Payara Micro を埋め込むことで、それらの API も気軽に使用することができます。

3. シャットダウン

Payara Micro を JavaFX に組み込むと、通常の Ctrl+C によるシャットダウンができません。これは JavaFX 側でシャットダウン・フックを完全に押さえており、Payara Micro のシャットダウン・コードをシャットダウン・フックに追加する余地がないためだと思われます。この制限のため、Payara Micro のシャットダウンは javafx.application.Application#stop メソッドから PayaraMicro#shutdown または PayaraMicroRuntime#shutdown メソッドを呼び出すことで実現する必要があります。

@Override
public void stop() throws Exception {
    if (runtime != null) {
        runtime.shutdown();
    }
}

なお、Payara Micro を明示的にシャットダウンしなかった場合には、それを動作させていた Java VM (プロセス) が残ったままとなりますので、注意が必要です。

4. まとめ

いくつかの制約事項はありますが、JavaFX と Payara Micro を組み合わせて使用することは可能です。javapackager を使用すれば、Java EE ランタイムを内包した JavaFX アプリケーションをネイティブ・パッケージとして配布することも可能になります。フロントエンドを JavaFX、バックエンドを Payara Micro で構成した、比較的小さな Java EE アプリケーションを開発および配布する場合には、ネイティブ・パッケージも作成できる上記の方法も選択肢のひとつとしてあげられることでしょう。

今回のサンプルコードは一応 https://github.com/khasunuma/payaramicro-on-javafx にあります。急ぎ追加した JAX-RS と WebSocket に関するコードが含まれていないため、あくまでテンプレートとしての扱いでお願いします。

明日は、Payara が「Payara が持つ 2 つのクラスタリング」、JavaFX がさくらばさん (@skrb)、Java EE がタイガーたいぞー (@tigertaizo)さんです。