JavaFXの非矩形ウィンドウにおけるWindowsとMacでのマウスイベントの違い

しばらく blog を書いていなかったので先日気がついた小ネタでも書きます。

かなり昔に JavaFX で xeyes のクローンを作ってみた話 を書いたことがあったのですが、最近あれをイチから作り直してみました。コードも GitHub にアップしています。 *1

github.com

以前のブログエントリからは次のようにかなり作りが変わっています。

  • 以前は Mac 上で Swing の EDT を実行すると HeadlessException が飛ぶ問題があり、仕方なく Swing を土台として作っていたのですが、現在はこの問題は解消されているので、JavaFX を土台としたものに変更しています。
    • 唯一、グローバルなマウスカーソルのアドレスを取得するのに AWT に頼っていますが、JavaFX 11 で待望の Robot クラスが追加されるので、11 になれば "Pure JavaFX" なアプリケーションにできる見込みです。
  • マウスカーソルの座標を取得するタイミングを以前は Swing の Timer で起こしていたのですが、これを JavaFXAnimationTimer を利用するように変更し、より滑らかに目玉が動くようになっています。
  • Stage のスタイルを StageStyle.TRANSPARENT に変更し、本家 xeyes のように背景を透過するようにしました。

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

最後のポイントに書いたように、Stage のスタイルを StageStyle.TRANSPARENT に変更したため、ウィンドウのタイトルバーやウィンドウ枠がなくなるため、ウィンドウの移動やリサイズを自力で実装する必要があります。

その際に気が付いたことがありました。リサイズや移動が可能であることをが分かるように、アプリケーション上でのマウスカーソルの位置によってマウスカーソルの形状を変えるようにしました。ところが Windows 上で動かした場合と Mac 上で動かした場合に次のような違いが見られたのです。

  • Mac 上で動かした場合、マウスカーソルが (透明になっている) ウィンドウの端や隅に到達してもちゃんとカーソル形状が変わる。
  • Windows 上で動かした場合、「目」の上でしかマウスカーソルの形状が変更しない。
    • と言うか、マウスイベントが全て「目」の上でしか発生しない。

この動きの違いに悩んで色々調べてみたのですが、どうも JavaFX 側の考え方としては Windows 版の動きが正 のようです。Mac 版の動きについて次のようなバグチケットが作られていました。

https://bugs.openjdk.java.net/browse/JDK-8088104

まあ考えてみれば理屈は分かります。非矩形に作ったのにマウスが矩形に沿って反応するのは確かに不自然です。自分が作ったこの xeyes については Windows 版ではウィンドウの四隅からリサイズが行えないという不便な点がありますが、縦方向と横方向のリサイズは可能なので、まあこれでいっかーとそのままにしましたw

というわけで小ネタでした。

*1:完全に自分用に作ったので IntelliJ 形式のプロジェクトそのままでアップしてます。まあ特殊なライブラリは一切使っていないので、他の IDE やエディタでも普通に扱えるはずです。

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 氏が開発していた Form 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 の魅力をアピールしたりしてました。

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:当時のさくらばさんの怒り様はまあすごかったです (^^;;

Asakusa Direct I/O formatted textの紹介

はじめに

このエントリは Asakusa Framework Advent Calendar 2017 の20日目のエントリです。実は 2 年ぶりくらいにお仕事で Asakusa Framework を使った開発をしているので、今年は参加することにしました。

Asakusa Framework は 6 年前の初期リリースから継続して機能の追加を積極的に行っています。先ほど 2 年ぶりに Asakusa で開発していると書きましたが、その間にも色々進化していて、確実に便利になっていると感じました。

今回はその中の Direct I/O formatted text について紹介したいと思います。これはバージョン 0.9.1 になって追加された、かなり新しい機能です。

Direct I/O は HDFSAWS S3、GCS といった分散ファイルシステム上に保存されたファイルを透過的に読み書きする機能ですが、これまで次のようなファイルの種類に応じてデータモデルへのマッピングを行う機能が用意されていました。

  • Direct I/O CSV
  • Direct I/O TSV
  • Direct I/O line

DirectI/O formatted text はこれまでファイルの種類に応じて別々の API を用いていたところに統一的な機能を提供し、さらに機能追加も行われた強力なライブラリになっています。次のような強みがあります。

  • より多様なデータ形式に対応できる
  • 不整合データに対して柔軟な操作設定ができる
  • 読み込み時にファイルについてのメタ情報を入れられる

詳細についてはドキュメントをじっくり読んでもらうとして、ここでは個人的に嬉しかった点を挙げていきたいと思います。

嬉しかった点その1

「きちんとしていないデータに対してかなり無理が利く」という点です。

例えば、複数のシステム間で連携をするとします。各システムにサービスインターフェースが用意されていればいいですが、そうも行かず、いわゆるファイル連携をすることも多いと思います。

このファイル連携、とにかく泥臭い対応が必要になることが多いですよね。連携先の各システムに「CSV ファイルで連携しましょう」って声を掛けても次のように綺麗なデータが来るとは限らないことが多かったりしますよね。

  • 改行コード、エンコーディングがばらばら
  • 同じデータに対応したものなのにヘッダがあったりなかったり
  • 固定長ファイルを単純に CSV 化したのか、各項目が空白埋めでご丁寧に桁を揃えられているとか
  • 行によってカラム数が異なる CSV 的なものが出てくるとか
    • 本体のデータ部分以外に見出しや合計行なんかも 1 つのファイルに混じっている例を見たことがあります...

Direct I/O formatted text はかなり柔軟な読み込みオプションがあり、このような汚いファイルに対してもかなり無理が利くようになっています。

Direct I/O formatted text を使うためには DMDL のモデル定義に @directio.text.tabular (CSV以外の形式のファイルを扱う場合に使い、区切り文字を指定する) もしくは @directio.text.csv 属性を指定します。

@directio.text.csv(
    charset = "Windows-31J",
    header = skip,
    trim_input = true,
    true_format = "1",
    false_format = "0",
    date_format = "yyyy/MM/dd",
    datetime_format = "yyyy/MM/dd HH:mm:ss",
    on_more_input = report,
    on_less_input = ignore
)
data_model = {

};

属性に指定しているオプションに注目してください。 charset 指定があるのは当然のこととして、どの値を真偽値としてマッピングするか日付時刻のフォーマットも指定可能です。

注目してほしいのは header 指定で、読み込みや書き込みにおいてのヘッダ指定を次のようにかなり柔軟に指定可能です。

指定値 内容
nothing 何もしない (ヘッダ行が無いものとして読み込み、ヘッダを出力しない)
force ヘッダが存在しているものとして読み込み (1行スキップする) 、ヘッダを出力する
skip 読み込み時にヘッダが あれば スキップし、ヘッダは出力しない
auto 読み込み時にヘッダが あれば スキップし、ヘッダを出力する

skipauto といった、ヘッダのあるなしをよしなに扱ってくれます。こんな気配りができるライブラリは割と珍しいと思いませんか。

trim_input オプションは先頭や末尾に空白文字が入っていると除去してくれます。これでわざわざ @Update 演算子を使わなくてもいいですね。

on_more_inputon_less_input なんてオプションもあります。前者は読み込み時にレコードに余計なフィールドがあった場合の動作を、後者はレコードのフィールドが不足している場合の動作を指定できるようになっており、少々変なデータでも対処できるようになっています。この手のライブラリは普通例外を飛ばして終わりということが多いですよね。

on_more_input の場合の指定内容は次の通りです。

指定値 内容
error エラーログを出して異常終了する
report 警告ログを出力した上で無視する
ignore 単に余剰フィールドを無視する

on_less_input の場合の指定内容は次の通りです。

指定値 内容
error エラーログを出して異常終了する
report 警告ログを出力した上で不足フィールドに NULL を入れる
ignore 不足フィールドに NULL を入れる

これで行によってカラム数が異なる CSV 的なもへの対処とかもできそうですよね。

嬉しかった点その2

「読み込み時にファイルのメタ情報をデータモデルに追加できる」という点です。

データフィールド属性に次のような読み込んだファイルに関する情報を埋め込むためのものが用意されています。

属性 内容
@directio.text.file_name 属性を指定されたフィールドにファイルパスを設定する
@directio.text.line_number 属性を指定されたフィールドに当該データのファイルの行番号を設定する (物理的な行番号であるため、途中で改行の入っているレコードが存在した場合、次のレコードは番号がスキップすることになる)
@directio.text.record_number 属性を指定されたフィールドに当該データのファイルのレコード番号を設定する (こちらは論理的な行番号)

でファイルのデータチェックで、ファイル名や行番号の情報も出力したいような場合でも Asakusa 上で実行できるようになりますね。

終わりに

とりあえずはこんなところです。他にも多彩な機能が用意されているので、詳しくはドキュメントを読んでください。

Asakusa Framework は開発の現場で遭遇する「これがあったらいいな」的な機能を結構貪欲に取り込み続けています。取り込んで欲しい機能があったら MLGitHub (日本語でも OK ですよ) などで積極的に提案してみることをお勧めします。

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 日にも書く予定ですが、そちらはもう少し大きなネタにする予定です。

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

JDK9でのjavapackagerについて

はじめに

以前自分の blog にて JDK に付属しているツールである javapackager について紹介したことがあります。このツールは主にクライアントサイド Java アプリケーションを配布可能な形態でパッケージングするためのツールです。ネイティブインストーラも生成することができます。

aoe-tk.hatenablog.com

このエントリではネイティブパッケージに含まれるランタイムについて、次のようなことを述べていました。

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

そして遂に Java9 がリリースされました。JDK9 の javapackager のマニュアル には次のような記載があります。

For self-contained applications, the Java Packager for JDK 9 packages applications with a JDK 9 runtime image generated by the jlink tool.

確かに jlink と連動してランタイムイメージを作ると記載されています。つまり、module-info.java を作っていれば、必要なモジュールだけを含んだランタイムイメージを作成することになり、アプリケーションの配布サイズが小さくなることが期待されます。というわけで早速 JDK9 の javapackager を試してみることにしました。

パッケージ対象となるアプリケーション

まず、パッケージ対象となるアプリケーションを作ります。とてもシンプルな JavaFX アプリケーションとして作ります。

package aoetk.sample;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Text;
import javafx.stage.Stage;

public class SampleApp extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception{
        primaryStage.setTitle("Packager Sample");
        StackPane stackPane = new StackPane();
        stackPane.getChildren().add(new Text("Packager Sample"));
        primaryStage.setScene(new Scene(stackPane, 300, 275));
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

ウィンドウの真ん中にテキストを表示するだけのとてもシンプルな JavaFX アプリケーションです。これを対象にパッケージングを行ってみます。

JDK8でのパッケージング

まず、JDK8 でコンパイルし、パッケージングしてみます。パスを JDK8 に通します。

>set PATH=C:\Program Files\Java\jdk1.8.0_144\bin;%PATH%

>javac -version
javac 1.8.0_144

>where javapackager
C:\Program Files\Java\jdk1.8.0_144\bin\javapackager.exe

JDK8 の javac でコンパイルし、JAR を作ります。

>javac -d bin src\aoetk\sample\SampleApp.java

>javapackager -createjar -nocss2bin -appclass aoetk.sample.SampleApp -srcdir bin -outdir artifact -outfile packager-sample.jar

>dir artifact
 ドライブ C のボリューム ラベルは Windows です
 ボリューム シリアル番号は ACFE-5623 です

 C:\Users\aoe\develop\packager-sample\artifact のディレクトリ

2017/10/08  00:32    <DIR>          .
2017/10/08  00:32    <DIR>          ..
2017/10/08  00:32             1,354 packager-sample.jar
               1 個のファイル               1,354 バイト
               2 個のディレクトリ  366,187,679,744 バイトの空き領域

この JAR に対し、ネイティブインストールイメージを作ります。今回はインストール後のイメージサイズを知りたいので、 -native の引数に image を渡してインストールイメージのみを作ります。

>javapackager -deploy -native image -outdir package -outfile packager-sample -srcdir artifact -srcfiles packager-sample.jar -appclass aoetk.sample.SampleApp -name "jdk8-sample" -title "JDK8Sample" -BappVersion=1.0 -Bwin.menuGroup="JDK8Sample"
アプリケーション・バンドルを作成しています: C:\Users\aoe\develop\packager-sample\package内のjdk8-sample
"モジュール: [java.rmi, java.sql, javafx.web, jdk.charsets, java.logging, java.xml.crypto, java.xml, jdk.xml.dom, jdk.jfr, java.datatransfer, jdk.packager.services, jdk.httpserver, javafx.base, jdk.net, java.desktop, java.naming, javafx.controls, java.prefs, java.security.sasl, jdk.naming.rmi, jdk.zipfs, java.base, jdk.crypto.ec, jdk.management.agent, java.management, java.sql.rowset, javafx.swing, jdk.jsobject, jdk.sctp, java.smartcardio, jdk.unsupported, jdk.jdwp.agent, jdk.scripting.nashorn, java.instrument, java.security.jgss, jdk.management, java.compiler, javafx.graphics, jdk.security.auth, java.scripting, javafx.fxml, jdk.dynalink, javafx.media, jdk.accessibility, java.management.rmi, jdk.naming.dns, jdk.security.jgss, jdk.localedata]をランタイム・イメージに追加しています。"
警告: Windows Defenderが原因でJavaパッケージャが機能しないことがあります。問題が発生した場合は、リアルタイム・モニタリン グを無効にするか、ディレクトリ"C:\Users\aoe\AppData\Local\Temp\"の除外を追加することにより、問題に対処できます。
結果のアプリケーション・バンドル: C:\Users\aoe\develop\packager-sample\package

これで jdk8-sample ディレクトリの下にインストールイメージが作られます。これはインストール後にインストールディレクトリに展開される構成そのものです。

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

ディレクトリサイズは次のように 167MB と、単純なアプリケーションにしては随分大きなサイズになっていることが分かります。

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

JDK9でのパッケージング

では Java9 で導入されたモジュールシステムを利用してパッケージングしてみることにしましょう。環境を JDK9 に変更します。

>set PATH=C:\Program Files\Java\jdk-9\bin;%PATH%

>javac -version
javac 9

>where javapackager
C:\Program Files\Java\jdk-9\bin\javapackager.exe

モジュールの設定を行います。まずはこのアプリケーションがどのモジュールを利用しているかを調べてみましょう。

>jdeps -s artifact\packager-sample.jar
packager-sample.jar -> java.base
packager-sample.jar -> javafx.base
packager-sample.jar -> javafx.graphics

java.base モジュールの他に javafx.base モジュール、 javafx.graphics モジュールに依存していることが分かります。 javafx.graphics モジュールは javafx.base モジュールに依存しているので module-info.java は次のように javafx.graphics モジュールへの依存を記載すれば OK です。

module aoetk.sample.packager {
    requires javafx.graphics;
    exports aoetk.sample;
}

これを JDK9 のコンパイラを使ってコンパイルし、JAR を作ります。

>javac -d bin src\module-info.java src\aoetk\sample\SampleApp.java

>javapackager -createjar -nocss2bin -appclass aoetk.sample.SampleApp -srcdir bin -outdir artifact -outfile packager-sample.jar

>dir artifact
 ドライブ C のボリューム ラベルは Windows です
 ボリューム シリアル番号は ACFE-5623 です

 C:\Users\aoe\develop\packager-sample\artifact のディレクトリ

2017/10/08  00:52    <DIR>          .
2017/10/08  00:52    <DIR>          ..
2017/10/08  00:52             1,642 packager-sample.jar
               1 個のファイル               1,642 バイト
               2 個のディレクトリ  366,004,936,704 バイトの空き領域

これを同じように javapackager を用いてインストールイメージを作るのですが、モジュールを使ったアプリケーションをパッケージングする場合は指定する引数が異なります。ですが、現在の javapackager のマニュアル はこの変更に追いついていません...。執筆時点で javapackager のコマンドラインオプションについて正確な記述があったのは JEP 275: Modular Java Application Packaging のみでした。

具体的には -srcdir-srcfiles-appclass の指定が無くなり、モジュールを使った Java アプリケーションを java コマンドで実行するときと同じようにモジュールパス (対象モジュールの JAR が置かれているディレクトリ) を -p (もしくは --module-path) で、実行クラスを -m (もしくは --module) でモジュール名とクラス名を組み合わせて指定します。

>javapackager -deploy -native image -outdir package -outfile packager-sample -p artifact -m aoetk.sample.packager/aoetk.sample.SampleApp -name "jdk9-sample" -title "JDK9Sample" -BappVersion=1.0 -Bwin.menuGroup="JDK9Sample"
アプリケーション・バンドルを作成しています: C:\Users\aoe\develop\packager-sample\package内のjdk9-sample
モジュールaoetk.sample.packagerは存在しません。
"モジュール: [aoetk.sample.packager]をランタイム・イメージに追加しています。"
モジュールaoetk.sample.packagerは存在しません。
警告: Windows Defenderが原因でJavaパッケージャが機能しないことがあります。問題が発生した場合は、リアルタイム・モニタリン グを無効にするか、ディレクトリ"C:\Users\aoe\AppData\Local\Temp\"の除外を追加することにより、問題に対処できます。
結果のアプリケーション・バンドル: C:\Users\aoe\develop\packager-sample\package

これで jdk9-sample ディレクトリの下にインストールイメージが作られます。

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

ディレクトリサイズを見ると 84.9MB と JDK8 の場合に比べて半分程度になっていることが分かります。確かに Jigsaw の効果が出ていますね!

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

インストールイメージの中身について

なお、インストールイメージの中身を調べてみると、こちらも興味深いものがありました。 アプリケーションや JRE の JAR が見当たらない のです! runtime ディレクトリの下を覗いてみると、何やら modules というそれっぽいファイルがあります。

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

このファイル、JAR か JMOD ファイルかと思いきや、特に ZIP 圧縮されていません。バイナリエディタで覗いてみると時々 CAFEBABE が登場しており、単にクラスファイルを 1 つのファイルにまとめたもののように見受けられます。色々調べてみましたが、このファイルが何であるかの解説を見つけられませんでした。誰か知っている人いますか?

(2017/10/10) 追記

id:MATSUZAKI 様よりコメントで情報を頂きました。jimage 形式のファイルであるとのことです。jimage については Java Magazine の Vol.25 に説明がありました。

jimage形式は、モジュール化されたランタイムに必要なクラスやリソースを管理するコンテナの形式です。 jimageファイルは、従来のようなzipベースの圧縮ではなく、クラスやリソースを高速に検索できるようにインデックスが付けられています。jimageのコンテンツ領域には、そのイメージのすべてのクラスとリソースが含まれており、位置情報にひも付けて管理されています。

JDK には jimage というコマンドがあったので (なお、現時点での JDK9 のドキュメントにはこのコマンドについての説明は見当たらず...) 、このコマンドで中身を閲覧してみました。

>jimage list modules
jimage: modules

Module: aoetk.sample.packager
    META-INF/MANIFEST.MF
    aoetk/sample/SampleApp.class
    module-info.class

Module: java.base
    META-INF/services/java.nio.file.spi.FileSystemProvider
    com/sun/crypto/provider/AESCipher$AES128_CBC_NoPadding.class
    com/sun/crypto/provider/AESCipher$AES128_CFB_NoPadding.class
    com/sun/crypto/provider/AESCipher$AES128_ECB_NoPadding.class
    ...

確かにクラスファイルやリソースファイルが含められていますね。

まとめ

ということで、Project Jigsaw の恩恵で、Java SE 9 からはアプリケーションのインストールイメージをより絞って配布が可能であることが分かりました。OracleOracle Java SEサポート・ロードマップ において、Java アプリケーションの配布は (予め配布先に Java をインストールさせるのではなく) JRE も一緒にバンドルした自己完結型パッケージングでの配布を推奨しています。jlink と javapackager を最大限に活用してきましょう。