Archive for 2012年12月14日
Java EE 7 WebSocket Client Sample Application with JavaFX
この記事はJavaEE Advent Calendar 2012の14日目の記事です。
昨日は @noriand さんによる「Spring on Glassfish」でした。
明日は@kokuzawaさんです。
2013 年 5 月 13 日追記:本ソースコードは、WebSocket の仕様が完全に FIX する前に記載したコードのため、既に記載している内容のコードでは動かなくなっています。新しい API の詳細は、javax.websocketパッケージ、javax.websocket.serverをご参照ください。 |
さて、今年は何を書こうかととてもなやんでいた所、昨日の深夜12時過ぎに、
@skrb さんから、「@yoshioterada 寺田さん、JavaFX Advent Calednarに登録していないようですけど…. GlassFish と JavaFX を絡めて、両方のAdvent Calendar に登録するのでもいいですよww」
とおっしゃって頂き(敷居があがり (^_^;) )、以前のセミナーで説明した Java の WebSocket のクライアント側の実装をご紹介しようという事で書いてみます。
※ また、本プログラムは、Java EE 7 に含まれる予定の WebSocket API を利用している事から、現時点での実装方法のサンプルとなります。Java SEE 7 の正式リリース時には API 等が変更される可能性も多いにありますので、どうぞその点はご注意ください。
サーバ側の実装コードは、先日、WebSocket Twitter タイムライン・取得サンプルのエントリで記載しました。このエントリを記載した後にも既に GlassFish v4 の b66 では API が若干変わっていますので、あくまでの参考としてご参照ください。
今回は、このサーバ側の実装をそのまま利用して、クライアント側のプログラムを JavaFX (Java のアプリケーション)として動作させるというのが今回のブログの趣旨となります。
作成する JavaFX の WebSocket アプリの動作は下記のようなイメージです。この JavaFX アプリケーションは、ボタンを押下すると WebSocket のサーバに接続しサーバから送信されるメッセージを受信し TextArea 内に表示するという内容です。
準備:
本プログラムを動作させるためには、下記のライブラリが必要になります。
* grizzly-framework-2.2.19.jar
* grizzly-http-2.2.19.jar
* grizzly-http-server-2.2.19.jar
* grizzly-websocket-2.2.19.jar
* tyrus-servlet-1.0-b08.jar
* javax.net.websocket-1.0-b08.jar
Grizzly 関連の jar ファイルはコチラから御入手ください。
※ Grizzly とは GlassFish のサーバ・エンジンを司る汎用サーバ・フレームワークで Grizzly のライブラリを利用する事で自身で低レベルのソケットプログラムを記載する事なく、ネットワーク通信のプログラムを記載する事が可能になります。今回クライアント側からサーバ側に対してネットワーク接続を行うために、この Grizzly のライブラリを内部的に利用します。
また、tyrus と javax.net.websocket の jar につきましては、https://maven.java.net/ からここで説明した内容と同様の方法でそれぞれのライブラリを検索して頂き、御入手ください。
それでは、NetBeans を使用して JavaFX のアプリケーションを作成しましょう。
まず、NetBeans から新規プロジェクトを作成します。
ここでは、プロジェクト名として「JavaFX-WebSocket」という名前で作成します。
次に、準備の所で入手した全ライブラリを本プロジェクトのライブラリ内に配置し利用できるようにします。
ここでは、”JavaFXWebSocket.java”、”Sample.fxml”、”SampleController.java”の3つのファイルが作成されていますので、これらを編集してプログラムを作っていきます。
まず、JavaFX SceneBuilder を使用してデザインを作成します。今回はとても簡単にするために、画面サイズを調整し、Label, TableView を貼付けます。また、TableView に対して ID として “table” を設定します。また、TableView 内の TableColumn に対して column
の ID を設定します。
できあがった、Sample.fxml は下記の通りです。
<?xml version="1.0" encoding="UTF-8"?> <?import java.lang.*?> <?import java.util.*?> <?import javafx.scene.*?> <?import javafx.scene.control.*?> <?import javafx.scene.layout.*?> <AnchorPane id="AnchorPane" prefHeight="400.0" prefWidth="453.0" xmlns:fx="http://javafx.com/fxml" fx:controller="javafx.websocket.SampleController"> <children> <Button fx:id="button" layoutX="326.0" layoutY="365.0" onAction="#handleButtonAction" text="Start TimeLine" /> <Label fx:id="label" layoutX="126.0" layoutY="120.0" minHeight="16.0" minWidth="69.0" /> <Label layoutX="14.0" layoutY="14.0" prefWidth="292.0" text="WebScoket Twitter TimeLine Client Smaple" underline="true" /> <TableView fx:id="table" layoutX="14.0" layoutY="45.0" prefHeight="311.0" prefWidth="425.0"> <columns> <TableColumn id="" maxWidth="445.0" prefWidth="445.0" text="メッセージ一覧" fx:id="column" /> </columns> </TableView> </children> </AnchorPane>
次にコードを実装していきます。まず、JavaFXWebSocket.java についてはデフォルトのまま一切編集を加えずにそのまま利用します。
package javafx.websocket; import javafx.application.Application; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.stage.Stage; public class JavaFXWebSocket extends Application { @Override public void start(Stage stage) throws Exception { Parent root = FXMLLoader.load(getClass().getResource("Sample.fxml")); Scene scene = new Scene(root); stage.setScene(scene); stage.show(); } public static void main(String[] args) { launch(args); } }
次に、SampleController.java を編集します。@FXML TableView table は、Sample.fxml に追加した TableView と一対一で対応します。次に、ボタンが押下された際の処理を実装しますが、Button が押下された時の処理は別スレッドで実装します。別スレッドで実装しないと、処理が終了するまで処理が専有してしまうため他に一切処理ができなくなります。ボタンが押下された際の処理は、javafx.concurrent.Service を継承したクラスで実装します。(※ JavaFX では非同期処理を javafx.concurrent.Service, javafx.concurrent.Task 等を利用して実装します。)
package javafx.websocket; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.ResourceBundle; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import javafx.concurrent.Service; import javafx.concurrent.Task; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.cell.PropertyValueFactory; import javax.websocket.ClientEndpointConfiguration; import javax.websocket.DefaultClientConfiguration; import javax.websocket.DeploymentException; import org.glassfish.tyrus.client.ClientManager; public class SampleController implements Initializable { @FXML private TableView table; @FXML private TableColumn<RowData,String> column; @FXML private void handleButtonAction(ActionEvent event) { TwitterCheckService thread = new TwitterCheckService(table); thread.start(); } @Override public void initialize(URL url, ResourceBundle rb) { table.setEditable(true); column.setResizable(true); column.setCellValueFactory(new PropertyValueFactory<RowData, String>("message")); } class TwitterCheckService extends Service { private TableView table; private CountDownLatch messageLatch = null; public TwitterCheckService(TableView table) { this.table = table; } @Override protected Task createTask() { Task<Void> task = new Task<Void>() { @Override protected Void call() throws Exception { messageLatch = new CountDownLatch(1); try { URI clientURI = new URI("ws://localhost:8080/TwitterTimeLine/twitter"); // ClientContainer cliContainer = ContainerProvider.getClientContainer(); ClientManager cliContainer = org.glassfish.tyrus.client.ClientManager.createClient(); ClientEndpointConfiguration clientConfig = new DefaultClientConfiguration(); cliContainer.connectToServer(new TwitterClient(table), clientURI); messageLatch.await(1, TimeUnit.SECONDS); } catch (DeploymentException | URISyntaxException | InterruptedException ex) { Logger.getLogger(SampleController.class.getName()).log(Level.SEVERE, null, ex); } return null; } }; return task; } } }
上記のクラスにおいて、初期化の initialize メソッド中に、column.setCellValueFactory(new PropertyValueFactory(“message”));
を記載しており、ここで Table の各行で表示する内容を設定しています。実際に各行で表示される内容は RowData の message に設定される内容を表示します。message は 単なる String 型ではなく、javafx.scene.text.Text 型としていますが、これは Table の各セルで文字列の改行が自動的になされないため、Text でラッピングし Text#setWrappingWidth で文字列の長さを調整しています。
package javafx.websocket; import javafx.scene.text.Text; public class RowData { private Text message; public RowData(Text message) { this.message = message; message.setWrappingWidth(400); } public Text getMessage() { return message; } public void setMessage(Text message) { this.message = message; } }
また、SampleController クラスの中で、WebSocket のクライアント側の実装として重要な部分を下記に抜粋します。まず、コメントしている側のコードなのですが、本来 WebSocket のクライアントの実装として推奨されるのはコメントしてある側のコードです。ClientContainer を ContainerProvider.getClientContainer() から取得するというのが正しい実装で、実際に利用するクラスは、システム・プロパティに
“webocket.clientcontainer.classname=実際のクラス名” という記載を行う事でプロパティから読み込みます。しかし、今回エラーが発生してし取得する事ができなかったため、ClientContainer の実装(ClientManager)を直接、org.glassfish.tyrus.client.ClientManager.createClient() から取得しています。
URI clientURI = new URI("ws://localhost:8080/TwitterTimeLine/twitter"); // ClientContainer cliContainer = ContainerProvider.getClientContainer(); ClientManager cliContainer = org.glassfish.tyrus.client.ClientManager.createClient(); ClientEndpointConfiguration clientConfig = new DefaultClientConfiguration(); cliContainer.connectToServer(new TwitterClient(table), clientURI); messageLatch.await(20, TimeUnit.SECONDS);
また、cliContainer.connectToServer(new TwitterClient(table), clientURI) で記載している TwitterClient(table) は、WebSocket のサーバ側の実装では @WebSocketEndpoint のアノテーションが付加されたクラスと同じ働きをするクライアント側の実装を行っているクラスと認識していただければと思います。下記に TwitterClient クラスの実装を記載します。
package javafx.websocket; import javafx.collections.ObservableList; import javafx.scene.control.TableView; import javafx.scene.text.Text; import javax.websocket.Session; import javax.websocket.WebSocketClient; import javax.websocket.WebSocketClose; import javax.websocket.WebSocketMessage; import javax.websocket.WebSocketOpen; @WebSocketClient public class TwitterClient { TableView table; ObservableList<RowData> list; public TwitterClient(TableView table) { this.table = table; } @WebSocketOpen public void onOpen(Session session) { System.out.println("Connection had opened."); } @WebSocketMessage public void onMessage(String message) { if (message.length() == 0) { return; } // Table 内で文字列を改行するために String を Text で Wrap Text text = new Text(message); list = table.getItems(); list.add(0,new RowData(text)); } @WebSocketClose public void closeConnection(Session session) { System.out.println("Connection had closed."); } }
サーバ側で WebSocket のコードを実装する場合は、@WebSocketEndpoint アノテーションを付加していました、クライアント側はその代わりに、@WebSocketClient アノテーションを付加したクラスを実装します。実際に記載する内容は、@WebSocketEndpoint の内容と同等で、@WebSocketOpen, @WebSocketMessage, @WebSocketClose のアノテーションを付加したメソッドをそれぞれ実装します。このプログラムでは、サーバ側からメッセージが配信されてき、サーバ側に対してメッセージを送信する必要はありませんので、実際に
は @WebSocketMessage を付加した public void onMessage(String message) メソッドの部分が重要になります。ここでは、サーバ側からメッセージが配信された場合、受信 した文字列を Text text = new Text(message) でラッピングし、現在 Table に設定されている Item を抜き出した後、Table の先頭行に RowData を追加しています。
この程度のプログラムであれば、特に難しい事を考えなくても簡単に、クライアント側の WebSocket プログラムを作成する事ができるかと思います。
最後に、
今年は Java EE の単独で Advent Calendar が作成された事に対してとても嬉しく思うと共に、作成してくださった、@megascus さん本当にありがとうございました。
Java EE 7 は、来年の春頃にリリースされますが、WebSocket の他にもとても便利になった API などが多数提供される予定です。いち早く、Java EE 7 の各機能を試してみたい方は GlassFish v4 の開発ビルドをダウンロードしていただく事で試していただく事もできます。是非今から Java EE 7 にお備えください。