torutkのブログ

ソフトウェア・エンジニアのブログ

Microsoft SQL Server JDBCドライバーにmodule-info定義を追加してモジュール対応

はじめに

Microsoft SQL Server JDBCドライバーのモジュール対応(module-info.classの組み込み)を試みました。経過をメモします。

経緯

SQL Server JDBCドライバーは、自動モジュール対応までしており、モジュールアプリケーションから利用できます。自動モジュールとは、JARファイルにはmodule-info.classが組み込まれておらず、マニフェストファイルのAutomatic-Module-Name属性にモジュール名が定義されたJARファイルです。

しかし、アプリケーションの配布イメージをJLinkで作成する際、自動モジュールを使用しているアプリケーションではエラーとなってしまいます。JLinkは、自動モジュールに対応していないためです。

そこで、後付けでmodule-info.classをSQL Server JDBCドライバーのJARファイルに突っ込み、モジュール対応したものとしてJLinkにかけてみます。

module-info.classの生成とJARファイルへの後付け(試行1回目)

まず、JDBCドライバーのJARファイル mssql-jdbc-7.4.1.jre11.jar の依存関係を抽出します。jdepsコマンドを使いますが、オプション指定にいろいろあり、何が正解か分かりませんので試した結果をメモします。

mssql-jdbc-7.4.1.jre11.jarの依存関係を調べる

--generate-module-info
D:\work> mkdir out
D:\work> jdeps --generate-module-info out mssql-jdbc-7.4.1.jre.jar
Missing dependence: D:\work\out\com.microsoft.sqlserver.jdbc\module-info.java not generated
エラー: 依存性が欠落しています
   com.microsoft.sqlserver.jdbc.KeyVaultCredential    -> com.microsoft.aad.adal4j.AuthenticationCallback    見つかりません
  :
--list-deps
D:\work> jdeps --list-deps mssql-jdbc-7.4.1.jre11.jar
   java.base
   java.logging
   java.naming
   java.security.jgss
   java.sql
   java.transaction.xa
   java.xml
--list-reduced-deps
D:\work> jdeps --list-reduced-deps mssql-jdbc-7.4.1.jre11.jar
   java.base
   java.naming
   java.security.jgss
   java.sql

--list-deps オプションと --list-reduced-deps オプションの差は、後者は推移的な依存関係のあるものはリストされないというもののようです。

module-info.javaを作成

module名は、mssql-jdbc-7.4.1.jre11.jar の自動モジュールで公開しているモジュール名 com.microsoft.sqlserver.jdbc とします。 APIとして公開するパッケージ com.microsoft.sqlserver.jdbc をexports宣言し、このAPIが実行時に最低限必要なモジュールをrequires宣言します。必要なモジュールは、先のjdepsコマンド(--list-reduced-depsオプション)で調べたものを記述します。なお、java.baseは暗黙でrequiresされるので省略します。

module com.microsoft.sqlserver.jdbc {
    exports com.microsoft.sqlserver.jdbc;
    requires java.naming;
    requires java.security.jgss;
    requires java.sql;
}

module-info.java から module-info.classを生成し、jarファイルに追加

D:\work> javac --patch-module com.microsoft.sqlserver.jdbc=mssql-jdbc-7.4.1.jre11.jar module-info.java

module-info.class が生成されます。 このmodule-info.class をJARファイルに追加します。

D:\work> jar uvf mssql-jdbc-7.4.1.jre11.jar module-info.class
module-infoが更新されました: module-info.class

JLink で使用

JDBCを使用するアプリケーションの配布イメージをjlinkで作成します。

エラー: 署名済モジュラJAR D:\work\mssql-jdbc-7.4.1.jre11.jarは現在サポートされていないため、エラーを抑止するには--ignore-signing-informationを使用します

とエラーになったので、jlinkのコマンドにオプション --ignore-signing-information を追加します。

警告: 署名済モジュラJAR D:\toru\Documents\work\job\mcc\Glowlink\spectrum_viewer\.\mssql-jdbc-7.4.1.jre11.jarは現在サポートされていません

とjlinkコマンドがエラーから警告に変わり、配布イメージが生成されました。

配布イメージのモジュールにJDBCが含まれるかを確認

D:\work\myapp\out\runtime> bin\java --list-modules
myapp
com.microsoft.sqlserver.jdbc
java.base@11.0.4
java.datatransfer@11.0.4
  :

一応生成できています。

実行

D:\work\myapp\out\runtime> bin\myapp.bat
java.sql.SQLException: No suitable driver found for jdbc:sqlserver://localhost;databaseName=spectrum;user=foo;password=bar
        at java.sql/java.sql.DriverManager.getConnection(Unknown Source)

JDBCドライバが見つからないとのエラーになってしまいました。

回避策(場当たり的な)

本来は不要ですが、プログラム中でJDBCドライバークラスを明示的にnewして登録すると実行可能となりました。

static {
    try {
        DriverManager.registerDriver(new com.microsoft.sqlserver.jdbc.SQLServerDriver());
    } catch (SQLException ex) {
        logger.log(Level.WARNING, "DriverManager cannot register", ex);
    }
}

実行時エラーの調査

JDBCドライバーが見つからないという実行時エラーの原因を探ってみます。

オリジナルのJDBCドライバーのモジュール情報

D:\libs> jar --describe-module --file mssql-jdbc-7.4.1.jre11.jar
モジュール・ディスクリプタが見つかりません。自動モジュールが導出されました。

com.microsoft.sqlserver.jdbc@7.4.1.jre11 automatic
requires java.base mandated
provides java.sql.Driver with com.microsoft.sqlserver.jdbc.SQLServerDriver
contains com.microsoft.sqlserver.jdbc
contains com.microsoft.sqlserver.jdbc.dataclassification
contains com.microsoft.sqlserver.jdbc.dns
contains com.microsoft.sqlserver.jdbc.osgi
contains microsoft.sql
contains mssql.googlecode.cityhash
contains mssql.googlecode.concurrentlinkedhashmap
contains mssql.security.provider

module-info.classを後付けしたJDBCドライバーのモジュール情報

D:\work> jar --describe-module --file mssql-jdbc-7.4.1.jre11.jar
com.microsoft.sqlserver.jdbc jar:file:///D:/work/mssql-jdbc-7.4.1.jre11.jar/!module-info.class
exports com.microsoft.sqlserver.jdbc
requires java.base mandated
requires java.naming
requires java.security.jgss
requires java.sql
contains com.microsoft.sqlserver.jdbc.dataclassification
contains com.microsoft.sqlserver.jdbc.dns
contains com.microsoft.sqlserver.jdbc.osgi
contains microsoft.sql
contains mssql.googlecode.cityhash
contains mssql.googlecode.concurrentlinkedhashmap
contains mssql.security.provider

モジュール情報の違いを分析

元のmssql-jdbc-7.4.1.jre11.jarと、module-info.classを後付け挿入したmssql-jdbc-7.4.1.jre11.jarとの違いを見ると、今回の実行時エラー(ドライバーが見つからない)に関係しそうな相違点は、provies項です。

provides java.sql.Driver with com.microsoft.sqlserver.jdbc.SQLServerDriver

各種JDBCドライバーは、SPI(Service Provider Interface)で提供されており、モジュール仕様以前のクラスパスではJARファイルの中にMETA-INF/services/java.sql.Driver ファイルを置き、このファイルの中にSPIの実装クラスのFQCNを記載します。 mssql-jdbc-7.4.1.jre11.jar の場合は、このファイルの中に次の記述があります。

com.microsoft.sqlserver.jdbc.SQLServerDriver

このJARファイルがクラスパスにあると、サービスローダーがSPIの実装クラスを認識してロードすることができます。

モジュール仕様では、SPIの実装はモジュール定義(module-info.java)にprovides宣言で記述します。

module-info.classの生成とJARファイルへの後付け(試行2回目)

module-info.javaに、provides宣言を追加し、コンパイルしたmodule-info.classをJARファイルに追加します。

module-info.javaの再記述

module com.microsoft.sqlserver.jdbc {
    exports com.microsoft.sqlserver.jdbc;
    provides java.sql.Driver with com.microsoft.sqlserver.jdbc.SQLServerDriver;
    requires java.naming;
    requires java.security.jgss;
    requires java.sql;
}

module-info.classの再生成

D:\work> javac --patch-module com.microsoft.sqlserver.jdbc=mssql-jdbc-7.4.1.jre11.jar module-info.java

module-info.class を mssql-jdbc-7.4.1.jre11.jar に挿入

D:\work>jar uvf mssql-jdbc-7.4.1.jre11.jar module-info.class
module-infoが更新されました: module-info.class

モジュール情報の確認

D:\work> jar --describe-module --file mssql-jdbc-7.4.1.jre11.jar
com.microsoft.sqlserver.jdbc jar:file:///D:/work/mssql-jdbc-7.4.1.jre11.jar/!module-info.class
exports com.microsoft.sqlserver.jdbc
requires java.base mandated
requires java.naming
requires java.security.jgss
requires java.sql
provides java.sql.Driver with com.microsoft.sqlserver.jdbc.SQLServerDriver
contains com.microsoft.sqlserver.jdbc.dataclassification
contains com.microsoft.sqlserver.jdbc.dns
contains com.microsoft.sqlserver.jdbc.osgi
contains microsoft.sql
contains mssql.googlecode.cityhash
contains mssql.googlecode.concurrentlinkedhashmap
contains mssql.security.provider

provides項が見えています。

JLinkで配布イメージ生成後実行

SQL Serverに接続して無事データベースアプリケーションとして実行できるようになりました。

まとめ

  • Microsoft SQL Server用のJDBCドライバー(ライブラリ)は、自動モジュール(Automatic module)形式のJARファイルとして配布されている(Ver. 7.0以降)
  • 自動モジュールのJARは、モジュール対応アプリケーションからモジュール指定でコンパイル・実行ができる
  • 自動モジュールのJARを使用しているモジュール対応アプリケーションは、JLinkで配布イメージを生成することができない
  • 非モジュール(自動モジュールも含めて)のJARをモジュールJARにするには、module-info.javaを記述してコンパイルした後、JARファイルに追加する
  • module-info.javaを記述するには、jdepsコマンドで依存関係を解析する

データベースを利用するアプリケーション(作成準備編)

動機

先月から折れ線グラフで表示するプログラムをJavaFXで作っています(自宅や出張中の宿で細々とです。出張先では通勤時間とか家事とかがないので捗りました)。

torutk.hatenablog.jp

スペクトラム・アナライザのデータをファイルから読み込む機能を持たせていますが、ファイルの数が増えてくると、ファイルの管理が大変になります。例えば、どの信号を解析したものか、それはいつのものか、解析に使った計測器は何か、計測時の条件はどうだったか、などなどをファイルと関連付けて管理しなくてはなりません。

ディレクトリ名とファイル名を工夫する等で多少は関連付けを表現できます。解析対象、計測器、計測日時などをディレクトリとし、条件をファイル名にする、といった工夫です。

信号種類A \ XWV8765-BA \ 20190818-02-51 \ ATT15_SPAN100M_RBW20K_GX.trace

しかし、単一ツリーのディレクトリ管理ではやがて限界に達してしまいます。特にディレクトリの底の条件、ファイル名に含んだ条件を探すときなどです。

そこで、データベースを使ってこれらのデータを系統立てて保存・管理し、表示したいデータを条件を指定してすぐに取り出せるようにします。

データは、ローカルなネットワーク上に置いて複数台のクライアントから利用可能とします。

どんなデータベースを使うか?

スペクトラム・アナライザのデータ(スペクトラム・アナライザの画面に表示されるある瞬間のデータ)は、1GHzの周波数範囲をRBW 20KHzで取得していた場合、 5万点の点列データとなります。周波数とパワー(X軸とY軸)を1点ごとに1レコードで管理すると1回分で5万レコードになります。1時間に1回計測があるとしたら、年間で1台あたり4億レコード超といった計算になります(10秒に1回だと1兆レコード超え)。

もしリアルタイムにこのレコードを記録し検索するのであれば、時系列データベースなどを使うことを検討します。

今回はスペクトラムデータを、計測条件を指定して取り出し表示するアプリケーションで使うことが主なユースケースとなります。スペクトラムデータの格納はオフラインで別途行うため、書き込みは検討外とします。 そこで、条件を指定してデータを取り出すことに向いていると考えるRDBMSを使うことにします。

ただし、点列データを1点1点レコードに入れるとサイズが増大してしまいますので、データベース設計上の課題とします。解決方針は、1回分の点列データをバイナリデータとして1つのレコードに格納します。 点列データの利用方法は、1回分のデータを一括して取り出すもので、点列データの中の特定の値だけ検索して取りだすことはないためです。(アプリケーションの機能として点列データの一部を切り取ることはありますが、データベースには要求しません)

データベースと開発ツール

RDBMSの選択

今回は、ゼロベースの選定はしません。既にある計測器の中で使われており、またローカルのネットワーク上に既にインストールしているデータベースにMicrosoft SQL Serverがあるので、それを使うこととします。

SQL Serverは、大規模(高スペックなサーバー上で稼働)用のEnterprise Edition、中規模用のStandard Editionがあり、それぞれ有償ライセンス(180日間の試用期間あり)となっています。また、小規模(1つのデータベース領域<10GB)のExpress Editionが無償ライセンスで提供されています。開発・テスト用にはDeveloper Editionが無償で提供されています。

SQL Serverの開発環境

開発用に、Express EditionあるいはDeveloper Editionを開発マシンに入れると便利です。 管理ツールとして、SQL Server Management Studio(SSMS)を合わせて入れます。

データベース設計用にはER図も使えるA5:SQL Mk-2を入れてみます。これはWindows用のみですが、無償で利用可能です。自宅環境なのでこれを使うことにしました。

仕事上であれば、有償のデータベース設計用ツールも視野に入れて、ER/Studio、ERWin、SI Object Browserなども候補に挙げるといいかと思います。

JavaからSQL Serverの利用

SQL Server用のJDBCドライバーがMicrosoftから提供されています。

JavaからRDBMSを使うときのAPI選定

JavaJava SE)からRDBMSを使うときに、APIとしていくつかの選択肢があります。

  • JDBC APIを使う JDBCドライバーを入れて、Java SE標準のJDBC APIを使いDBとデータをやり取りする方法
  • Java Persistent APIJPA)を使う JPA仕様に準拠したサードパーティ・ライブラリ(eclipselink等)を使い、JPAのO/Rマッパー経由でDBとデータをやり取りする方法
  • サードパーティ・ライブラリ独自APIを使う 標準APIではなく、サードパーティ・ライブラリが独自に提供するAPIを使います。いろいろ種類がありますが、おおざっぱに分類すると
    • JDBCをラップする比較的小さなライブラリ(iciql、jooq、speedment等)を使う
    • 独自のマッパーを提供する比較的大きなライブラリ(MyBatis、DOMA等)を使う

今回のAPI選定基準

次の観点を選定基準にします。

  • Java SE 以外のサードパーティ・ライブラリへの依存を少なくしたい
  • アプリケーションのソースコード構成を簡潔に保ちたいので自動生成のソースや設定ファイルは極力無にしたい
  • RDBMSでのテーブル(スキーマ)設計、SQLの書き方をこの機に勉強したい

ということで、JDBC APIを使う方法とします。

RDBMSの勉強を上げたのは、RDBMSアプリ開発での利用経験が少ないからです。 これまでのソフトウェア開発経験で使ったデータベースは、Microsoft AccessのJETエンジン、ObjectStore DB(OODBMS)です。OracleSQLiteMySQL、H2 Database Engine(+JPA)は開発インフラとして整備、チューニングに携わりましたが、これらDBをアプリから使う開発はあまりしていません。 ソフトウェア開発とは別に、Redmineの環境構築・運用管理でMySQLを扱っているので、RDBMSの素養はRedmineの管理経験に拠るところが大きいです。

JDBCドライバーを使ってDBアクセス

Java SEのJDBC APIでは、まず使用するデータベース用のJDBCドライバーを入れる必要があります。

SQL Server用のJDBCドライバーの入手

Microsoft SQL Serverについては、MicrosoftからType 4 JDBCドライバーが提供されています。

Microsoft SQL Server 用 JDBC Driver のダウンロード - SQL Server | Microsoft Docs

2019-08-18時点では、最新バージョン7.4.1がリリースされています。これは、

となっています。

これをダウンロードし、適切なディレクトリに展開します(リリースノートではC:\Program Files下を推奨)。今回は、@C:\Program Files\Java\sqljdbc_7.4@に展開しました。 JDK 8用、JDK 11用、JDK 12用のJARファイルが存在します。

mssql-jdbc-7.4.1.jre8.jar
mssql-jdbc-7.4.1.jre11.jar
mssql-jdbc-7.4.1.jre12.jar

JDK 11で使用する場合

Javaコンパイル、実行時にモジュールパスにJDBCドライバーのJARファイルを指定します。

C:\work> javac -p "C:\Program Files\Java\sqljdbc_7.4\mssql-jdbc-7.4.1.jre11.jar;out\myapp" ...

JDBC APIを使ったプログラミング

JDBC 4.0(Java SE 6以降)では、JDBCドライバーがクラスパス上にあれば次の2つの方法で接続します。

  • DriverManager を使う
  • DataSource を使う

JDBCのドキュメントによると、DriverManagerよりもDataSourceが推奨されています。ソースコードにデータベース接続設定(ホスト、ユーザー、パスワード等)を記述するDriverManagerよりも、JNDIで接続する外部(ディレクトリサーバー等)に設定を置くDataSourceがよいということです。アプリケーションサーバー上で実行する場合は簡単に利用可能です。

しかし、Java SE 環境では、 JNDI経由でDataSourceを使うのは敷居が高いので、DriverManagerを使う方法で実装します。

JavaFXでGUI全体のフォントをCSSで指定

はじめに

前回のブログの続きです。 torutk.hatenablog.jp

画面の国際化対応で日本語プロパティを追加してみました。その際使用される日本語のフォントはJavaのコードやCSSでコントロール毎に指定可能ですが、全体を変更することも可能です。

CSSのrootセレクタに-fx-font-familyでフォントを指定します。 ここで、どのようなフォントファミリ名を指定できるか、OpenJDK 11で試してみました。

CSSの記述例

JavaFXCSSファイルを作成、.rootセレクタに-fx-font-familyでフォント名を指定します。

.root {
    -fx-font-family: "Meiryo";
}

当初いろいろググってみたところ、「外部フォント」の指定には、@font-faceでフォントファイルのURLを記述、なおTTFファイルは有効でTTCファイルは指定できないといった記述をみかけました。 しかし、Windows OS上でOpenJDK 11をにおいては、@font-faceを指定しなくてもWindows OSのフォントとしてインストールされているフォントの指定が可能でした。

動作確認(画面キャプチャ)環境

項目 内容
OS Windows 10 64bit (1903)
JDK Scene Builder 11.0内蔵のOpenJDK 11

Scene Builderで画面を作成している場合、Scene Builderの[プレビュー]メニュー > [Sceneスタイルシート] > [スタイルシートの追加] でCSSファイルを指定します。 すると、Scene Builder上のデザイン画面、およびプレビュー画面は指定したCSSファイルの設定が反映されます。 また、CSSファイルを変更すると自動で変更を反映してくれます。ですので、CSSでの見栄え設定であれば、アプリケーションを再ビルド・実行することなくScene Builder上で確認できるのでとっても楽に確認ができました。

CSSファイルでのフォント指定とその画面

では、実際にCSSファイルのrootセレクタにフォントファミリを指定して表示を確認していきます。

まずはデフォルト(指定なし)の日本語フォント

まずは、何もフォントファミリを指定しなかった場合の日本語フォントです。最初は、OpenJDKのfontconfig.propertiesで定義されるMSゴシックが使われると思っていました。しかし実行してみるとアンチエイリアスが効いたフォントが表示されました。

f:id:torutk:20190728103937p:plain

デバッグプリントでコントロール(Label)のフォントを調べてみると、次のようになっていました。

Font[name=System Regular, family=System, style=Regular, size=12.0]

これは、javafx.scene.text.FontクラスのgetDefaultメソッドが返すフォントで、JavaFXAPIドキュメント(JavaFX 8 日本語APIドキュメント)には次の記述があります。

デフォルト・フォントのファミリはSystem、スタイルは通常はRegularとなり、サイズについては、判断可能な範囲においてユーザーのデスクトップ環境と一致するものが取得されます。

Windows 10 日本語版では、デスクトップのシステムフォントが「游ゴシック」となっているので、デフォルトで表示されるフォントが游ゴシックとなっています。 Windows 7 日本語版であれば、おそらくMSゴシックではないかと思われます。

フォントファミリにMS Gothicを指定

CSSファイルに以下を記述します。

.root {
    -fx-font-family: "MS Gothic";
}

表示は次の様になります。お馴染みのギザギザフォントですね。

f:id:torutk:20190728104948p:plain

フォントファミリ名は、英語で記述しました。日本語で「MS ゴシック」等と記述した場合は設定が反映されず、デフォルトフォントで表示されました。

フォントファミリにMeiryoを指定

.root {
    -fx-font-family: "Meiryo";
}

f:id:torutk:20190728105456p:plain

フォントファミリにMeiryo UIを指定

.root {
    -fx-font-family: "Meiryo UI";
}

f:id:torutk:20190728105532p:plain

フォントファミリにYu Minchoを指定

.root {
    -fx-font-family: "Yu Mincho";
}

f:id:torutk:20190728105609p:plain

フォントファミリにYu Mincho Demiboldを指定

.root {
    -fx-font-family: "Yu Mincho Demibold";
}

f:id:torutk:20190728105653p:plain

フォントファミリにHGMinchoEを指定(HG明朝E)

.root {
    -fx-font-family: "HGMinchoE";
}

f:id:torutk:20190728105733p:plain

JavaFXのグラフの凡例部分を操作可能にする

はじめに

先週書いた次のブログの続編です。 torutk.hatenablog.jp

一つのグラフ表示領域に、複数のグラフ(Series)を重畳表示した際に、簡単な操作でそれぞれのグラフを非表示にしたり表示したりとできると使い勝手が向上します。

グラフに凡例を表示した場合、それぞれの凡例をクリックするとその凡例に対応するグラフを表示する、非表示すると切り替えられるようにします。

課題

凡例全体のNodeは、Legendクラス(com.sun.javafx.chart.Legend)で実装されています。しかし、このcom.sun.javafx.chartパッケージは、アプリケーション側のモジュールには公開されていません。

ぐぐって検索した情報は、子ノードを辿ってLegendクラスかどうか判別してLegendを見つけ、Legendにキャストしてそのitemを取得して利用するもので、Java SE 9以降では使えません(--add-exportsオプション指定でコンパイルすれば可能)。

各凡例を表示するNodeを取得する方法

それぞれの凡例は、CSSセレクタ .chart-legend-item が指定されています。JavaFXAPIには、セレクタを指定してNodeを取得する機能(lookupおよびlookupAll)が提供されているので、これを使って凡例のNodeを取得します。

@FXML
LineChart<Float, Float> spectrumChart;
:
Set<Node> legendItems = spectrumChart.lookupAll(".chart-legend-item");

それぞれの凡例にマウスハンドラーを設定するには例えば次のようにします。

legendItems.forEach(item -> item.setOnMouseClicked(event -> System.out.println(event + " clicked")));

各凡例をクリックしたら対応するグラフを表示/非表示する

クリックされた凡例のNodeにマウスハンドラーを設定することはできましたが、次はそこから対応するグラフを取得して、その表示を切り替える方法を考えなくてはなりません。

それぞれの凡例はLabelクラスのインスタンスで、ラベルに表示されるテキストはグラフ(Series)のnameプロパティとなっています。

そこで、取得したそれぞれの凡例をLabel型にキャストしてラベルに表示されるテキストを取得し(getTextメソッド)、次にそのテキストを名前として持つSeriesをLineChartから探して取得します。

Labelにキャストしてテキストを取り出す

legendItems.forEach(item -> item.setOnMouseClicked(event -> {
    if (item instanceof Label) {
        String name = ((Label) item).getText();
    }
}));

Label型へのキャストが可能な場合に、Labelへキャストし、getTextメソッドでラベルに表示されるテキストを取得します。

チャート(LineChart)に登録されているSeriesからテキストと同じ名前のものを取り出す

legendItems.forEach(item -> item.setOnMouseClicked(event -> {
    if (item instanceof Label) {
        String name = ((Label) item).getText();
        Optional<XYChart.Series<Float, Float>> any = spectrumChart.getData().stream()
                .filter(series -> series.getName().equals(name))
                .findAny();
    }
}));

chartのdataプロパティには表示するグラフ(Series)がリストで登録されています。これを順次アクセスしSeriesのnameプロパティが凡例ラベルのテキストと一致するものを取り出します。

クリックした凡例に一致するグラフ(Series)の表示/非表示を切り替える

legendItems.forEach(item -> item.setOnMouseClicked(event -> {
    if (item instanceof Label) {
        String name = ((Label) item).getText();
        Optional<XYChart.Series<Float, Float>> any = spectrumChart.getData().stream()
                .filter(series -> series.getName().equals(name))
                .findAny();
        any.ifPresent(series -> {
            boolean currentVisible = series.getNode().isVisible();
            series.getNode().setVisible(!currentVisible);
        });
    }
}));

取り出したグラフ(Series)のgetNodeメソッドで画面に表示されるグラフのNodeを取得し、その表示状態をisVisibleで取得します。そして、その真偽を反転させてsetVisibleに指定し、表示/非表示を反転させます。

グラフが非表示のときは凡例の表示を薄くしたい

実現方法を悩みましたが、CSSの疑似クラスを使って非表示の際に薄く(透過度を変えて)表示させることにしました。

まず、CSSで、.labelに疑似クラス haze を記述します。

.label:haze {
    -fx-opacity: 0.2;
}

次に、凡例ラベルのマウスハンドラー内でグラフ(Series)の表示/非表示切り替えに合わせて疑似クラスの有効・無効を凡例ラベルに設定します。

private static final PseudoClass HAZE_PSEUDO_CLASS = PseudoClass.getPseudoClass("haze");
    :
legendItems.forEach(item -> item.setOnMouseClicked(event -> {
    if (item instanceof Label) {
        String name = ((Label) item).getText();
        Optional<XYChart.Series<Float, Float>> any = spectrumChart.getData().stream()
                .filter(series -> series.getName().equals(name))
                .findAny();
        any.ifPresent(series -> {
            boolean toBeInvisible = series.getNode().isVisible();
            series.getNode().setVisible(!toBeInvisible);
            item.pseudoClassStateChanged(HAZE_PSEUDO_CLASS, toBeInvisible);
        });
    }
}));

JavaFXで折れ線グラフを出そうとしてみた

はじめに(お題)

電気信号を時間領域から周波数領域に変換し、周波数に対するエネルギー量を解析できる測定器にスペクトラム・アナライザがあります。今回、このスペクトラム・アナライザで解析した結果をファイルに保存し、別な場所にあるPC上でその結果を表示、分析したいとします。

計測器にCSVファイルで結果を保存する機能があればこれをExcelで読み込ませてグラフ化することは容易ですが、今回の計測器はバイナリデータのファイルに保存するものでした。そこで、JavaFXで保存したバイナリデータのファイルを読み込み折れ線グラフ(LineChart)で表示するプログラムを作成してみます。

LineChart

JavaFXのプロジェクトを作成し、折れ線グラフ(LineChart)を表示する画面をScene Builderで作成しました。 ファイル構成は次です。

ファイル名 内容
SimpleSpectrumApp.java Application派生クラス、プログラムのエントリ(main)
SimpleSpectrumView.fxml 画面(ビュー)のレイアウトを定義するFXMLファイル
SimpleSpectrumView.css 画面(ビュー)の見栄えを定義するCSSファイル
SimpleSpectrumViewController.java 画面(ビュー)に対応するコントローラークラス
SimpleSpectrumViewModel.java GUI内のモデルクラス

デフォルトのLineChart

Scene BuilderでLineChartを画面にポトペタすると次の表示となります。 f:id:torutk:20190706211210p:plain

縦軸は数値メモリとなっていますが、横軸は表示されていません。Scene Builderの左下ペインを見ると、LineChartの子ノードとしてNumberAxisとCategoryAxisが1つずつ置かれています。縦軸がNumberAxisで、横軸がCategoryAxisです。NumberAxisは名前の通り数値の軸を表し、CategoryAxisは何らかの分類(県名、年別、チーム別、など)の軸を表します。

Scene Builder上では、LineChartの子ノードの軸を変更する方法が見つからなかったので、FXMLファイルを直接エディタで開き修正しました。

      <LineChart BorderPane.alignment="CENTER">
        <xAxis>
-         <CategoryAxis side="BOTTOM" />
+         <NumberAxis side="BOTTOM" />
        </xAxis>
        <yAxis>
          <NumberAxis side="LEFT" />
        </yAxis>
      </LineChart>

横軸がNumberAxisに変更されました。 f:id:torutk:20190706212808p:plain

最初のコード

SimpleSpectrumApp.java
package sample;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class SimpleSpectrumApp extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception {
        Parent root = FXMLLoader.load(getClass().getResource("SimpleSpectrumView.fxml"));
        primaryStage.setTitle("Simple Spectrum");
        primaryStage.setScene(new Scene(root));
        primaryStage.show();
    }

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

JavaFXの典型的なApplication派生クラスの実装です。mainメソッドでプロセスのエントリを用意し、startメソッドでFXMLファイルを読み込みシーングラフを作成、Stageを表示させています。

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

<?import javafx.scene.chart.LineChart?>
<?import javafx.scene.chart.NumberAxis?>
<?import javafx.scene.layout.BorderPane?>

<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.SimpleSpectrumViewController">
   <center>
      <LineChart fx:id="chart" BorderPane.alignment="CENTER">
        <xAxis>
          <NumberAxis side="BOTTOM" fx:id="xAxis" />
        </xAxis>
        <yAxis>
          <NumberAxis fx:id="yAxis" side="LEFT" />
        </yAxis>
      </LineChart>
   </center>
</BorderPane>

Scene Builderでポトペタ作成したFXMLファイルで、ビュー(画面)を定義しています。 コントローラークラスを指定しているので、このFXMLがロードされると自動でコントローラークラスのインスタンスが生成されます。

SimpleSpectrumViewController.java
package sample;

import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;

import java.net.URL;
import java.util.ResourceBundle;

public class SimpleSpectrumViewController implements Initializable {
    @FXML
    LineChart chart;
    @FXML
    NumberAxis xAxis;
    @FXML
    NumberAxis yAxis;

    SimpleSpectrumViewModel model = SimpleSpectrumViewModel.ofRandom();

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        chart.getData().setAll(model.getSeriesList());
    }
}

FXMLに対応するコントローラークラスです。 コントローラーから操作するビュー部品(コントロール)は、@FXMLアノテーションを指定し、FXML側で指定したfx:idの識別子と同じ名前の変数名とすることで、FXMLロード時にインスタンスがインジェクションされます。

データについては、今回はデータファイルの読み込み部分が未実装なので、ランダムな値を表示するモデルを使います。

SimpleSpectrumViewModel.java
package sample;

import javafx.scene.chart.XYChart;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;

public class SimpleSpectrumViewModel {

    private List<XYChart.Series<Double, Double>> seriesList = new ArrayList<>();

    public List<XYChart.Series<Double, Double>> getSeriesList() {
        return Collections.unmodifiableList(seriesList);
    }

    public static SimpleSpectrumViewModel ofRandom() {
        var model = new SimpleSpectrumViewModel();
        model.seriesList.add(createRandomSeries("Random"));
        return model;
    }

    private static XYChart.Series<Double, Double> createRandomSeries(String name) {
        XYChart.Series<Double, Double> series = new XYChart.Series<>();
        series.setName(name);
        for (double x = 950d; x <= 1450d; x++) {
            series.getData().add(new XYChart.Data<Double, Double>(
                    x, ThreadLocalRandom.current().nextGaussian() * 5.0 - 60.0));
        }
        return series;
    }
    
}

JavaFXではモデルクラスについては特にフレームワークが用意されていませんが、コントローラーを軽くするために、ちょっとしたロジックであってもモデルクラスを設けるようにしています。

表示確認のために、データファイルを読み込む前にランダムなスペクトルを表示するモデルを作りました。

JavaFXの折れ線グラフ(LineChart)に表示する1本の連続する線は、データ系列(XYChart.Series)で定義し、複数のデータ系列を表示することができます。 データ系列は、複数の点データ(XYChart.Data)のリストとして作られています。 今回は、1つのデータ系列を用意しています。 用意するデータは、周波数軸(X軸)の950~1450までの間を1刻みで作成し、縦軸の電力は正規分布乱数で平均-60.0、分散5.0としています。

表示

プログラムを実行すると、次の画面が表示されました。

f:id:torutk:20190707043824p:plain

いろいろ改善したい点があります。

  • 縦軸、横軸に名称・単位を表示したい
  • 〇の表示(シンボル)は不要としたい
  • 折れ線を細くしたい
  • 縦軸、横軸の表示範囲を指定したい

LineChartの設定

縦軸、横軸に名称を表示

これは、Scene Builderで、軸のLabelプロパティに文字列を設定すれば表示できます。 f:id:torutk:20190707050535p:plain

縦軸、横軸の表示範囲を指定

縦軸は、-30.0~-80.0の範囲を表示し、横軸は950~1450の範囲を表示します。 表示すべきデータを読み込んだ際にデータに応じた値を設定するので、コードで制御します。

前回からのコード変更部分を次に記載します。

    @Override
    public void initialize(URL location, ResourceBundle resources) {
+       xAxis.setLowerBound(950d);
+       xAxis.setUpperBound(1450d);
+       yAxis.setLowerBound(-80d);
+       yAxis.setUpperBound(-30d);
        chart.getData().setAll(model.getSeriesList());
    }

NumberAxisには、表示する軸の値の上限値・下限値を指定するlowerBound、upperBoundがあります。 ここに表示する軸の数値範囲を指定します。この上下限値設定は、LineChartにデータ(XYChart.Series)を登録する前に実行する必要があります。あとから実行したら表示には反映されません。

ただし、これだけでは意図したとおりの動作をしません。NumberAxisのAutoRangingプロパティが有効だと、登録したデータの値に応じた軸の数値範囲に自動調整が働いてしまいます。今回はScene Builder上でAutoRangingプロパティを無効にします(次の図参照)。

f:id:torutk:20190707101033p:plain

プログラムを実行すると、次の様に意図した数値範囲でグラフが表示されました。

f:id:torutk:20190707101215p:plain

  • グラフの罫線間隔については、NumberAxisのTickUnitプロパティで制御可能です。
〇の表示(シンボル)をなくす

デフォルトでは、折れ線グラフの点データに丸いシンボルが表示されます。 データ数が少ないときは、データとデータを補完する線とを明確に表現するこの表示が適していますが、データ数が多いときは上図のようにシンボルだらけで肝心の折れ線が見えなくなってしまいます。

シンボルをなくすのであれば、LineChartのCreateSymbolプロパティを無効にします(次の図参照)。

f:id:torutk:20190707103825p:plain

プログラムを実行すると、次のとおり丸いシンボルが表示されなくなりました。

  • シンボルを消すのではなく、シンボルの形状を変更したいときは、CSSセレクターに.chart-line-symbolを指定し見栄えを記述します。

f:id:torutk:20190707104015p:plain

折れ線を細く

デフォルトでは、折れ線の幅がちょっと太いので、これを細くします。形状の変更はCSSで記述します。 セレクターに.chart-series-lineを指定し見栄えを記述します。

.chart-series-line {
    -fx-stroke-width: 0.75px;
}

CSSファイルは、アプリケーション派生クラスでSceneに設定するか、FXMLで最上位要素に指定するかの方法があります。今回は、FXML(Scene Builder)上で最上位要素のBorderPaneにCSSファイルを設定します。CSSファイルはFXMLファイルと同じ場所に置き、BorderPaneのStylesheetsプロパティに指定します(次の図参照)。

f:id:torutk:20190707105138p:plain

プログラムを実行すると、次のとおり折れ線が細くなりました。

f:id:torutk:20190707105538p:plain

凡例の記号を四角ではなく線にする

CSSファイルで凡例の記号を定義している@.chart-legend-item-symbol@の定義を変更します。

.chart-legend-item-symbol {
    -fx-background-radius: 0;
    -fx-background-insets: 0;
    -fx-shape: "M0,5 L0,7 L12,7 L12,5 Z";
    -fx-scale-shape: false;
}
.default-color0.chart-legend-item-symbol {
    -fx-background-color: CHART_COLOR_1;
}
.default-color1.chart-legend-item-symbol {
    -fx-background-color: CHART_COLOR_2;
}
.default-color2.chart-legend-item-symbol {
    -fx-background-color: CHART_COLOR_3;
}
.default-color3.chart-legend-item-symbol {
    -fx-background-color: CHART_COLOR_4;
}
.default-color4.chart-legend-item-symbol {
    -fx-background-color: CHART_COLOR_5;
}
.default-color5.chart-legend-item-symbol {
    -fx-background-color: CHART_COLOR_6;
}
.default-color6.chart-legend-item-symbol {
    -fx-background-color: CHART_COLOR_7;
}
.default-color7.chart-legend-item-symbol {
    -fx-background-color: CHART_COLOR_8;
}
  • デフォルトの凡例シンボルは枠が系列色、中が白色塗りつぶしの円です。@-fx-background-radius@と@-fx-background-insets@はデフォルトの定義を無効化するのに指定する模様です(細部よくわからない)
  • グラフはデフォルトで8つまで色分けできるようにCHART_COLOR_1からCHART_COLOR_8まで色が定義されています。svg-pathで定義した枠の中をグラフの対応する色で塗りつぶすために、@.default-colorX.chart-legend-item-symbol@を定義しています。

インターネットとつながらないソフトウェア開発環境で使うツール

前提

ソフトウェア開発環境がインターネットと隔離されていることは、昨今のセキュリティ事情からよくあることと思います*1

ソフトウェアエンジニアの場合、事務作業で使うPC環境とは別にソフトウェア開発用のPC環境があり、それは事務作業で使うネットワーク環境とは異なるソフトウェア開発のネットワーク環境につながっており、インターネットとはつながらない(あるいは強度な制約のもとインターネットにつながる)環境となります。

例えば、事務作業用のPCと開発用のPCが物理的に分かれていて、それぞれ別々なネットワークに接続されていることや、事務作業用PCの利用は仮想マシンを通して行い、開発用PCと仮想マシン間のデータ交換は不可となっていることなどです。

開発したソフトウェアを組み込む製品と開発環境をつないで、ソフトウェアのインストール・更新、ログの取得、デバッグ、各種設定をする際は、製品が開発用ネットワークとはつながっていないため、開発用PCを開発用ネットワークから切り離して使うこともあります。場合によっては開発用ネットワークなしに単独で最初から最後まで開発を行うこともあります。

このように開発環境がインターネットとは接続されていない場合は、開発に使うツールの選定時点からそのことを考慮に入れておくことが不可欠です。

開発環境で使うツールの準備

インターネットと隔離されているので、開発環境で使うツールは開発環境用のネットワークの中で展開できるようにする必要があります。

ここで困るのが、Webインストーラーのみ提供され、インターネットとつながっていないとインストールができないツールです*2

開発用PCをインターネットにつないでインストールしてから開発環境用ネットワークに持ち込めばいいのでは?との声があるかもしれませんが、開発データを抱えたPCをインターネットに接続することは原則不可でしょう。

インストールはできるもののライセンス認証にインターネット接続が必須となるツールも使うことができません。

ツール本体はオフラインでインストールできても、プラグインの追加がインターネット接続必須となると困ります。

インストールができたとしても、実行時に問題があるというケースも困ります*3

主要なJava開発ツール(統合開発環境)のNetBeansEclipseIntelliJ IDEAは、いずれもインストールイメージをオフラインで展開して使用できます。

ビルドツール

maven、gradleはJavaのビルドツールとして非常に流行っておりますが、インターネットに接続していることをほぼ前提としているツールなので、インターネットと隔離した開発環境での使用には不適です。

mavenは、自身の実行にもインターネットからモジュールのダウンロードが発生します。オフラインオプションもありますが、一度オンラインでダウンロードしたキャッシュを使うものなので、ゼロから使うには、mavenセントラルリポジトリのクローンをNexusArtifactoryなどで開発ネットワーク内に構築し、各開発PCからはそのクローンを参照する必要があります*4。ただし、開発環境から切り離しての作業には制限があります*5

gradleは、mavenに比べるとオフライン使用がまだ容易ですが、オフラインで使用できるようにするための手間はやはり発生します。以前、Android Studioで開発した際は、SDKのインストール場所、ライブラリの配置などを調整する必要がありました。その時の取り組みは次のブログに記載しています。

現時点では、引き続きAntを使うのがインターネット隔離環境では適すると言えます。

mavenセントラルリポジトリのライブラリ利用におけるセキュリティリスク

mavenリポジトリを利用する場合、利用するライブラリが依存するライブラリ群を逐一把握して揃えなくてもビルド時に自動で取得してくれる利便性があります。しかし、これは逆に意図しないライブラリが入り込むことになります。意図しないものに、知財的な問題からバックドア等のセキュリティ脆弱性問題までいろいろな問題が含まれます。

開発者が使用するPC個別に直接mavenリポジトリへアクセスしライブラリをダウンロードしてくるので、マルウェア等が入り込み別サイトに誘導されダウンロードしてしまうということも考えられます。

バージョン管理ツール

開発用PCが開発ネットワークに接続して使用、あるいは切り離して使用するので、リポジトリサーバーへの常時接続がなくても管理できる分散リポジトリ方式のGitやMercurialがよいでしょう。特に製品組み込み後にその場で修正をした場合は、開発ネットワークから切り離されている間にリポジトリへのコミットを重ねておき、もどってから中央リポジトリに反映することができるのは大きな利点です。

CIツール

計画的な(デイリーの)ビルド・成果物作成はJenkinsなどのサーバーを使いますが、製品と隣り合わせでビルド・インストールを繰り返すときには持ち出しできる(開発PCに一緒に乗せて使える)CIツールがあるといいですね。これは今後調査していきたいと思っています。

不具合管理・プロジェクト管理ツール

プロジェクトの不具合状況、進捗状況は、ソフトウェア開発メンバーだけでなく、組織の管理者層や品質保証部門からも参照したい情報です。ソフトウェア開発環境の中に閉じたツールを立てると、開発メンバーしか共有できません。
隔離したネットワークの双方から参照したい場合の実現方法は今後調査していきたいと思っています。

コミュニケーションツール

開発環境とは別に、事務作業用の環境で使うか、開発環境の中に独自にツールを用意するかの選択(あるいは併用)となります。
ビルド結果の通知、プロジェクト管理ツールからの通知などをコミュニケーションツールで受けるので開発環境の中に置きたいところですが、開発以外のメンバーとのコミュニケーションには事務作業用の環境で使えるツールが必要です。インターネット接続環境ではチャットツールの活用でコミュニケーションが良好になっています。隔離したネットワーク内およびその外側とでコミュニケーションをとる方法を今後調査したいところです。

現状と今後

前提で書いた開発環境は、かなり制約がつよい(利便性が犠牲になっている)環境となっています。プログラマー視点で普段目にする技術情報では、便利な技術の紹介がほとんどで、インターネット上のサービスをガンガン使うものばかりですが、所属する会社組織において要求されるセキュリティレベルによってはそうした便利技術が使えないということが私の経験では多いです。

前提のような環境を強いられなくてもセキュリティが保てるような開発環境をどうしたら構築できるかについて、少しずつ検討していきたいと考えています。

インターネット接続できない場所が意外と

先日出張の際、建物の中のある会議室では携帯キャリアによってつながる/つながらないが分かれました。
また、出張の合間に車で移動する際、キャリアすべてがつながらない場所が少なからずありました。

*1:開発用PCがインターネットとつながってメールやWebができると、開発環境にある情報が漏洩するリスクがあり、入口対策でファイアウォールやウィルス対策をしていても、標的型攻撃や開発ツール・ライブラリ等に仕込まれたサプライチェーン攻撃等による漏洩は防げません。開発環境に攻撃が及んでも社外に漏洩しないよう隔離をして対策を取るケースです。

*2:Visual Studio 2017以降はISOイメージが提供されなくなり、オフラインインストールが困難になりました。インターネットに接続したPCでいったんWebインストーラーを動かしオフラインイメージをダウンロード、それを検疫等必要な処置をして開発環境へ持ち込み、開発PCへインストールする手順は用意されていますが、コード署名の証明書が追加で必要なるとか、ダウンロードしたPCと構成が違うときにエラーになるといったトラブルが生じることがあります。また、インターネットに接続するPCでWebインストーラーを実行できるかは組織のセキュリティポリシーにより許されないこともあります。

*3:過去にあったのが、アセンブリ(DLLファイル)にコード署名が施されており、ツールを起動するたびにコード署名の検証をするがその際インターネット接続を試み、タイムアウトが発生してから起動するというもの。ツールの起動に数分待たされてしまいました。

*4:すごい大変

*5:キャッシュしたコマンドは実行可能だが、切り離してから始めて使うコマンドはキャッシュがないのでエラーになってしまう

JavaFXアプリケーションでユーザーが編集可能なCSSファイルを置くには

問題

JavaFXアプリケーションを作成する際に、画面のレイアウトをFXMLで記述、色やフォントなどの見栄えをCSSファイルに記述し、FXMLのルートノードにCSSファイルを指定しました。

模式的なソースファイル構成は次のようになります。

myapp
  +-- src
        +-- mypackage
              +-- MyApp.java
              +-- MyApp.fxml
              +-- MyApp.css

MyApp.javaの中でFXMLファイルを読み込みます。

Parent root = FXMLLoader.load(getClass().getResource("MyApp.fxml"));
Scene scene = new Scene(root);

MyApp.fxml の中でCSSファイルを指定します。

<HBox stylesheets="@MyApp.css" xmlns=...>

このアプリケーションをビルドすると、CSSファイルは他のクラスファイル等と一緒にJARファイルまたはjlinkでモジュールファイルに含まれます。

このアプリケーションを実行するマシンに配布したあとに、画面の見栄えをカスタマイズしようとCSSファイルを取り出して編集、再組み込みすることは簡単ではありません。
JARファイルならzipアーカイブツールでCSSファイルを取り出し編集後再度JARファイルに戻すことは可能です(かなり面倒です)。
jlinkのモジュールファイルの場合、zipではないので取り出し編集は困難です。

そこで、ビルド時にJARファイルまたはモジュールファイルに組み込まれるデフォルトの設定を記述したCSSファイルとは別に、独立したCSSファイルをアプリケーション実行時に読み込み反映する方法を探ってみました。

sceneに別CSSファイルを設定するも反映されず

カレントディレクトリにMyCustom.cssファイルが存在すればそれをSceneのスタイルシートとして設定するコードを追加しました。

Path path = Paths.get("MyCustom.css");
if (Files.exists(path)) {
    scene.getStylesheets().add(path.toUri().toString());
}

しかし、これではMyCustom.cssに記述したカスタム設定が反映されませんでした。

このコードでは、SceneとそのrootノードのHBoxにそれぞれ別のスタイルシートが設定されます。

Scene         ---> MyCustom.css
  +-- HBox    ---> MyApp.css

試行錯誤と資料調査をしたところ、JavaFXのドキュメント「JavaFX CSS Reference Guide」に次の記述を見つけました。

Style sheets from a Parent instance are considered to be more specific than those styles from Scene style sheets.

つまり、Sceneのスタイルシートよりもその子ノード(rootノード)のスタイルシートの方が優先されてしまいます

sceneのrootノードにスタイルシートを設定する

そこで、Sceneのrootノードに対してスタイルシートを設定するようにしました。

Path path = Paths.get("MyCustom.css");
if (Files.exists(path)) {
    scene.getRoot().getStylesheets().add(path.toUri().toString());
}

これで、アプリケーションを実行するときのカレントディレクトリにMyCustom.cssファイルが存在すれば、その内容が反映されるという仕組みができました。