IK.AM

@making's tech note


【JavaFX GlassFish Advent Calendar 2013 14日目 JavaFXからREST Webサービスにアクセスする

🗃 {Programming/Java/JavaFX/AdventCalendar/2013}
🗓 Updated at 2013-12-14T14:11:16Z  🗓 Created at 2013-12-14T14:11:16Z   🌎 English Page

この記事は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」を参考にしました。

Java EE 7 Recipes: A Problem-Solution Approach (Recipes Apress)
Josh Juneau
Apress (2013-05-22)
売り上げランキング: 106,234

本当はJAX-RSでサーバーサイドを実装するところまでやってGlassFish Advent Calendarのネタにもなるようにしたかったけど、間に合わなかった!ごめんなさい蓮沼さん! 明日こそはサーバーサイドをつくる!


✒️️ Edit  ⏰ History  🗑 Delete