Java Client Roadmap Updateによせて (後編)

というわけで先日アップした次のエントリの後編です。

aoe-tk.hatenablog.com

前回は年寄りの思い出話という感じでしたがまさかの大きな反響を頂いて驚いています。後編については JavaFX や Swing、そしてクロスプラットフォーム GUI の今後について思うところを書いていきたいと思います。

JavaFX は今後どうなる?

今回の決定で JavaFXJDK リリースから分離されることになったわけですが、逆に言うと JDK のリリースサイクルに縛られること無く開発を進められることになります。そして、私の感覚からすると、当面 JavaFX が廃れるような心配はしなくていいと見ています。

JavaFXJava EE と同様によりオープンソースコミュニティに今後の開発をゆだねることになりましたが、JavaFX のコミュニティは今でもとても盛り上がっています。OpenJFX の ML では今でも盛んに議論がされていますし、先のホワイトペーパーでも「情熱的なコミュニティ」であると賞賛しています。

実は今回の発表の少し前に OpenJFX のリーダーである Kevin Rushforth 氏から OpenJFX コミュニティの今後の方向性について問いかけがありました。

http://mail.openjdk.java.net/pipermail/openjfx-dev/2018-February/021335.html

この議論では大変な盛り上がりを見せ、より外部の人が入りやすくなるような雰囲気にしようという方向になりました。その一環として GitHub に OpenJFX のリポジトリのミラーも作られ、PR を受け付けられるようにしています (OCA へのサインは必要です) 。

github.com

このように、JDK のリリースサイクルに縛られず、より外部も参加しやすくなることで、かえって開発のスピードが上がる可能性もあると考えています。前編でも述べたように海外では結構採用事例があり、Gluon のように JavaFX をビジネスのメインにしている会社もあります *1

と言うわけで、Java の標準 GUI では無くなりましたが、優秀なライブラリとしてあり続けるだろうと思っています。前編でも述べましたが、とても開発しやすい API ですし、JavaGUI を作るときの最有力候補であることは変わらないと思っています。コミュニティとの連携も取りやすくなる方向になっていますし、業務で JavaFX を採用したところがあってもそんなに心配しなくてもいいと思います。

私自身はここ数年フロントエンドとは余り縁の無い仕事をしている事もあり、業務で JavaFX を触る機会は無かったのですが、自分の身の回りでで GUI を使った道具が必要な時は JavaFX で作っていました。今後もそのためには JavaFX を使い続けると思っています。

Swing について

紆余曲折を経て、Java のデフォルト GUI は Swing に戻った感があるのですが、前編でも述べたように OpenJDK では再び Swing に力が入りそうな雰囲気になっています。Swing に JavaFX の良いところ (FXML とかバインディングとか) が移植されるような流れにならないかなあ。

私は Swing は一定の成功を収めたと思っています。何より Swing が覇権を握っている分野があります。それは IDE です。NetBeans と JetBrains の IDE だけじゃないかと突っ込まれそうですが、特に JetBrains IDE のここ最近の躍進ぷりはすごいです。JavaScriptPythonPHPRuby、Go など実に様々な言語コミュニティで人気を博しています。

クロスプラットフォーム GUI の今後

今回の件でクロスプラットフォーム GUI というのはやっぱり難しいものだなとは思いました。でも需要があるのは確かです。ゲームエンジンの様にユースケースをより絞ったものは普及していますしね。

汎用的なクロスプラットフォーム GUI で一番成功しているのはやはり Qt でしょうか。モバイルへの進出にも成功していますし。開発言語は C++ ですが、他のプログラミング言語へのバインディング も多いです。ですが、Java バインディングである Jambi が死んでしまったんですよね...。 *2

後は有力候補としてはやはり Electron なんですかねえ。私は好きじゃないんですよ。ぶっちゃけ Chrome ブラウザそのものなので、Electron アプリを 1 つ立ち上げると (潤沢にリソースを使う) Chrome ブラウザが余計に 1 つ立ち上がるようなものなので、使う側としてそんなに好きじゃないんです。Web 開発の難しさをデスクトップ GUI アプリ開発に持ち込みますし、そんなに JavaScript + CSS で開発したいですか?と言いたい感じです。まあ古典的なスタイルの GUI 開発が好きな人間としては、WebComponents に期待しているところです。

モバイル OS 戦争に敗れ去った MS は Xamarin に力を入れていますし、最近は Flutter なんてのも登場しましたし、今後もクロスプラットフォーム GUI へのトライアルは色々な形で出てくるでしょうね、というところで雑感垂れ流しのエントリを締めくりりたいと思います。

*1:この会社は JavaFXiOSAndroid 向けアプリを開発できる、Gluon Mobile などを販売しています。

*2:3 年ほどコミットがなく、公式サイトもドメインが乗っ取られています...。

Java Client Roadmap Updateによせて (前編)

既にご存知の方も多いと思いますが、先日 Oracle から JavaFX をはじめとする、Java のクライアントテクノロジーについて今後のロードマップが発表されました。

https://blogs.oracle.com/java-platform-group/the-future-of-javafx-and-other-java-client-roadmap-updates

上記ブログエントリでは主に JavaFX の今後の扱いについて述べていますが、以下のホワイトペーパーにはそのほかに Applet や Java Web Start、そして Swing/AWT といった Java のクライアントテクノロジー全般の今後のロードマップについて記載されています。

http://www.oracle.com/technetwork/java/javase/javaclientroadmapupdate2018mar-4414431.pdf

このホワイトペーパーを読んで、まあ色々思うことがありました。Java のクライアントテクノロジーについての過去も振り返りながら私が感じたことを色々語りたくなったので、このエントリを書きました。まあ巷で言う「ポエム」ってやつですね。

長くなったので前後編に分けて書くことにしました。

ホワイトペーパーに書かれていたこと

ホワイトペーパーに記載されていた内容を簡単にまとめると次の通りです。

  • Oracle Java SE 11 からは JavaFX を同梱しないことを確定
    • これまで Oracle JDK には JavaFX を同梱、OpenJDK には同梱しないという状況だったが Oracle JDK も同様になる
    • 今後も OpenJDK には同梱されないという状況はそのまま
    • JavaFXJava のデフォルト GUI ツールキットではなくなることを意味する
  • Applet のサポートは予告通り Java SE 8 までで、Java SE 11 には含まれなくなる
  • Java Web Start についても Java SE 11 以降には含まないことを明言
    • これは今回の発表で初めて明らかになったこと
  • Swing と AWT については Java SE 11 においても開発を継続する

JavaFX については後ほど述べるとして、注目してもらいたいことは Applet、Web Start の扱いです。 これらに対する実行環境のアップデートが提供され続けるのは Java SE 8 のサポート期間の間だけ ということです。

よって、特に Applet を使った業務システムを展開しているところは Java SE 8 の無償アップデート提供期間中に対応を検討する必要があります。特にブラウザ Java Plug-in 部分はクライアント PC のセキュリティに直結するため、アップデートせずに放置するという選択肢は危険です。対応策としては次のようになります。

  • Oracleの商用サポートを契約し、継続して実行環境のアップデートを受け取る
  • アプリケーションを改修し、ブラウザ上で動かすのではなく、JRE を同梱したスタントアロンアプリケーションとしての配布に切り替える

というわけで、このロードマップ発表で Java のクライアントテクノロジーは転換点を迎えることになりました。これまでの経緯をちょっと振り返ってみようかなと思います。

Java クライアントテクノロジーの変遷について振り返り

Java 誕生時の GUI

Java は誕生時から AWT という GUI ツールキットを同梱していました。Java の主な目的の1つに Web ブラウザ上で GUI アプリケーションを動かせるようにすることがあったからです。

AWT は画面に描画する GUI 要素は WindowsMac といった実行環境のコントロールを使っていました。それに対して抽象化層を提供するものであったため、用意された GUI 要素は最大公約数的なものでした。そこで、AWT をベースに、Java2D を使用して全ての GUI 要素を自分で描画するSwing というツールキットが登場します。これは当時 Netscape 社が独自に開発したものに Sun が目を付けて共同開発したものです。

Swing はどの OS でも動く GUI を構築できるということで非常に画期的でしたが、如何せん登場時期が早すぎました (20世紀のことです) 。当時のクライアント PC のスペックでは動作が重すぎました。私は MMX Pentium 166MHz のマシンで初めて Swing を動かしたのですが、全ての動きがスローモーションで笑っちゃうしかなかったのを覚えています。

ですがこれにめげることなく継続的にアップデートを続け、JDK1.4 あたりでようやく実用的な速度が出るようになり、この頃から業務アプリケーション向けクライアント開発をはじめ、そこそこ採用されるようになってきた印象があります。私もこの時期に Swing での開発を経験しています。

Swing の発展

そして Java 5 から 6 に掛けた辺りで、当時既に普及していた Web アプリケーションに対して、当時の HTML の表現力の限界からリッチクライアントが再び見直される流れが出てきます。特に Macromedia (現在は Adobe に吸収) が Flash を業務クライアントとして推すことに熱心で、ここから Flex が誕生します。その流れを受け、Java SE 6 の頃に Sun は Swing 関連の技術に大きなてこ入れを行います。

  • Java.net にデスクトップ専用のプロジェクトができる
  • Java Plug-in の強化
  • デスクトップ環境と連携する API の追加
    • 特定のファイル・タイプに関連付けられたデフォルト・アプリケーションと対話する機能を提供する java.awt.Desktop クラス
    • デスクトップ環境のシステムトレイにアクセスし、アプリケーション独自のトレイアイコン、メニューを追加できる java.awt.SystemTray クラス
  • Swing 開発のためのアプリケーションフレームワークプロジェクトとその関連技術の開発が始まる

RIA の潮流と JavaFX の登場

この頃が Swing の全盛期だったかなあと思っています。そして大体同じ頃に別の流れが出てきました。それがコンシューマ向け Web を対象とした RIA の流れです。MicrosoftSilverlight をリリースした辺りから Flash と共に次のような特徴を備えたものとして注目され始めました。

  • HTML の限界を超えた操作性を提供
  • Web ブラウザ上でもデスクトップ上でも実行可能
  • お仕着せのコンポーネントでは無く、デザイナが GUI 要素を自由自在にデザインできる
    • この頃 iPhone がマルチタッチによる操作性と流麗なグラフィックの GUI をひっさげてデビューしたことも大きな影響を与えていたと思います

もちろん Java にも Applet や Web Start という同様の技術がありました。Swing も GUI 要素を自分で描画する仕組みになっているため、頑張ればどんな見た目にすることもできます。しかし、デザイナが扱うのには向いていないプラットフォームでした。

そこで Sun が目を付けたのが、Sun が買収した企業である SeeBeyond に所属していた Christopher Oliver 氏が開発していた From Follows Function (F3) というプロジェクトでした。 これは確か Swing を開発しやすくするための DSL だったと思います。 (さくらばさんより指摘があり、これは正しくないとのことでした。F3 は Flash + ActionScriptDSL を目指したもので、たまたま実行の土台として Swing を使っていたとのことです。) Sun はこの技術をベースに JavaFX というプロジェクトを立ち上げ、次のような特徴を備えたコンシューマ向け GUI 開発、実行環境としてデビューさせます。

最初は Swing のラッパーコンポーネントという印象もあった JavaFX ですが、Sun はこれを AWT に頼らない GUI ツールキットとして発展させます。Prism エンジンを開発して GPU を積極的に活用する新世代の Java GUI ツールキットになっていきました。この頃から Sun は Swing への投資を急速にトーンダウンさせ、先ほど挙げた Swing App Framework 関連の JSR は中止してしまいました。これは当時 Swing 開発者からの反発もかなり大きかったことを覚えています。

ともかく Sun は JavaFX をすごくアピールしていました。Sun の開催するカンファレンスでは JavaFX を使った流麗なインターフェースをよくデモしていました。ですが、最も JavaFX を浸透させたかったスマートフォンへの展開を失敗してしまいます。旧 Windows Mobile に提供したのみで、水面下で Android への採用も持ちかけていたという話を聞いたこともあるのですが、現実はそうでないことは皆さんのよく知るところです。

そして Oracle による Sun 買収というイベントが起きます。

Oracle の買収と JavaFX の方向転換、発展

Oracle の買収で JavaFX がどうなるか不安視されましたが、JavaFX は引き続き盛り上げていくことを宣言します。ですが、OracleJavaFX をコンシューマ向け開発では無く、あくまで業務向け開発のツールにすることを重視しました。...とは Oracle ははっきり言っていませんが、その後の動きや Oracle のビジネス領域から考えれば明白でした。

JavaFX 2.0 において、JavaFX Script を廃止することが決まりました。そして、Java + FXML で開発するというスタイルに変わります。当然のことながらこれは大騒ぎになりました。 *1

ですが、結果としてこれは JavaFX にはプラスとなりました。本来の Java 開発者層が JavaFX に関心を持ち、注目度がぐっと上がりました (そのかわり Web デザイナ層は離れていきましたが...) 。古くささが見えてきた Swing と比べ、次のような点が優れていました。

  • FXML による GUI 構造の分離
  • プロパティとバインディング API によるリアクティブなスタイルでのビュー更新
    • これは遅延実行の仕組みを備えるなど、他のプラットフォームと比べても優れていました
  • Lambda Ready

私が JavaFX を本格的に触りだしたのもやはりこの時期でした。Flex による RIA 開発経験をしたこともあり、ユーザーに様々な UX を提供可能な RIA の可能性に目を向けていたところ、Java で開発可能な RIA プラットフォームが登場したので注目をしたのです。Flex と比べて優れているところも多いと評価していました。

2013 年の Java Day Tokyo の Java the Night に登壇したとき はこんなツールをデモして、JavaFX の魅力をアピールしたりしてました。

f:id:aoe-tk:20130519005331p:plain

Oracle は積極的に JavaFX の開発を進め、JavaFX 8 までは盛んに機能追加も行い、今後 Java の標準 GUIJavaFX になると宣言しました。JavaOne で iOSAndroidバイスでの稼働をデモしたりと、Oracle は本気だと私もすごく期待していました。

主に欧州を中心に業務系システムやトレーディングツールなどでの採用例が出てくるようになります。NASA など科学技術研究の現場での採用も多かったように思います。 Eclipse GEF も最新版では JavaFX ベースになっています。

そして Oracle の方向転換

ですが、JavaFX 8 のリリースがピークでした。JavaFX 8 リリースの後辺りから次のように徐々に暗雲が垂れ込んできました

  • iOSAndroid への搭載の話はトーンダウンし、OpenJFX でソースだけ公開してお茶を濁す
    • 肝心の JVM 部分の公開がなかった
  • Raspberry Pi での実行にも積極的になったかと思えば、1 年後にはトーンダウン
    • 最初は Oracle Java SE Embedded に JavaFX を同梱していましたが、途中からやめてしまいました
  • Scene Builder も 2.0 リリースを最後にメンテナンスモードに

Java 9 では Jigsaw 対応で手一杯で新機能の追加はほとんどなく、明らかにリソースを絞っているように見えました。そして今回の発表につながります。

ホワイトペーパーを見る限り、OracleJava のクライアントテクノロジーに対する興味がほとんど無いことが分かります。現在は Web ファースト、モバイルファーストの流れであると結論付けています。まあ Oracle の立ち位置的にここまで見てくれたのが奇跡的ですらあったのかも知れませんが。

なお OpenJDK コミュニティは AWT、Swing、Java2D の強化に積極的で、そういう意味では様々な紆余曲折を経て Java 標準 GUI の座は再び Swing に戻ってきたのかも知れません。

と言うわけで Java のクライアントテクノロジーの歴史を振り返ったところで前編終了。長くなりすぎて疲れましたw 後編では JavaFX 、そしてクロスプラットフォーム GUI の今後について思うところを書こうかなと思っています。

*1:当時のさくらばさんの怒り様はまあすごかったです (^^;;

Bean ValidationのJavaFX対応

このエントリは JavaFX Advent Calendar 2017 の 19 日目のエントリです。前日は id:planet-az さんによる「 簡単なミュージックプレーヤーをさらにいじってみた 」でした。

はじめに

今回取り上げるのは、 JSR 380 Bean Validation 2.0JavaFX 対応についてです。そうです、Java EE に含まれる Bean Validation が JavaFX に対応したのです! まさか EE の仕様が JavaFX のことを考慮するとは思わなかったので、これは非常に驚きました。

具体的には JavaFX のプロパティ API に対して Bean Validation が対応しました。このエントリではその使い方について紹介したいと思います。

Bean Validation 2.0 について

Bean Validation 2.0 は Java EE 8 に含まれる仕様です。以下の Web サイトに Specification がまとめられています。

http://beanvalidation.org/2.0/spec/

新機能の 1 つとして次のようなものが挙げられています。

  • Support for validating container elements by annotating type arguments of parameterized types, e.g. List<@Positive Integer> positiveNumbers (see Container element constraints); this also includes:
    • More flexible cascaded validation of collection types; e.g. values and keys of maps can be validated now: Map<@Valid CustomerType, @Valid Customer> customersByType
    • Support for java.util.Optional
    • Support for the property types declared by JavaFX
    • Support for custom container types by plugging in additional value extractors (see Value extractor definition)

新たにコンテナ要素のバリデーションに対応しているとあります。コレクションや Optional オブジェクトなどの中身に対してバリデーションを掛けられるようになったのです。そしてこの対応の一環として、 JavaFX のプロパティもデフォルトで対応するようになったのです。 *1

JavaFX アプリケーションでの Bean Validation の使い方

それでは早速使ってみます。折角 JavaFX を使っているので、バインドを活用してみることにします。

作成するサンプルアプリケーション

作ってみるのは次のようなユーザ登録フォームを想定したものです。

f:id:aoe-tk:20171218231023p:plain

テキストフィールドに文字を打ち込むたびにバリデーションを行い、正しい文字列が入力されたタイミングでエラーメッセージが消えるようになります。E-mail、パスワードどちらも OK になれば登録ボタンがクリック可能になります。

f:id:aoe-tk:20171218231509g:plain

E-mail 欄のバリデーションには Bean Validation 2.0 で新たに入った @Email を使ってみることにします。

依存性の設定

まず、JavaFX アプリケーションで Bean Validation を使えるようにします。参照実装である Hibernate Validator を使います。Maven だと pom.xml に次のように依存設定を記述します。

<?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>

<%-- Snip --%>

    <dependencies>
        <dependency>
            <groupId>org.hibernate.validator</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>6.0.4.Final</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish</groupId>
            <artifactId>javax.el</artifactId>
            <version>3.0.0</version>
        </dependency>
    </dependencies>

<%-- Snip --%>

</project>

Model、FXML の実装

Model のコードは次のようになります。

package aoetk.sample.beanvalidation;

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;

public class Model {
    @NotEmpty
    @Email
    private StringProperty email = new SimpleStringProperty("");

    @Size(min = 8)
    private StringProperty password = new SimpleStringProperty("");

    public String getEmail() {
        return email.get();
    }

    public StringProperty emailProperty() {
        return email;
    }

    public void setEmail(String email) {
        this.email.set(email);
    }

    public String getPassword() {
        return password.get();
    }

    public StringProperty passwordProperty() {
        return password;
    }

    public void setPassword(String password) {
        this.password.set(password);
    }
}

フィールドに着目してください。このように JavaFX プロパティに対して直接 Bean Validation のアノテーションを指定可能になっています。

次に View としての FXML のコードを示します。

<?xml version="1.0" encoding="UTF-8"?>

<%-- import略 --%>
<GridPane hgap="10.0" vgap="10.0" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml/1" fx:controller="aoetk.sample.beanvalidation.Controller">
   <rowConstraints>
      <RowConstraints minHeight="10.0" />
      <RowConstraints minHeight="10.0" />
      <RowConstraints minHeight="10.0" />
   </rowConstraints>
   <columnConstraints>
      <ColumnConstraints hgrow="NEVER" minWidth="10.0" />
      <ColumnConstraints hgrow="ALWAYS" minWidth="10.0" />
   </columnConstraints>
   <children>
      <Label text="E-mail" />
      <Label text="Password" GridPane.rowIndex="1" />
      <Button fx:id="registerButton" mnemonicParsing="false" text="Register" GridPane.columnSpan="2" GridPane.halignment="CENTER" GridPane.rowIndex="2" />
      <VBox GridPane.columnIndex="1">
         <children>
            <TextField fx:id="emailField" prefHeight="27.0" prefWidth="350.0" />
            <Label fx:id="emailErrorLabel" text="Label" textFill="red" />
         </children>
      </VBox>
      <VBox GridPane.columnIndex="1" GridPane.rowIndex="1">
         <children>
            <PasswordField fx:id="passwordField" />
            <Label fx:id="passwordErrorLabel" text="Label" textFill="red" />
         </children>
      </VBox>
   </children>
   <padding>
      <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
   </padding>
</GridPane>

Scene Builder で見ないとちょっと分かりにくいかも知れませんね。入力フィールドに加え、エラーメッセージについても fx:id 属性を与えて Controller 側でアクセスできるようにしている点に注目してください。

Controller でのバインド、バリデーション実装

そして、この Model と View を Controller でバインドし、さらにフィールドに対して入力が発生した場合にバリデーションが実行されるようにします。

// (import略)
public class Controller implements Initializable {
    @FXML
    Label emailErrorLabel;

    @FXML
    Label passwordErrorLabel;

    @FXML
    Button registerButton;

    @FXML
    TextField emailField;

    @FXML
    PasswordField passwordField;

    private StringProperty emailMessage = new SimpleStringProperty("");

    private StringProperty passwordMessage = new SimpleStringProperty("");

    private Model model = new Model();

    private Validator validator;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        final ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
        validator = validatorFactory.getValidator();
        validateModel();

        emailErrorLabel.textProperty().bind(emailMessage);
        passwordErrorLabel.textProperty().bind(passwordMessage);
        registerButton.disableProperty().bind(emailMessage.isNotEmpty().and(passwordMessage.isNotEmpty()));
        emailErrorLabel.managedProperty().bind(emailMessage.isNotEmpty());
        passwordErrorLabel.managedProperty().bind(passwordMessage.isNotEmpty());
        model.emailProperty().bind(emailField.textProperty());
        model.passwordProperty().bind(passwordField.textProperty());
        emailField.textProperty().addListener(observable -> validateModel());
        passwordField.textProperty().addListener(observable -> validateModel());
    }

    private void validateModel() {
        final Set<ConstraintViolation<Model>> violations = validator.validate(model);
        emailMessage.set("");
        passwordMessage.set("");
        for (ConstraintViolation<Model> violation : violations) {
            if (violation.getPropertyPath().toString().equals("email")) {
                emailMessage.set(violation.getMessage());
            } else if (violation.getPropertyPath().toString().equals("password")) {
                passwordMessage.set(violation.getMessage());
            }
        }
    }
}

initialize() メソッドの最初で Validator オブジェクトを取得している点については特に説明不要ですね。その後すぐに validateMode() メソッドをコールして初期状態でのバリデーションを実行しています。

エラーメッセージは Validator によるバリデーションの結果出力されたメッセージをそのまま使うようにしました。メッセージを格納する StringProperty オブジェクトを Controller のフィールドとして持たせ、それをエラーメッセージの Label とバインドさせています。

mailErrorLabel.textProperty().bind(emailMessage);
PasswordErrorLabel.textProperty().bind(passwordMessage);

また、このメッセージが空文字の場合はボタンをクリック可能にし、エラーメッセージラベルもシーングラフから取り除くようにしました。これも次のようにバインドを活用しています。
(managed プロパティを false 設定すると一時的にシーングラフから取り除かれます)

registerButton.disableProperty().bind(emailMessage.isNotEmpty().and(passwordMessage.isNotEmpty()));
emailErrorLabel.managedProperty().bind(emailMessage.isNotEmpty());
passwordErrorLabel.managedProperty().bind(passwordMessage.isNotEmpty());

そして、テキストボックスやパスワードフィールドの入力内容を Model のプロパティに反映させるためにバインドします。

model.emailProperty().bind(emailField.textProperty());
model.passwordProperty().bind(passwordField.textProperty());

テキストボックスやパスワードフィールドの textProperty をリッスンし、文字列が入力されるたびにバリデーションを発火します。

emailField.textProperty().addListener(observable -> validateModel());
passwordField.textProperty().addListener(observable -> validateModel());

最後にバリデーションを実行する validateModel() の実装を見てみます。モデルに対してバリデーションを実行し、結果として返ってきた ConstraintViolation オブジェクトのプロパティ名をチェックし、対応するプロパティ用のエラーメッセージ StringProperty オブジェクトにメッセージをセットしています。 後はバインドの仕組みで自動的にメッセージ表示/非表示やボタンのクリック可/不可が制御されます。

private void validateModel() {
    final Set<ConstraintViolation<Model>> violations = validator.validate(model);
    emailMessage.set("");
    passwordMessage.set("");
    for (ConstraintViolation<Model> violation : violations) {
        if (violation.getPropertyPath().toString().equals("email")) {
            emailMessage.set(violation.getMessage());
        } else if (violation.getPropertyPath().toString().equals("password")) {
            passwordMessage.set(violation.getMessage());
        }
    }
}

JavaFX のバインドの仕組みを活用することで、プレゼンテーションサイドのロジックがかなり宣言的になっていることが分かると思います。

このようにして先のスクリーンショットに示したような動作を実現しました。

コードの全体は GitHub にアップしています。

https://github.com/aoetk/javafx-beanvalidation-sample

まとめ

以上に示したように、Bean Validation 2.0 では JavaFX のプロパティを対象としたバリデーションが可能になりました。バインドなど JavaFX の機構を活用して、ユーザに対して応答性の良いバリデーションの仕組みを実装できるようになります。

今回はキー入力のタイミングでバリデーションを行ってみましたが、もちろん他にもフォーカスアウトで実行する、(多くの Web アプリのように) ボタンクリックのタイミングで実行する、あるいはバリデーションエラーを受け取ったときは入力をキャンセルしてそもそも入力できない文字は入れられないようにする...などなど色んな実装方法が考えられます。皆さんなりのやり方で色々試してみるといいでしょう。

明日は...まだ埋まっていないな。みんな書いて書いて!

*1:Value extractor を実装することでカスタムのコンテナに対するバリデーションを行うことも可能になります

JavaFXアプリケーションでJava 9のモジュールを使うときの注意点

このエントリは JavaFX Advent Calendar 2017 の 1 日目のエントリです。最初を飾るのは初めてです。まだ参加者が少ないので、みんな参加してね!

いよいよJava SE 9がリリースされました。やはり9の注目はJigsawことモジュールシステムですね。特にJavaFXにとってはjavapackagerを用いてパッケージングした際の配布サイズを小さくすることができるので、特に重要ですね。

aoe-tk.hatenablog.com

ですが、JavaFXアプリケーションでモジュールシステムを利用すると早速引っ掛かるポイントがあります。それはFXMLです。

次のような FXML を使った簡単なアプリケーションを見てみましょう。

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>

<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="300.0" prefWidth="400.0" xmlns="http://javafx.com/javafx/9" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.Controller">
   <center>
      <StackPane prefHeight="150.0" prefWidth="200.0" BorderPane.alignment="CENTER">
         <children>
            <Label text="This is Label." fx:id="label" />
         </children>
      </StackPane>
   </center>
   <bottom>
      <HBox alignment="CENTER" BorderPane.alignment="CENTER">
         <children>
            <Button alignment="CENTER" mnemonicParsing="false" text="Button" onAction="#click" />
         </children>
         <BorderPane.margin>
            <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
         </BorderPane.margin>
      </HBox>
   </bottom>
</BorderPane>
package sample;

import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Label;

public class Controller {
    @FXML
    private Label label;

    @FXML
    void click(ActionEvent event) {
        label.setText("You clicked!");
    }
}

FXMLのいいところとして、ビューXMLとペアになるControllerクラスに対して、XML側に記述したコンポーネントへのアクセスをDIを使って提供している点ですね。publicなフィールドだけでなく、package privateやprivateなフィールドとしての宣言も可能で、その場合は @FXML アノテーションを付与すると、インスタンスがインジェクトされるようになります。イベント実行メソッドも同様に @FXML アノテーションでバインドできます。

ですが、この @FXML アノテーションを使う場合は注意が必要です。次のように module-info.java を作ってみましょう。

module fxml9sample {
    requires javafx.controls;
    requires javafx.fxml;
    exports sample;
}

すると次のような例外が飛びます。 javafx.fxml モジュールに属するクラスがこのControllerクラスに対してリフレクションによるアクセスを試みるためです。

Exception in Application start method
java.lang.reflect.InvocationTargetException
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
(中略)
Caused by: javafx.fxml.LoadException: 
fxml9sample/out/production/fxml9sample/sample/sample.fxml:15

    at javafx.fxml/javafx.fxml.FXMLLoader.constructLoadException(FXMLLoader.java:2625)
    at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2603)
(中略)
Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make field private javafx.scene.control.Label sample.Controller.label accessible: module fxml9sample does not "opens sample" to module javafx.fxml
    at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:337)
    at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:281)
    at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:176)
    at java.base/java.lang.reflect.Field.setAccessible(Field.java:170)
    at javafx.fxml/javafx.fxml.FXMLLoader$ControllerAccessor.addAccessibleFields(FXMLLoader.java:3495)
    at javafx.fxml/javafx.fxml.FXMLLoader$ControllerAccessor.access$3900(FXMLLoader.java:3344)
    at javafx.fxml/javafx.fxml.FXMLLoader$ControllerAccessor$1.run(FXMLLoader.java:3460)
    at javafx.fxml/javafx.fxml.FXMLLoader$ControllerAccessor$1.run(FXMLLoader.java:3456)
    at java.base/java.security.AccessController.doPrivileged(Native Method)
    at javafx.fxml/javafx.fxml.FXMLLoader$ControllerAccessor.addAccessibleMembers(FXMLLoader.java:3455)
    at javafx.fxml/javafx.fxml.FXMLLoader$ControllerAccessor.getControllerFields(FXMLLoader.java:3394)
    at javafx.fxml/javafx.fxml.FXMLLoader.injectFields(FXMLLoader.java:1170)
    at javafx.fxml/javafx.fxml.FXMLLoader.access$1600(FXMLLoader.java:105)
    at javafx.fxml/javafx.fxml.FXMLLoader$ValueElement.processValue(FXMLLoader.java:865)
    at javafx.fxml/javafx.fxml.FXMLLoader$ValueElement.processStartElement(FXMLLoader.java:759)
    at javafx.fxml/javafx.fxml.FXMLLoader.processStartElement(FXMLLoader.java:2722)
    at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2552)
    ... 17 more
Exception running application sample.Main

これを防ぐためには、FXMLと対になるControllerクラスが属しているパッケージに対して javafx.fxml モジュールからのリフレクションによるアクセスを許可する必要があります。そのために次のように opens 文を使って javafx.fxml モジュールに対してアクセスを許可する必要があります。

module fxml9sample {
    requires javafx.controls;
    requires javafx.fxml;
    exports sample;
    opens sample to javafx.fxml; // <- これを追加
}

既存のJavaFXアプリケーションをJava 9に移行する際に意外と引っ掛かるポイントなので注意してください。

というわけで、1 日目のエントリは小ネタでした。19 日にも書く予定ですが、そちらはもう少し大きなネタにする予定です。

明日は...執筆時点でまだいない! 誰か書いてー。

Java 9で強化されたデスクトップ環境関連のAPIをJavaFXで使ってみる

このエントリは JavaFX Advent Calendar 2016 の 18 日目のエントリです。前日は id:nodamushi さんによる「 JavaFX9からPlatformに追加されるAPIについて 」でした。

はじめに

今回のエントリは JDK9 に新たに取り込まれる "JEP 272: Platform-Specific Desktop Features" を JavaFX から触ってみるというものです。

実は JDK9 では AWT/Swing/Java2D にかなり手が入ります。次のように多くの JEP が 9 には取り込まれます。

利用者に見えない内部的な強化や HiDPI 対応のような流石に対応しないとまずいものが中心ですが、その中でも JEP 272 はアプリケーション開発者側から見ても大きな機能追加になります。

まずはこの JEP 272 について紹介し、そしてこれが JavaFX からどの程度利用できるか調べてみた結果について述べていきたいと思います。

JEP 272 について

JEP 272 は "Platform-Specific Desktop Features" の名の通り、デスクトップ環境特有の機能を Java からも利用できるようにするというものです。それもプラットフォーム固有の機能も積極的に利用するというものです。

ご存じの通り Java のデスクトップ GUI アプリケーションでは、そのアプリケーションを実行するデスクトップ環境が提供している機能を利用することは中々難しかったりします。クロスプラットフォームアプリケーションの宿命かも知れません。

ですが、過去に Java 6 でそのための API が追加されたことがありました。

  • 特定のファイル・タイプに関連付けられたデフォルト・アプリケーションと対話する機能を提供する java.awt.Desktop クラス。
    • デスクトップ環境で設定されたデフォルトブラウザやエディタ、メーラなどを Java 側から起動することができます。
  • デスクトップ環境のシステムトレイにアクセスし、アプリケーション独自のトレイアイコン、メニューを追加できる java.awt.SystemTray クラス。

システムトレイを使ったりすると、よりネイティブアプリケーションっぽくなりますよね。ですが、その後の各 OS の進化に伴い、Java から使えないデスクトップ環境の機能がどんどん増えてきました。

例えば、Windows のタスクバー、Mac の Dock では次のようにプログレスバーを表示することができるようになっています。

f:id:aoe-tk:20161218003523p:plain:w1000 f:id:aoe-tk:20161218003543p:plain:w1000

バッジも表示できるようになっていますね。

f:id:aoe-tk:20161218003757p:plainf:id:aoe-tk:20161218003806p:plain:h94

でもこれらの機能には Java からアクセスすることができません。

また、Mac 特有の話ですが、Mac で実行するアプリケーションでは、メニューバーにアプリケーション名のメニューが通常のメニューの左に表示されます。アプリケーションの About ダイアログや環境設定ダイアログはここから開けるようにするよう Mac 上で動くアプリケーションは統一されています。でも、Mac 特有の話なので、Java アプリケーションはここにアクセスすることができません。

f:id:aoe-tk:20161218004452p:plain:w250

JEP 272 はこういったデスクトップ環境が提供する機能へ Java アプリケーションでもアクセスできるようにするものなのです。

提供されている機能を列挙します。

  • デスクトップ環境が起こすイベント (スリープ、サインアウト、フォアグラウンド/バックグラウンドの切り替えなど) に対してイベントリスナやイベントハンドラを登録する。
  • タスクバーへのアクセス。プログレス表示やバッジ表示、コンテキストメニューの追加を行える。
  • Mac 特有のアプリケーションメニューへのアクセス。

API レベルでは次のような追加になります。

  • java.awt.desktop パッケージの追加
    • デスクトップ環境で発生するイベントに対応した各種イベントクラスや、それに対するイベントリスナ、ハンドラが定義されている。
  • java.awt.Desktop クラスにメソッド追加
    • デスクストップ環境で発生する各種イベントに対して、イベントリスナの追加、ハンドラの登録を行うメソッドが追加されている。
  • java.awt.Taskbar クラスの新規追加
    • タスクバー (Mac での Dock) を操作するためのメソッドが定義されている。

この APIAWT の API です。でもこのエントリは JavaFX Advent Calendar のエントリです! なので、JavaFX からこれら機能を利用できるかを調べてみることにしましょう。

JavaFX から JEP 272 を利用する

それでは JavaFX から試してみることにします。ソースコードの全体は gist にアップしています。

https://gist.github.com/aoetk/7d5cc13e64239d1233e6dd879fed682e

Desktop クラスの利用

java.awt.Desktop クラスを使うと、デスクトップ環境で発生する様々なイベントに対して応答することができるようになります。まずはこのクラスのインスタンスを取得してみます。

if (Desktop.isDesktopSupported()) {
    Desktop desktop = Desktop.getDesktop();
    addAppEvents(desktop);
    setSystemMenuHandler(desktop);
} else {
    System.out.println("デスクトップはサポートされていません.");
}

実行している環境が Desktop クラスをサポートしているかを確認するメソッドがあるので、どの環境でも動かせるよう、必ずこれでチェックするようにしましょう。static メソッドである getDesktop() メソッドを使ってインスタンスを取得します。

デスクトップ環境で発生するイベントに対して応答できるようにしてみます。イベントが発生するとその旨を ListView に表示するようにします。スクリーンのスリープを例に取ると次のようになります。

// (中略)
@FXML
ListView<String> displayList;

private ObservableList<String> eventList = FXCollections.observableArrayList();
// (中略)

private void addAppEvents(Desktop desktop) {
    // (中略)
    if (desktop.isSupported(Desktop.Action.APP_EVENT_SCREEN_SLEEP)) {
        desktop.addAppEventListener(new ScreenSleepListener() {
            @Override
            public void screenAboutToSleep(ScreenSleepEvent screenSleepEvent) {
                addMessage("画面がスリープしようとしています.");
            }

            @Override
            public void screenAwoke(ScreenSleepEvent screenSleepEvent) {
                addMessage("画面がスリープから復帰しました.");
            }
        });
    } else {
        System.out.println("ScreenSleepEventはサポートされていません.");
    }
    // (中略)
}

// (中略)
private void addMessage(String msg) {
    Platform.runLater(() -> eventList.add(msg));
}

Desktop#addAppEventListner() メソッドを使って各種イベントリスナを登録します。画面スリープに対応するイベントは ScreenSleepEvent になります。イベント別にサポート有無をチェック可能なので、チェックするようにしましょう。

JavaFX からの利用に当たって注意点があります。それはイベントリスナの処理は AWT のイベントディスパッチスレッドで実行されるということです。このスレッドは JavaFX のアプリケーションスレッドとは別スレッドです。従って、 Platform.runLater() メソッドに処理をくるむ必要があります (addMessage() メソッドの実装に注目) 。

この他にも AppForegroundEvent (フォアグランド/バックグラウンドの変化に反応するイベント) 、 AppHiddenEvent (Mac 特有の「アプリケーションを隠す」に反応するイベント) 、SystemSleepEvent (システムのスリープに反応するイベント) 、 UserSessionEvent (ログインユーザのスイッチに反応するイベント) に対するリスナをセットして実行してみました。

Mac では全てのイベントに対応しています。アプリケーション起動後、「アプリケーションを隠して復帰 -> スクリーンをスリープして復帰 -> システムをスリープして復帰 -> ユーザスイッチをして復帰」を行った結果を示します。

f:id:aoe-tk:20161218020017p:plain:w532

バックグラウンドに回ったことを検知できるのはいいですね。そのタイミングで処理を停止してリソースの消費を防いだりするようなことができますね。

Windows 環境では残念ながら SystemSleepEventUserSessionEvent にしかサポートしていませんでした。システムをスリープ、復帰させた結果を示します。

f:id:aoe-tk:20161218020602p:plain:w301

Mac のアプリケーションメニューの利用

次に Mac のアプリケーションメニューにアクセスしています。これも Desktop クラスに対してハンドラを登録する形でカスタマイズします。まずは About メニューから。

if (desktop.isSupported(Desktop.Action.APP_ABOUT)) {
    // 現時点ではJavaFXでは何も起きない
    desktop.setAboutHandler(aboutEvent -> {
        Alert alert = new Alert(Alert.AlertType.INFORMATION);
        alert.setContentText("オリジナルのAboutダイアログです.");
        alert.setHeaderText("設定");
        alert.show();
    });
} else {
    System.out.println("Aboutメニューはサポートされていません.");
}

Desktop#setAboutHandler() メソッドを使い、About メニューがクリックされた時の応答処理を登録します。オリジナルのダイアログを出そうとしたのですが...JavaFX ではそもそも About メニューの追加がされませんでした。( ;∀;)

ちなみに Swing アプリケーションで試したときはうまく動作しました。

次に設定メニューです。こちらは Desktop#setPreferencesHandler() メソッドを使い、設定メニューがクリックされた時の応答処理を登録します。

if (desktop.isSupported(Desktop.Action.APP_PREFERENCES)) {
    // 現時点ではJavaFXではエラーが起きる
    desktop.setPreferencesHandler(preferencesEvent -> {
        Alert alert = new Alert(Alert.AlertType.INFORMATION);
        alert.setContentText("オリジナルの設定ダイアログです.");
        alert.setHeaderText("設定");
        alert.show();
    });
}

ですが、このコードを実行すると Cocoa 側から次のエラーメッセージが返ってきました...。

2016-12-17 21:21:08.978 java[1903:815004] *** -[__NSArrayM insertObject:atIndex:]: object cannot be nil
2016-12-17 21:21:08.984 java[1903:815004] (
    0   CoreFoundation                      0x00007fff88a2c452 __exceptionPreprocess + 178
    1   libobjc.A.dylib                     0x00007fff89405f7e objc_exception_throw + 48
    2   CoreFoundation                      0x00007fff88942a40 checkForCloseTag + 0
    3   AppKit                              0x00007fff8e19f8ce -[NSMenu insertItem:atIndex:] + 521
    4   libawt_lwawt.dylib                  0x00000001353c156f addMenuItem + 174
    5   libawt_lwawt.dylib                  0x00000001353c13f0 -[ApplicationDelegate _updatePreferencesMenu:enabled:] + 195
    6   JavaNativeFoundation                0x000000013536fd60 +[JNFRunLoop _performCopiedBlock:] + 17
(以下略)
)

AWT のフレームが生成されていることを前提に処理を進めているように推測されます。これはちょっと残念。なお、Swing で試したときはちゃんと動作しました。

というわけで、結論としては JavaFX では現状 Mac のアプリケーションメニューの利用はできないということになります。

Taskbar の利用

次にタスクバー (Dock) の利用を試してみることにします。バッジの表示とプログレス表示を試してみます。

if (Taskbar.isTaskbarSupported()) {
    Taskbar taskbar = Taskbar.getTaskbar();
    if (taskbar.isSupported(Taskbar.Feature.ICON_BADGE_NUMBER)) {
        taskbar.setIconBadge("10");
    } else {
        System.out.println("タスクバーのアイコンバッジへの数値登録はサポートされていません.");
    }
    addAction(taskbar);
} else {
    System.out.println("タスクバーはサポートされていません.");
}

タスクバーへのアクセスは Taskbar クラスを通して行います。インスタンスは static メソッドである Taskbar.getTaskbar() メソッドを使って取得します。

アイコンバッジのセットには Taskbar#setIconBadge() メソッドを使います。Mac 環境で試すと次のようにバッジの表示に成功しました!

f:id:aoe-tk:20161218174009p:plain:w423

Windows 環境は Taskbar クラスの利用そのものは可能だったものの、アイコンバッジの設定はサポートされていませんでした。まあ、Windows でのタスクバーアイコンへのバッジ登録が可能になったのは Anniversary Update からですしね。

次にプログレス表示を試してみます。次のように、JavaFX アプリ側のプログレス表示と、タスクバー側のプログレス表示を同時に行うようにしてみました。 AnimationTimer を使って表示しています。

AnimationTimer timer = new AnimationTimer() {
    private long startTime;

    @Override
    public void handle(long currentTime) {
        long elapsedTime = currentTime - startTime;
        if (elapsedTime > PROGRESS_TIME) {
            stop();
            progress.setProgress(1.0);
            startButton.setDisable(false);
        } else {
            double rateForProgressBar = Long.valueOf(elapsedTime).doubleValue() / PROGRESS_TIME;
            progress.setProgress(rateForProgressBar);
            int rateForTaskBar = (int) (rateForProgressBar * 100);
            if (taskbar.isSupported(Taskbar.Feature.PROGRESS_VALUE)) {
                taskbar.setProgressValue(rateForTaskBar);
            }
        }
    }

    @Override
    public void start() {
        startTime = System.nanoTime();
        progress.setProgress(0);
        if (taskbar.isSupported(Taskbar.Feature.PROGRESS_VALUE)) {
            taskbar.setProgressValue(0);
        } else {
            System.out.println("タスクバーのプログレス表示はサポートされていません.");
        }
        super.start();
    }
};

タスクバーのプログレス表示は Taskbar#setProgressValue() メソッドを使います。0 から 100 の間の数値をセットします。JavaFXプログレスバーは 0 から 1 と違うんですよね...。

Mac で試したところ、次のように Dock 側に進捗を表示できるようになりました!

f:id:aoe-tk:20161218175830g:plain

Windows は残念ながら未サポートでした。何でや、Windows7 の時からサポートしてたのに!

まとめ

というわけで JEP 272 の機能を JavaFX で試してみました。分かったことをまとめると次の通りです。

  • JavaFX からも JEP 272 の機能は一部を除き利用可能。
    • ただし、イベントリスナの処理は AWT のスレッドで実行される点に注意。
  • Mac のアプリケーションメニューにアクセスする機能は JavaFX からは使えない。
  • フル機能が使えるのは Mac だけ。Windows では一部の機能しかサポートしていない。
    • Linux では Ubuntu の Unity 環境でのみ使えるようです。今回は時間がなくて試せませんでした。

Mac から優先して実装されているのは理由があります。かつて Mac 環境向けの JavaApple 自身が開発、提供しており、こういた Mac 環境特有の機能にアクセスするために EAWT という API を提供していました。

Java 7 以降、Mac 向けの Java は OpenJDK で開発されるようになりますが、この EAWT は 7、8 にはバンドルが続いていました。ですが 9 からは提供をやめることになり、代替機能を Java 側で用意することになりました。それがこの JEP 272 であるということです。

ともあれ、よりネイティブアプリケーションっぽく振る舞えるような機能が実装されたのは嬉しいことです。今後のアップデートで Mac 以外の環境向けにも実装が進んでいくことでしょう。JavaFX 向けには 10 から同等機能の実装が予定されていますが、前倒しで実装とかされないかなあ。 *1

明日は誕生日枠の id:yumix_h さんの予定です。

*1:8 の時も u40 でかなりの機能追加が入ったことがあったので

JavaFX Maven PluginのWebサイトが復活して便利になっていた

JavaFXMaven で開発する際に便利なプラグインとして JavaFX Maven Plugin というものがあります。これは javapackager を使ったビルド、パッケージング作業を Maven 経由で実行できるようにするプラグインです。

蓮沼さんの以下のブログエントリでも紹介されています。

www.coppermine.jp

NetBeans にも JavaFX 開発向けの Maven プロジェクトを作成するためのウィザードがあるのですが、こちらは Exec Maven Plugin を使って javapackager のコマンド実行をラップしただけのものです。パッケージングについても、依存ライブラリの JAR を全て解凍してから 1 つの JAR にパッケージングするようになっており、今ひとつという印象です。

この JavaFX Maven Plugin ですが、ドメインを維持することができなくなったのか、ドキュメントを載せていた Web サイトが途中で消滅してしまいました。 GitHub リポジトリの README でも Web Archive の方 にリンクが貼られているという状態ですw

ところが最近になって github.io 上に新たに Web サイトが作られるようになり、中々便利な作りになっていました。 (GitHub のコミットログを見ていると、メンテナが最初の作者とは別の人になったようで、そのことも影響している?)

http://javafx-maven-plugin.github.io/

以下のキャプチャを見てもらうと分かりますが、作りたいプロジェクトの内容を画面上でぽちぽち選択していくと、右側に pom.xml に記述すべき内容が吐き出されるようになっています。これを自分のプロジェクトの pom.xml にコピーすればいいわけです。最近こういうの増えましたねえ。

f:id:aoe-tk:20160417210337g:plain

出力を JAR にするかネイティブパッケージにするか、ビルドは Maven JavaFX Plugin のゴールを直接叩くのか、それとも Maven ライフサイクルの中で実行するのか、などといった選択肢が用意されています。

以上、ちょっと嬉しかったのでお知らせまで。

javapackagerの紹介

このエントリは JavaFX Advent Calendar 2015 の 19 日目のエントリです。前日は id:yumix_h さんによる「 JavaFXで画面解像度を調べてみる 」でした。

今回は JDK に付属しているツールである javapackager について紹介します。このツール、私が見る限り公式のドキュメント以外では断片的な解説しか無い (主にネイティブパッケージの解説などでしか登場しない) ように見受けられるので、ここでこのツールができること全般について紹介したいと思います。

アプリケーション配布を巡る環境の変化

まず、javapackager のようなツールが登場した背景について触れたいと思います。これにはアプリケーション配布を巡る環境の変化が大きく関わっていると考えています。

既にご存じの通り、Java は "Write Once, Run Anywhere." を目指して作られたものであり、Java で実装、ビルドしたアプリケーションはどの環境でもそのまま動くことが期待されています。

この Java アプリケーションの配布ですが、元々はどのプラットフォームにも JRE がインストールされていることを前提とした考え方でした。クライアントアプリケーションの配布は実行可能 JAR の形態で行うことが普通です。サーバサイド Java では JRE の上にさらに Java EE コンテナのレイヤを敷き、アプリケーションは WAR もしくは EAR として配布しますね。

ところが、最近はこのように予めランタイムを別途インストールしておき、その上にアプリケーションをデプロイするスタイルはあまり好まれなくなっているように見受けられます。必要なものは1つのパッケージに全て含まれており、箱から出したらすぐ使えるような形態 (これを「自己完結型パッケージ」と呼びます) が好まれるようになっています。

これは OS ベンダ提供のアプリケーションストアという配布形態の登場が大きく関係していると考えています。アプリケーションストアから配布するアプリケーションはサンドボックス化されており、自己完結型であることが望まれています。エンドユーザとしても、アプリケーションストアからワンクリックでインストールが可能なスタイルに慣れてしまうと、わざわざランタイムをインストールするのは煩わしく感じられるようになるでしょう。特にデフォルトでは JavaFlash などがインストールされておらず、アプリケーションストアの利用率が高い Mac 環境ではその傾向が強いように思われます。

そしてもう一つの環境の変化はセキュリティです。どの OS、プラットフォームでも以下のようにセキュリティ対策が強化され、以前のように何も考えずに「野良アプリケーション」をほいほいインストールできなくなっています。

  • Java Web StartJava Applet では未署名のアプリケーションの実行はブロックされるようになりました。
  • Windows では未署名アプリケーションをダウンロードする際にはブラウザが警告を出します。
  • Mac では Gatekeeper という仕組みが導入され、デフォルトの状態では Mac App Storeからインストールしたアプリケーション、もしくはAppleデベロッパ登録し、そのデベロッパ ID で署名したアプリケーションのみが起動可能になっています。
  • FirefoxGoogle Chrome といった Web ブラウザの拡張機能でさえも、ブラウザベンダが運営する配布サイトからのみのインストールに制限されるようになりました。

JDK に同梱されている javapackager はこのようなクライアントアプリケーション配布を巡る環境の変化に対応できるようになっています。

javapackager の概要

javapackager は JDK に付属しており、Java アプリケーションのパッケージング、デプロイメントのためのツールです。

元々は javafxpackager という名前で、その名の通り JavaFX アプリケーションをパッケージングするための専用ツールとして JavaFX SDK に同梱されていました。JDK7 update6 以降は JDK に同梱されるようになり、Java アプリケーション全般に利用可能なツールになりました。JDK8 update20 以降は javapackager に名を変え、名実共に Java SE のためのツールに昇格 (?) しています。

javapackager が提供する機能は次のようなものになります。

  • 実行可能 JAR のビルド
  • 配布用パッケージの生成
    • Java Web StartJava Applet 向けバンドルの生成
    • 自己完結型パッケージの生成
      • インストール先の OS に合わせたネイティブインストーラの生成
      • JRE も含めたパッケージを生成し、OS 側にランタイムの事前インストールを要求しない
  • 配布プラットフォームに合わせたアプリケーションの署名
  • アプリケーションストア向けパッケージの生成

以降では上に挙げた javapackager の各機能について簡単に説明していきます。

なお、この javapackager の使い方を含む、クライアント Java アプリケーションのデプロイメント全般について詳細に解説した Oracle 公式のドキュメントがあります。日本語化もされています。詳細についてはこちらを参照してもらうとして、ここではつかみの部分を解説することにします。

Java Platform, Standard Editionデプロイメント・ガイド
http://docs.oracle.com/javase/jp/8/docs/technotes/guides/deploy/

javapackager 提供機能の簡単な説明

基本的な使い方

javapackager はコマンドラインツールです。javac や jar コマンドなどと同じディレクトリにインストールされているはずなので、これらコマンドへのパスが通っていれば使うことができます。また、Ant 向けタスクも用意されているのですが、ここではコマンドラインとしての使い方に限定して説明します。Ant での利用方法については上記ドキュメントを参照してください *1

次のような使い方をします。

$ javapackager コマンド [オプション]

javapackager に続けて実行したいタスクに応じたコマンドを指定するのが基本です。コマンド別にオプションがあり、必要に応じて指定します。コマンドは次の 5 種類があります。

コマンド 実行するタスク
-createbss CSS ファイルをバイナリ形式に変換する。
-createjar 実行可能 JAR を生成する。
-deploy デプロイ可能なパッケージを生成する。
-makeall -createjar と -deploy の両方を実行し、実行しているプラットフォームで生成可能な全ての形式のアーカイブを生成する。
-signjar JAR ファイルを署名する。

実行可能 JAR のビルド

まずは基本となる実行可能 JAR のビルドです。通常の jar コマンドが提供する機能に加え、次のような JavaFX 固有の機能を提供しています。

  • プリローダアプリケーションの指定
  • CSS のバイナリ化

1番目にあるプリローダとは、JavaFX アプリケーション本体が起動するまでの間に表示する小さなアプリケーションのことです。主にネットワークからアプリケーションをダウンロードする Java Web StartApplet で使われます。

2番目は CSS ファイルをバイナリ変換するというもので、CSS ファイルが巨大な場合などに、ファイルのパース速度を上げる目的で使用されます *2

-createjar コマンドを使って本機能を利用します。以下にコマンドの実行方法を示します。

$ javapackager -createjar -nocss2bin -appclass アプリケーションクラス名 -srcdir JARに含めるファイルのディレクトリ -outdir JARの出力ディレクトリ -outfile JARファイル名 -preloader プリローダクラス名

-nocss2bin オプションを指定すると、CSS ファイルのバイナリ化が実行されなくなります。未指定の場合だと CSS ファイルのバイナリ化が勝手に実行されてしまうので注意が必要です。 -appclass オプションで指定するのは main メソッドを含むクラスの名前です。マニフェストファイルは勝手に作ってくれますが、自分で追加の属性を指定したい場合は -manifestAttrs オプションに続けて "名前=値,名前=値,..." の形式で指定します。

配布用パッケージの生成

これが javapackager のメイン機能となります。アプリケーション JAR ファイルを準備した状態で -deploy コマンドを使って実行しますが、オプションの指定次第で様々なことができます。

まずは基本的な使い方を示します。以下に示すコマンドを実行すると、Java Web StartApplet 向けに JNLP ファイル及び Applet を実行する HTML ファイルが出力されます。

$ javapackager -deploy -outdir 出力ディレクトリ -outfile 出力ファイル名 -srcdir JARのあるディレクトリ -srcfiles 対象となるJARファイル名 -appclass アプリケーションクラス名 -name アプリケーション名称 -title アプリケーションタイトル

このコマンドを実行すると -name オプションで指定した名前の HTML、JNLP ファイルが出力されます。-title オプションは JNLP ファイル内の <title> タグに反映されます。また、これから説明する自己完結型パッケージの生成でも意味を持ちます。他にもオプションはあるのですが、詳細は javapackager コマンドのリファレンス ( Windows 向けMac、Linux 向け ) を参照してください。

ここでさらに -native オプションを指定すると、自己完結型パッケージを生成することが可能になります。インストール先の OS 向けのネイティブインストーラを生成し、アプリケーション専用の JRE も含めたインストールイメージを作成します。これにより、インストール先の OS に JRE の事前インストールを要求せず、また OS に予めインストールされている public JRE の影響も受けません。これならば「アプリケーションの検証が終わっていないので、JRE のアップグレードはしないでください (> <)」みたいなかっこ悪いことも言わずに済みますね。:)

-native オプションに続けて、次のバンドルタイプを指定することができます。

タイプ 説明
all 無指定の場合はこれが選ばれる。installer、image の両方を指定した場合と同じ結果になる。
installer コマンドを実行している OS が対応している全てのインストーラを生成する。
image アプリケーションインストールディレクトリの内容を展開する。Mac の場合は .app ディレクトリを作り、その下に展開する。
exe Windows の .exe インストーラを生成する。Inno Setup のバージョン 5 以上がインストールされている必要がある。
msi Windows の .msi インストーラを生成する。WiX Toolset のバージョン 3.8 以上がインストールされている必要がある。
dmg MacDMG パッケージ (ドラッグ&ドロップ形式のインストーラ) を生成する。
pkg Mac の PKG インストーラを生成する。
mac.appStore Mac App Store 用のパッケージを生成する。
rpm RPM パッケージを生成する。
deb Debian パッケージを生成する。

指定できるタイプはコマンドを実行する OS で利用可能なパッケージに限定されます。Windows では exe や msi を指定できますが、dmg や pkg、rpmdeb は指定できません。Mac App Store 向けのバンドルタイプもありますね!

-native オプションを使って、自己完結型パッケージを生成する場合、パッケージ固有のオプションを -Bオプション名=値 の形式で指定することができます。ここでは、WindowsMac 向けの代表的なオプションを紹介します。

Windows 向けパッケージ生成オプション

Windows の場合、-native exe もしくは -native msi オプションでインストーラを生成しますが、さらに次のようなオプションを指定することで、カスタマイズを行うことができます。

オプション 説明
-BappVersion=バージョン文字列 アプリケーションのバージョンを指定します。アプリケーションのプロパティで確認できるバージョンになります。
-Bicon=icoファイルのパス アプリケーションのアイコンファイル (.ico ファイル) を指定します。パスは -srcdir で指定するディレクトリからの相対パスになります。
-Bcopyright=コピーライト文字列 アプリケーションのコピーライト文字列を指定します。
-BlicenseFile=ファイルパス exe の場合にのみ有効で、インストーラに使用許諾契約を表示したい場合、そのファイルへのパスを指定します。
-BmenuHint=boolean インストール後、スタートメニューにショートカットを追加したい場合は true を指定します。
-BshortcutHint=boolean インストール後、デスクトップにショートカットを追加したい場合は true を指定します。
-BsystemWide=boolean アプリケーションをユーザローカルにインストールするか、システムレベルにインストールするかを選択します。
-Bwin.menuGroup=グループ名称 スタートメニューにショートカットを追加する場合、スタートメニューにグループを追加した上でその下にショートカットを作ります。そのグループ名を指定します。
-Bvendor=任意文字列 アプリケーションを提供する組織や個人名などを指定します。

結構色んな指定ができることが分かるでしょう。少し重要なのが -BsystemWide オプションです。true にした場合はシステムレベルでインストールされ、いわゆる Program Files ディレクトリ以下にイントールされるようになります。つまり、インストールに管理者権限が必要となります。false の場合はユーザローカルにインストールされます (インストールディレクトリは %LOCALAPPDATA%) 。この場合は管理者権限は不要です。

生成されたインストーラに対して、SignTool を使って署名することもできます。

以下に、実際のコマンド実行例を示します。自分が開発している Social Bookmark Viewer FX をパッケージ化したときのコマンドです。

> javapackager -deploy -native exe -outdir target -outfile SocialBookmarkViewer -srcdir target -srcfiles social-bookmark-viewer-fx-0.1-SNAPSHOT.jar -appclass aoetk.bookmarkviewer.MainApp -name "SocialBookmarkViewer" -title "Social Bookmark Viewer" -BappVersion=0.1 -BsystemWide=true -Bwin.menuGroup="Social Bookmark Viewer"

これを実行すると、次のような出力が得られます。
f:id:aoe-tk:20151219164935p:plain

生成されたインストーラを実行すると、次のようなおなじみのアプリケーションインストーラが立ち上がることになります。
f:id:aoe-tk:20151219165022p:plain

インストールが完了すると、スタートメニューにも登録されました!
f:id:aoe-tk:20151219165035p:plain

インストールディレクトリの下は次のようになっています。exe を実行して起動します。runtime ディレクトリの下には JRE のライブラリが入っています *3
f:id:aoe-tk:20151219165052p:plain

Mac 向けパッケージ生成オプション

Mac の場合は -native dmg-native pkg オプションを使いますが、Windows の場合と異なり、インストーラを生成するために特別なアプリケーションをインストールする必要が無いので、単純に -native だけを指定しちゃっても問題ありません。Mac 向けのカスタマイズオプションを示します。

オプション 説明
-BappVersion=バージョン文字列 アプリケーションのバージョンを指定します。アプリケーションのプロパティで確認できるバージョンになります。
-Bicon=icnsファイルのパス アプリケーションのアイコンファイル (.icns ファイル) を指定します。パスは -srcdir で指定するディレクトリからの相対パスになります。
-Bcopyright=コピーライト文字列 アプリケーションのコピーライト文字列を指定します。
-BlicenseFile=ファイルパス pkg の場合にのみ有効で、インストーラに使用許諾契約を表示したい場合、そのファイルへのパスを指定します。
-BsystemWide=boolean アプリケーションをユーザローカルにインストールするか、システムレベルにインストールするかを選択します。
-Bmac.CFBundleName=名称 アプリケーションメニューバーに表示するアプリケーション名称を -name オプションで指定した名称とは別の名称にしたい場合に指定します。
-Bmac.signing-key-developer-id-app=署名キー名 dmg の場合のオプション。Gatekeeper 向け署名を行いたい場合に指定します。キーがインストールされている場合はデフォルトでそれが使用されるようです。
-Bmac.signing-key-developer-id-installer=署名キー名 pkg の場合のオプション。Gatekeeper 向け署名を行いたい場合に指定します。キーがインストールされている場合はデフォルトでそれが使用されるようです。

Mac の場合、-BsystemWide は true にすると /Applications ディレクトリにインストールされます。Mac の場合はここにアプリケーションをインストールするのが一般的であるため、無指定の場合は true 扱いになります。false にした場合はユーザの Desktop ディレクトリがターゲットになります。

また、Mac 向けにはアプリケーションを "Gatekeeper Ready" にするための対応が入っていることが分かりますね。

Mac 向けのコマンド実行例も同様に示しておきますね。

$ javapackager -deploy -native -outdir target -outfile SocialBookmarkViewer -srcdir target -srcfiles social-bookmark-viewer-fx-0.1-SNAPSHOT.jar -appclass aoetk.bookmarkviewer.MainApp -name "SocialBookmarkViewer" -title "Social Bookmark Viewer" -BappVersion=0.1

これを実行すると、次のような出力が得られます。
f:id:aoe-tk:20151219021419p:plain

生成された DMG ファイルを起動すると、次のようなおなじみのインストール画面が出てきます。
f:id:aoe-tk:20151219021613p:plain

まとめ

このエントリでは javapackager の使い方について掴みの部分の紹介を行いました。最近のクライアントアプリケーション配布を巡る環境の変化に対応した、重要なツールであることを分かってもらえたらと思います。

このツールはクライアントアプリケーションだけでは無く、サーバアプリケーションでも使えると思います。最近はサーバアプリケーションもコンテナにデプロイするのでは無く、単一 JAR に全てをパッケージングしてデプロイする方法も好まれるようになってきました。javapackager を使えば、ランタイムごとパッケージングできるので、より配布が容易になるのではないでしょうか。

javapackager は今回紹介した内容の他にもまだ色々できることがあります (Mac App Store 向けパッケージの作成とか) 。詳しくは上に挙げた Oracle 公式のドキュメント読んでもらえればと。

明日は @ さんの予定です。

*1:というか、この公式ドキュメントでは Ant タスクで利用する方法ばかり説明されています

*2:バイナリ変換すると、ファイルの拡張子が ".css" から ".bss" に変わります。コード側もそれに合わせる必要があります。

*3:昔は JDK を丸ごと放り込むという豪快な感じになっていましたが、最近は結構スリムアップしました。JDK9 の Jigsaw が入るともっと効率よくなるでしょう。