この記事はJavaFX Advent Calendar 2013の14日目、昨日はwatermintさんのJavaFXアプリケーションの手触り感をととのえる、Fextileでした。
またこの記事はGlassFish Advent Calendar 2013の14日目、昨日は蓮沼さんのGlassFishの構成項目とモニタリング項目でした。 (ごめんなさい!JAX-RSでサーバーサイドをつくってGlassFish Advent Calendarの記事にもする予定でしたけどタイムオーバー!クライアントだけで許してください!)
ぼくのJavaFX力は@skrbさんのハンズオンを少し触ったくらいで、JavaFXの細かい話は知りません・・(当日となりでJavaEEハンズオンの講師をしていたので受けていない・・)
ぼくはJavaFXの詳細機能より、アプリのアーキテクチャ的なことのほうが興味があり、以前紹介したThin Server ArchitectureをJavaFXで実現するべく、今回はJavaFXからRESTを叩くHello World的なものに挑戦してみました。
たたくRESTサービスは超簡単なもので、先日Spring Frameworkが出したチュートリアル集であつかっているものを使います。Backbone.jsで叩く例はこちら。
こんなやつです。http://rest-service.guides.spring.io/greeting?name=yourname
実装する上でのポイントは
- JAX-RS Clientを使う
- Webサービスには別スレッドからアクセスする
です。ハンドラメソッドから直接REST Client叩けば簡単なのですが、せっかくなので将来的なユーザビリティを意識して、この記事にあるようにjavafx.concurrent.Service
をつかって別スレッドでアクセスしてみます。
ソースコードを貼っていきます・・
ちなみに、雛形をNetBeans 7.4でつくって、開発はIntelliJ IDEA 13で行いました。
pom.xml
JAX-RS Clientを使うためにjersey-client
を使うのと、JSONを扱うためにjersey-media-json-jackson
が必要です。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>javafx</groupId>
<artifactId>javafx-rest</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>javafx-rest</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<mainClass>javafx.rest.MainApp</mainClass>
</properties>
<organization>
<!-- Used as the 'Vendor' for JNLP generation -->
<name>Your Organisation</name>
</organization>
<dependencies>
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-client</artifactId>
<version>2.4.1</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<version>2.4.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.6</version>
<executions>
<execution>
<id>unpack-dependencies</id>
<phase>package</phase>
<goals>
<goal>unpack-dependencies</goal>
</goals>
<configuration>
<excludeScope>system</excludeScope>
<excludeGroupIds>junit,org.mockito,org.hamcrest</excludeGroupIds>
<outputDirectory>${project.build.directory}/classes</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.2.1</version>
<executions>
<execution>
<id>unpack-dependencies</id>
<phase>package</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>${java.home}/../bin/javafxpackager</executable>
<arguments>
<argument>-createjar</argument>
<argument>-nocss2bin</argument>
<argument>-appclass</argument>
<argument>${mainClass}</argument>
<argument>-srcdir</argument>
<argument>${project.build.directory}/classes</argument>
<argument>-outdir</argument>
<argument>${project.build.directory}</argument>
<argument>-outfile</argument>
<argument>${project.build.finalName}.jar</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
<compilerArguments>
<bootclasspath>${sun.boot.class.path}${path.separator}${java.home}/lib/jfxrt.jar</bootclasspath>
</compilerArguments>
</configuration>
</plugin>
</plugins>
</build>
</project>
エントリポイント
この辺はテンプレですね。
package javafx.rest;
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class MainApp extends Application {
@Override
public void start(Stage stage) throws Exception {
Parent root = FXMLLoader.load(getClass().getResource("/fxml/GreetView.fxml"));
Scene scene = new Scene(root);
scene.getStylesheets().add("/styles/Styles.css");
stage.setTitle("Greet FX");
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
モデルクラス
REST APIのスキーマにあわせてモデルを作成します。
package javafx.rest;
public class Greet {
private Long id;
private String content;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
@Override
public String toString() {
return "Greet{" +
"id=" + id +
", content='" + content + '\'' +
'}';
}
}
ビュー
テキストフィールドとボタンがあるシンプルなビューです。
<?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="120.0" prefWidth="320.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/2.2" fx:controller="javafx.rest.GreetController">
<children>
<Button fx:id="button" layoutX="220.0" layoutY="37.0" onAction="#handleAction" prefHeight="21.999908447265625" text="greet" />
<Label fx:id="label" layoutX="33.0" layoutY="66.0" minHeight="16.0" minWidth="69.0" />
<TextField fx:id="name" layoutX="33.0" layoutY="36.0" onAction="#handleAction" prefWidth="162.0" />
</children>
</AnchorPane>
非同期サービスの実装
RESTサービスにアクセスする処理を実行するjavafx.concurrent.Task
をつくるavafx.concurrent.Service
実装クラスです。引数はコントローラーからバインドしてもらうためにjavafx.beans.property.StringProperty
をつかいます。プロパティは櫻庭さんのハンズオンで習いましたね。RESTにアクセスする処理はGreetRestClient
に外だししました。
package javafx.rest;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
public class GreetService extends Service<Greet> {
private GreetRestClient greetRestClient = new GreetRestClient();
private StringProperty name = new SimpleStringProperty();
public void setName(String name) {
this.name.set(name);
}
public String getName() {
return name.get();
}
public StringProperty nameProperty() {
return name;
}
@Override
protected Task<Greet> createTask() {
return new Task<Greet>() {
@Override
protected Greet call() throws Exception {
return greetRestClient.greet(name.get());
}
};
}
}
REST Webサービスアクセス
JAX-RS Clientを使います。簡単です。
(スタンドアローンのJersey ClientでJSONをパースするには、どうもnew ClientConfig().register(new JacksonFeature())
が必要なようで、これの設定を調べるのに一番時間がかかった・・・)
package javafx.rest;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.jackson.JacksonFeature;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.core.MediaType;
public class GreetRestClient {
Client client = ClientBuilder.newClient(new ClientConfig().register(new JacksonFeature()));
public Greet greet(String name) {
if (name == null || name.isEmpty()) {
return null;
}
Greet greet = client.target("http://rest-service.guides.spring.io").path("greeting").queryParam("name", name)
.request(MediaType.APPLICATION_JSON_TYPE)
.get(Greet.class);
return greet;
}
}
コントローラークラス
いよいよコントローラーです。initialize
メソッドでテキストフィールドのプロパティをGreetService
にバインドして、コールバックの設定もします。
折角なのでリクエストしている間は再入力できないようはテキストフィールドとボタンを非活性にして、レスポンスがきたら活性にもどしています。(エラー処理はしていない・・)
Service
の使い方が正しいのかどうかわかっていません。。
package javafx.rest;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.concurrent.WorkerStateEvent;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
public class GreetController implements Initializable {
@FXML
private Label label;
@FXML
private Button button;
@FXML
private TextField name;
GreetService greetService = new GreetService();
@FXML
private void handleAction(ActionEvent event) {
label.setText("Now loading...");
greetService.restart();
disable();
}
@Override
public void initialize(URL url, ResourceBundle rb) {
greetService.nameProperty().bind(name.textProperty());
greetService.setOnSucceeded(new EventHandler<WorkerStateEvent>() {
@Override
public void handle(WorkerStateEvent event) {
Greet greet = (Greet) event.getSource().getValue();
if (greet != null) {
label.setText(greet.getContent() + " (id=" + greet.getId() + ")");
}
enable();
}
});
greetService.start();
}
void disable() {
name.setDisable(true);
button.setDisable(true);
}
void enable() {
name.setDisable(false);
button.setDisable(false);
}
}
実行!
こんな感じです。
この記事はJava EE 7 Recipes: A Problem-Solution Approach (Recipes Apress)の「18-5. Incorporating REST Services into JavaFX Applications」を参考にしました。
Apress (2013-05-22)
売り上げランキング: 106,234
本当はJAX-RSでサーバーサイドを実装するところまでやってGlassFish Advent Calendarのネタにもなるようにしたかったけど、間に合わなかった!ごめんなさい蓮沼さん! 明日こそはサーバーサイドをつくる!