JavaFXの非矩形ウィンドウにおけるWindowsとMacでのマウスイベントの違い
しばらく blog を書いていなかったので先日気がついた小ネタでも書きます。
かなり昔に JavaFX で xeyes のクローンを作ってみた話 を書いたことがあったのですが、最近あれをイチから作り直してみました。コードも GitHub にアップしています。 *1
以前のブログエントリからは次のようにかなり作りが変わっています。
- 以前は Mac 上で Swing の EDT を実行すると HeadlessException が飛ぶ問題があり、仕方なく Swing を土台として作っていたのですが、現在はこの問題は解消されているので、JavaFX を土台としたものに変更しています。
- マウスカーソルの座標を取得するタイミングを以前は Swing の Timer で起こしていたのですが、これを JavaFX の AnimationTimer を利用するように変更し、より滑らかに目玉が動くようになっています。
- Stage のスタイルを StageStyle.TRANSPARENT に変更し、本家 xeyes のように背景を透過するようにしました。
最後のポイントに書いたように、Stage のスタイルを StageStyle.TRANSPARENT
に変更したため、ウィンドウのタイトルバーやウィンドウ枠がなくなるため、ウィンドウの移動やリサイズを自力で実装する必要があります。
その際に気が付いたことがありました。リサイズや移動が可能であることをが分かるように、アプリケーション上でのマウスカーソルの位置によってマウスカーソルの形状を変えるようにしました。ところが Windows 上で動かした場合と Mac 上で動かした場合に次のような違いが見られたのです。
- Mac 上で動かした場合、マウスカーソルが (透明になっている) ウィンドウの端や隅に到達してもちゃんとカーソル形状が変わる。
- Windows 上で動かした場合、「目」の上でしかマウスカーソルの形状が変更しない。
- と言うか、マウスイベントが全て「目」の上でしか発生しない。
この動きの違いに悩んで色々調べてみたのですが、どうも JavaFX 側の考え方としては Windows 版の動きが正 のようです。Mac 版の動きについて次のようなバグチケットが作られていました。
https://bugs.openjdk.java.net/browse/JDK-8088104
まあ考えてみれば理屈は分かります。非矩形に作ったのにマウスが矩形に沿って反応するのは確かに不自然です。自分が作ったこの xeyes については Windows 版ではウィンドウの四隅からリサイズが行えないという不便な点がありますが、縦方向と横方向のリサイズは可能なので、まあこれでいっかーとそのままにしましたw
というわけで小ネタでした。
Java Client Roadmap Updateによせて (後編)
というわけで先日アップした次のエントリの後編です。
前回は年寄りの思い出話という感じでしたがまさかの大きな反響を頂いて驚いています。後編については JavaFX や Swing、そしてクロスプラットフォーム GUI の今後について思うところを書いていきたいと思います。
JavaFX は今後どうなる?
今回の決定で JavaFX は JDK リリースから分離されることになったわけですが、逆に言うと JDK のリリースサイクルに縛られること無く開発を進められることになります。そして、私の感覚からすると、当面 JavaFX が廃れるような心配はしなくていいと見ています。
JavaFX は Java EE と同様によりオープンソースコミュニティに今後の開発をゆだねることになりましたが、JavaFX のコミュニティは今でもとても盛り上がっています。OpenJFX の ML では今でも盛んに議論がされていますし、先のホワイトペーパーでも「情熱的なコミュニティ」であると賞賛しています。
実は今回の発表の少し前に OpenJFX のリーダーである Kevin Rushforth 氏から OpenJFX コミュニティの今後の方向性について問いかけがありました。
http://mail.openjdk.java.net/pipermail/openjfx-dev/2018-February/021335.html
この議論では大変な盛り上がりを見せ、より外部の人が入りやすくなるような雰囲気にしようという方向になりました。その一環として GitHub に OpenJFX のリポジトリのミラーも作られ、PR を受け付けられるようにしています (OCA へのサインは必要です) 。
このように、JDK のリリースサイクルに縛られず、より外部も参加しやすくなることで、かえって開発のスピードが上がる可能性もあると考えています。前編でも述べたように海外では結構採用事例があり、Gluon のように JavaFX をビジネスのメインにしている会社もあります *1 。
と言うわけで、Java の標準 GUI では無くなりましたが、優秀なライブラリとしてあり続けるだろうと思っています。前編でも述べましたが、とても開発しやすい API ですし、Java で GUI を作るときの最有力候補であることは変わらないと思っています。コミュニティとの連携も取りやすくなる方向になっていますし、業務で JavaFX を採用したところがあってもそんなに心配しなくてもいいと思います。
私自身はここ数年フロントエンドとは余り縁の無い仕事をしている事もあり、業務で JavaFX を触る機会は無かったのですが、自分の身の回りでで GUI を使った道具が必要な時は JavaFX で作っていました。今後もそのためには JavaFX を使い続けると思っています。
Swing について
紆余曲折を経て、Java のデフォルト GUI は Swing に戻った感があるのですが、前編でも述べたように OpenJDK では再び Swing に力が入りそうな雰囲気になっています。Swing に JavaFX の良いところ (FXML とかバインディングとか) が移植されるような流れにならないかなあ。
私は Swing は一定の成功を収めたと思っています。何より Swing が覇権を握っている分野があります。それは IDE です。NetBeans と JetBrains の IDE だけじゃないかと突っ込まれそうですが、特に JetBrains IDE のここ最近の躍進ぷりはすごいです。JavaScript、Python、PHP、Ruby、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 へのトライアルは色々な形で出てくるでしょうね、というところで雑感垂れ流しのエントリを締めくりりたいと思います。
Java Client Roadmap Updateによせて (前編)
既にご存知の方も多いと思いますが、先日 Oracle から JavaFX をはじめとする、Java のクライアントテクノロジーについて今後のロードマップが発表されました。
上記ブログエントリでは主に JavaFX の今後の扱いについて述べていますが、以下のホワイトペーパーにはそのほかに Applet や Java Web Start、そして Swing/AWT といった Java のクライアントテクノロジー全般の今後のロードマップについて記載されています。
http://www.oracle.com/technetwork/java/javase/javaclientroadmapupdate2018mar-4414431.pdf
このホワイトペーパーを読んで、まあ色々思うことがありました。Java のクライアントテクノロジーについての過去も振り返りながら私が感じたことを色々語りたくなったので、このエントリを書きました。まあ巷で言う「ポエム」ってやつですね。
長くなったので前後編に分けて書くことにしました。
ホワイトペーパーに書かれていたこと
ホワイトペーパーに記載されていた内容を簡単にまとめると次の通りです。
- Oracle Java SE 11 からは JavaFX を同梱しないことを確定
- 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 を同梱したスタントアロンアプリケーションとしての配布に切り替える
- Oracle もホワイトペーパーで推奨している対応です
- 既に JDK にはそのための javapackeger というツールが同梱されている ので、これを活用しましょう
というわけで、このロードマップ発表で Java のクライアントテクノロジーは転換点を迎えることになりました。これまでの経緯をちょっと振り返ってみようかなと思います。
Java クライアントテクノロジーの変遷について振り返り
Java 誕生時の GUI
Java は誕生時から AWT という GUI ツールキットを同梱していました。Java の主な目的の1つに Web ブラウザ上で GUI アプリケーションを動かせるようにすることがあったからです。
AWT は画面に描画する GUI 要素は Windows や Mac といった実行環境のコントロールを使っていました。それに対して抽象化層を提供するものであったため、用意された 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 の追加
- Swing 開発のためのアプリケーションフレームワークプロジェクトとその関連技術の開発が始まる
RIA の潮流と JavaFX の登場
この頃が Swing の全盛期だったかなあと思っています。そして大体同じ頃に別の流れが出てきました。それがコンシューマ向け Web を対象とした RIA の流れです。Microsoft が Silverlight をリリースした辺りから Flash と共に次のような特徴を備えたものとして注目され始めました。
もちろん Java にも Applet や Web Start という同様の技術がありました。Swing も GUI 要素を自分で描画する仕組みになっているため、頑張ればどんな見た目にすることもできます。しかし、デザイナが扱うのには向いていないプラットフォームでした。
そこで Sun が目を付けたのが、Sun が買収した企業である SeeBeyond に所属していた Christopher Oliver 氏が開発していた Form Follows Function (F3) というプロジェクトでした。 これは確か Swing を開発しやすくするための DSL だったと思います。 (さくらばさんより指摘があり、これは正しくないとのことでした。F3 は Flash + ActionScript な DSL を目指したもので、たまたま実行の土台として Swing を使っていたとのことです。) Sun はこの技術をベースに JavaFX というプロジェクトを立ち上げ、次のような特徴を備えたコンシューマ向け GUI 開発、実行環境としてデビューさせます。
- デスクトップ、スマートフォン、TV など様々な機器を対象
- JavaFX Script という GUI 構築のための DSL の採用
- CSS によるスタイリングの分離
- Adobe Illustrator や Photoshop に対してプラグインを提供し、デザイナが 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 は引き続き盛り上げていくことを宣言します。ですが、Oracle は JavaFX をコンシューマ向け開発では無く、あくまで業務向け開発のツールにすることを重視しました。...とは 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 の標準 GUI は JavaFX になると宣言しました。JavaOne で iOS、Android デバイスでの稼働をデモしたりと、Oracle は本気だと私もすごく期待していました。
主に欧州を中心に業務系システムやトレーディングツールなどでの採用例が出てくるようになります。NASA など科学技術研究の現場での採用も多かったように思います。 Eclipse GEF も最新版では JavaFX ベースになっています。
そして Oracle の方向転換
ですが、JavaFX 8 のリリースがピークでした。JavaFX 8 リリースの後辺りから次のように徐々に暗雲が垂れ込んできました
- iOS、Android への搭載の話はトーンダウンし、OpenJFX でソースだけ公開してお茶を濁す
- 肝心の JVM 部分の公開がなかった
- Raspberry Pi での実行にも積極的になったかと思えば、1 年後にはトーンダウン
- Scene Builder も 2.0 リリースを最後にメンテナンスモードに
- 現在は Gluon がメンテナンスを継続 しています
Java 9 では Jigsaw 対応で手一杯で新機能の追加はほとんどなく、明らかにリソースを絞っているように見えました。そして今回の発表につながります。
ホワイトペーパーを見る限り、Oracle は Java のクライアントテクノロジーに対する興味がほとんど無いことが分かります。現在は 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 は HDFS や AWS 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 |
読み込み時にヘッダが あれば スキップし、ヘッダを出力する |
skip
や auto
といった、ヘッダのあるなしをよしなに扱ってくれます。こんな気配りができるライブラリは割と珍しいと思いませんか。
trim_input
オプションは先頭や末尾に空白文字が入っていると除去してくれます。これでわざわざ @Update
演算子を使わなくてもいいですね。
on_more_input
や on_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 は開発の現場で遭遇する「これがあったらいいな」的な機能を結構貪欲に取り込み続けています。取り込んで欲しい機能があったら ML や GitHub (日本語でも OK ですよ) などで積極的に提案してみることをお勧めします。
Bean ValidationのJavaFX対応
このエントリは JavaFX Advent Calendar 2017 の 19 日目のエントリです。前日は id:planet-az さんによる「 簡単なミュージックプレーヤーをさらにいじってみた 」でした。
はじめに
今回取り上げるのは、 JSR 380 Bean Validation 2.0 の JavaFX 対応についてです。そうです、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 を使っているので、バインドを活用してみることにします。
作成するサンプルアプリケーション
作ってみるのは次のようなユーザ登録フォームを想定したものです。
テキストフィールドに文字を打ち込むたびにバリデーションを行い、正しい文字列が入力されたタイミングでエラーメッセージが消えるようになります。E-mail、パスワードどちらも OK になれば登録ボタンがクリック可能になります。
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 アプリのように) ボタンクリックのタイミングで実行する、あるいはバリデーションエラーを受け取ったときは入力をキャンセルしてそもそも入力できない文字は入れられないようにする...などなど色んな実装方法が考えられます。皆さんなりのやり方で色々試してみるといいでしょう。
明日は...まだ埋まっていないな。みんな書いて書いて!
JavaFXアプリケーションでJava 9のモジュールを使うときの注意点
このエントリは JavaFX Advent Calendar 2017 の 1 日目のエントリです。最初を飾るのは初めてです。まだ参加者が少ないので、みんな参加してね!
いよいよJava SE 9がリリースされました。やはり9の注目はJigsawことモジュールシステムですね。特にJavaFXにとってはjavapackagerを用いてパッケージングした際の配布サイズを小さくすることができるので、特に重要ですね。
ですが、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 アプリケーションを配布可能な形態でパッケージングするためのツールです。ネイティブインストーラも生成することができます。
このエントリではネイティブパッケージに含まれるランタイムについて、次のようなことを述べていました。
昔は 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
ディレクトリの下にインストールイメージが作られます。これはインストール後にインストールディレクトリに展開される構成そのものです。
ディレクトリサイズは次のように 167MB と、単純なアプリケーションにしては随分大きなサイズになっていることが分かります。
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
ディレクトリの下にインストールイメージが作られます。
ディレクトリサイズを見ると 84.9MB と JDK8 の場合に比べて半分程度になっていることが分かります。確かに Jigsaw の効果が出ていますね!
インストールイメージの中身について
なお、インストールイメージの中身を調べてみると、こちらも興味深いものがありました。 アプリケーションや JRE の JAR が見当たらない のです! runtime
ディレクトリの下を覗いてみると、何やら modules
というそれっぽいファイルがあります。
このファイル、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 からはアプリケーションのインストールイメージをより絞って配布が可能であることが分かりました。Oracle も Oracle Java SEサポート・ロードマップ において、Java アプリケーションの配布は (予め配布先に Java をインストールさせるのではなく) JRE も一緒にバンドルした自己完結型パッケージングでの配布を推奨しています。jlink と javapackager を最大限に活用してきましょう。